@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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -95
- package/dist/analytics/{emit.d.ts → claude/emit.d.ts} +1 -1
- package/dist/analytics/claude/emit.d.ts.map +1 -0
- package/dist/analytics/{emit.js → claude/emit.js} +34 -7
- package/dist/analytics/claude/emit.js.map +1 -0
- package/dist/analytics/{hook-trigger.d.ts → claude/hook-trigger.d.ts} +1 -1
- package/dist/analytics/claude/hook-trigger.d.ts.map +1 -0
- package/dist/analytics/{hook-trigger.js → claude/hook-trigger.js} +2 -2
- package/dist/analytics/claude/hook-trigger.js.map +1 -0
- package/dist/analytics/claude/log.d.ts.map +1 -0
- package/dist/analytics/{log.js → claude/log.js} +1 -1
- package/dist/analytics/claude/log.js.map +1 -0
- package/dist/analytics/{merge.d.ts → claude/merge.d.ts} +2 -1
- package/dist/analytics/claude/merge.d.ts.map +1 -0
- package/dist/analytics/{merge.js → claude/merge.js} +13 -1
- package/dist/analytics/claude/merge.js.map +1 -0
- package/dist/analytics/{pricing.d.ts → claude/pricing.d.ts} +1 -13
- package/dist/analytics/claude/pricing.d.ts.map +1 -0
- package/dist/analytics/{pricing.js → claude/pricing.js} +6 -14
- package/dist/analytics/claude/pricing.js.map +1 -0
- package/dist/analytics/{projection.d.ts → claude/projection.d.ts} +31 -7
- package/dist/analytics/claude/projection.d.ts.map +1 -0
- package/dist/analytics/{projection.js → claude/projection.js} +631 -327
- package/dist/analytics/claude/projection.js.map +1 -0
- package/dist/analytics/{spawn.d.ts → claude/spawn.d.ts} +4 -4
- package/dist/analytics/claude/spawn.d.ts.map +1 -0
- package/dist/analytics/{spawn.js → claude/spawn.js} +4 -3
- package/dist/analytics/claude/spawn.js.map +1 -0
- package/dist/analytics/{state.d.ts → claude/state.d.ts} +1 -1
- package/dist/analytics/claude/state.d.ts.map +1 -0
- package/dist/analytics/{state.js → claude/state.js} +2 -2
- package/dist/analytics/claude/state.js.map +1 -0
- package/dist/analytics/claude/transcript.d.ts.map +1 -0
- package/dist/analytics/{transcript.js → claude/transcript.js} +1 -1
- package/dist/analytics/claude/transcript.js.map +1 -0
- package/dist/analytics/codex/api-request.d.ts +108 -0
- package/dist/analytics/codex/api-request.d.ts.map +1 -0
- package/dist/analytics/codex/api-request.js +155 -0
- package/dist/analytics/codex/api-request.js.map +1 -0
- package/dist/analytics/codex/apply-patch.d.ts +21 -0
- package/dist/analytics/codex/apply-patch.d.ts.map +1 -0
- package/dist/analytics/codex/apply-patch.js +49 -0
- package/dist/analytics/codex/apply-patch.js.map +1 -0
- package/dist/analytics/codex/classifier.d.ts +28 -0
- package/dist/analytics/codex/classifier.d.ts.map +1 -0
- package/dist/analytics/codex/classifier.js +111 -0
- package/dist/analytics/codex/classifier.js.map +1 -0
- package/dist/analytics/codex/emit.d.ts +47 -0
- package/dist/analytics/codex/emit.d.ts.map +1 -0
- package/dist/analytics/codex/emit.js +158 -0
- package/dist/analytics/codex/emit.js.map +1 -0
- package/dist/analytics/codex/events-emit.d.ts +62 -0
- package/dist/analytics/codex/events-emit.d.ts.map +1 -0
- package/dist/analytics/codex/events-emit.js +555 -0
- package/dist/analytics/codex/events-emit.js.map +1 -0
- package/dist/analytics/codex/pricing.d.ts +57 -0
- package/dist/analytics/codex/pricing.d.ts.map +1 -0
- package/dist/analytics/codex/pricing.js +125 -0
- package/dist/analytics/codex/pricing.js.map +1 -0
- package/dist/analytics/codex/projection.d.ts +51 -0
- package/dist/analytics/codex/projection.d.ts.map +1 -0
- package/dist/analytics/codex/projection.js +1477 -0
- package/dist/analytics/codex/projection.js.map +1 -0
- package/dist/analytics/codex/spawn.d.ts +27 -0
- package/dist/analytics/codex/spawn.d.ts.map +1 -0
- package/dist/analytics/codex/spawn.js +64 -0
- package/dist/analytics/codex/spawn.js.map +1 -0
- package/dist/analytics/codex/status-snapshot.d.ts +80 -0
- package/dist/analytics/codex/status-snapshot.d.ts.map +1 -0
- package/dist/analytics/codex/status-snapshot.js +206 -0
- package/dist/analytics/codex/status-snapshot.js.map +1 -0
- package/dist/analytics/codex/transcript.d.ts +51 -0
- package/dist/analytics/codex/transcript.d.ts.map +1 -0
- package/dist/analytics/codex/transcript.js +134 -0
- package/dist/analytics/codex/transcript.js.map +1 -0
- package/dist/analytics/codex/types.d.ts +253 -0
- package/dist/analytics/codex/types.d.ts.map +1 -0
- package/dist/analytics/codex/types.js +29 -0
- package/dist/analytics/codex/types.js.map +1 -0
- package/dist/analytics/shared/classifier.d.ts.map +1 -0
- package/dist/analytics/{classifier.js → shared/classifier.js} +9 -0
- package/dist/analytics/shared/classifier.js.map +1 -0
- package/dist/analytics/shared/errors.d.ts.map +1 -0
- package/dist/analytics/shared/errors.js.map +1 -0
- package/dist/analytics/shared/tokens.d.ts +14 -0
- package/dist/analytics/shared/tokens.d.ts.map +1 -0
- package/dist/analytics/shared/tokens.js +17 -0
- package/dist/analytics/shared/tokens.js.map +1 -0
- package/dist/analytics/{types.d.ts → shared/types.d.ts} +42 -9
- package/dist/analytics/shared/types.d.ts.map +1 -0
- package/dist/analytics/shared/types.js.map +1 -0
- package/dist/clients/base.d.ts +9 -0
- package/dist/clients/base.d.ts.map +1 -1
- package/dist/clients/claude/hooks/activity-end.js +1 -1
- package/dist/clients/claude/hooks/activity-end.js.map +1 -1
- package/dist/clients/claude/hooks/activity-start.js +1 -1
- package/dist/clients/claude/hooks/activity-start.js.map +1 -1
- package/dist/clients/claude/hooks/clear-verdict.d.ts.map +1 -1
- package/dist/clients/claude/hooks/clear-verdict.js +14 -0
- package/dist/clients/claude/hooks/clear-verdict.js.map +1 -1
- package/dist/clients/claude/hooks/session-end.d.ts.map +1 -1
- package/dist/clients/claude/hooks/session-end.js +7 -1
- package/dist/clients/claude/hooks/session-end.js.map +1 -1
- package/dist/clients/claude/hooks/session-start.d.ts.map +1 -1
- package/dist/clients/claude/hooks/session-start.js +7 -1
- package/dist/clients/claude/hooks/session-start.js.map +1 -1
- package/dist/clients/claude/hooks/session-status.d.ts.map +1 -1
- package/dist/clients/claude/hooks/session-status.js +13 -9
- package/dist/clients/claude/hooks/session-status.js.map +1 -1
- package/dist/clients/claude/hooks/track-action.d.ts.map +1 -1
- package/dist/clients/claude/hooks/track-action.js +26 -1
- package/dist/clients/claude/hooks/track-action.js.map +1 -1
- package/dist/clients/claude/hooks/verify-gate.d.ts.map +1 -1
- package/dist/clients/claude/hooks/verify-gate.js +8 -1
- package/dist/clients/claude/hooks/verify-gate.js.map +1 -1
- package/dist/clients/claude/index.d.ts +1 -0
- package/dist/clients/claude/index.d.ts.map +1 -1
- package/dist/clients/claude/index.js +18 -14
- package/dist/clients/claude/index.js.map +1 -1
- package/dist/clients/claude/util.d.ts.map +1 -1
- package/dist/clients/claude/util.js +55 -0
- package/dist/clients/claude/util.js.map +1 -1
- package/dist/clients/codex/commands/ironbee-verify/SKILL.md +58 -0
- package/dist/clients/codex/hooks/activity-end.d.ts +9 -0
- package/dist/clients/codex/hooks/activity-end.d.ts.map +1 -0
- package/dist/clients/codex/hooks/activity-end.js +65 -0
- package/dist/clients/codex/hooks/activity-end.js.map +1 -0
- package/dist/clients/codex/hooks/activity-start.d.ts +17 -0
- package/dist/clients/codex/hooks/activity-start.d.ts.map +1 -0
- package/dist/clients/codex/hooks/activity-start.js +38 -0
- package/dist/clients/codex/hooks/activity-start.js.map +1 -0
- package/dist/clients/codex/hooks/clear-verdict.d.ts +55 -0
- package/dist/clients/codex/hooks/clear-verdict.d.ts.map +1 -0
- package/dist/clients/codex/hooks/clear-verdict.js +299 -0
- package/dist/clients/codex/hooks/clear-verdict.js.map +1 -0
- package/dist/clients/codex/hooks/require-verdict.d.ts +30 -0
- package/dist/clients/codex/hooks/require-verdict.d.ts.map +1 -0
- package/dist/clients/codex/hooks/require-verdict.js +109 -0
- package/dist/clients/codex/hooks/require-verdict.js.map +1 -0
- package/dist/clients/codex/hooks/require-verification.d.ts +12 -0
- package/dist/clients/codex/hooks/require-verification.d.ts.map +1 -0
- package/dist/clients/codex/hooks/require-verification.js +136 -0
- package/dist/clients/codex/hooks/require-verification.js.map +1 -0
- package/dist/clients/codex/hooks/session-start.d.ts +10 -0
- package/dist/clients/codex/hooks/session-start.d.ts.map +1 -0
- package/dist/clients/codex/hooks/session-start.js +94 -0
- package/dist/clients/codex/hooks/session-start.js.map +1 -0
- package/dist/clients/codex/hooks/track-action-monitor.d.ts +10 -0
- package/dist/clients/codex/hooks/track-action-monitor.d.ts.map +1 -0
- package/dist/clients/codex/hooks/track-action-monitor.js +168 -0
- package/dist/clients/codex/hooks/track-action-monitor.js.map +1 -0
- package/dist/clients/codex/hooks/track-action-pre.d.ts +18 -0
- package/dist/clients/codex/hooks/track-action-pre.d.ts.map +1 -0
- package/dist/clients/codex/hooks/track-action-pre.js +35 -0
- package/dist/clients/codex/hooks/track-action-pre.js.map +1 -0
- package/dist/clients/codex/hooks/track-action.d.ts +22 -0
- package/dist/clients/codex/hooks/track-action.d.ts.map +1 -0
- package/dist/clients/codex/hooks/track-action.js +350 -0
- package/dist/clients/codex/hooks/track-action.js.map +1 -0
- package/dist/clients/codex/hooks/verify-gate.d.ts +15 -0
- package/dist/clients/codex/hooks/verify-gate.d.ts.map +1 -0
- package/dist/clients/codex/hooks/verify-gate.js +105 -0
- package/dist/clients/codex/hooks/verify-gate.js.map +1 -0
- package/dist/clients/codex/index.d.ts +42 -0
- package/dist/clients/codex/index.d.ts.map +1 -0
- package/dist/clients/codex/index.js +427 -0
- package/dist/clients/codex/index.js.map +1 -0
- package/dist/clients/codex/platforms/command-verify.backend.md +108 -0
- package/dist/clients/codex/platforms/command-verify.browser.md +108 -0
- package/dist/clients/codex/platforms/command-verify.node.md +61 -0
- package/dist/clients/codex/platforms/rule.backend.md +32 -0
- package/dist/clients/codex/platforms/rule.browser.md +17 -0
- package/dist/clients/codex/platforms/rule.node.md +28 -0
- package/dist/clients/codex/platforms/skill.backend.md +95 -0
- package/dist/clients/codex/platforms/skill.browser.md +28 -0
- package/dist/clients/codex/platforms/skill.node.md +62 -0
- package/dist/clients/codex/rules/ironbee-verification.md +48 -0
- package/dist/clients/codex/skills/ironbee-verification.md +80 -0
- package/dist/clients/codex/util.d.ts +193 -0
- package/dist/clients/codex/util.d.ts.map +1 -0
- package/dist/clients/codex/util.js +784 -0
- package/dist/clients/codex/util.js.map +1 -0
- package/dist/clients/cursor/hooks/activity-end.js +1 -1
- package/dist/clients/cursor/hooks/activity-end.js.map +1 -1
- package/dist/clients/cursor/hooks/clear-verdict.d.ts +5 -2
- package/dist/clients/cursor/hooks/clear-verdict.d.ts.map +1 -1
- package/dist/clients/cursor/hooks/clear-verdict.js +12 -3
- package/dist/clients/cursor/hooks/clear-verdict.js.map +1 -1
- package/dist/clients/cursor/hooks/session-end.js +1 -1
- package/dist/clients/cursor/hooks/session-end.js.map +1 -1
- package/dist/clients/cursor/hooks/session-start.js +1 -1
- package/dist/clients/cursor/hooks/session-start.js.map +1 -1
- package/dist/clients/cursor/hooks/verify-gate.d.ts.map +1 -1
- package/dist/clients/cursor/hooks/verify-gate.js +6 -1
- package/dist/clients/cursor/hooks/verify-gate.js.map +1 -1
- package/dist/clients/cursor/index.d.ts +1 -0
- package/dist/clients/cursor/index.d.ts.map +1 -1
- package/dist/clients/cursor/index.js +22 -13
- package/dist/clients/cursor/index.js.map +1 -1
- package/dist/clients/registry.d.ts.map +1 -1
- package/dist/clients/registry.js +2 -1
- package/dist/clients/registry.js.map +1 -1
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/claude/index.d.ts.map +1 -0
- package/dist/commands/{claude.js → claude/index.js} +12 -6
- package/dist/commands/claude/index.js.map +1 -0
- package/dist/commands/{otel.d.ts → claude/otel.d.ts} +5 -1
- package/dist/commands/claude/otel.d.ts.map +1 -0
- package/dist/commands/{otel.js → claude/otel.js} +9 -5
- package/dist/commands/claude/otel.js.map +1 -0
- package/dist/commands/claude/process-analytics.d.ts +19 -0
- package/dist/commands/claude/process-analytics.d.ts.map +1 -0
- package/dist/commands/{process-analytics.js → claude/process-analytics.js} +16 -15
- package/dist/commands/claude/process-analytics.js.map +1 -0
- package/dist/commands/{statusline-toggle.d.ts → claude/statusline-toggle.d.ts} +2 -2
- package/dist/commands/claude/statusline-toggle.d.ts.map +1 -0
- package/dist/commands/{statusline-toggle.js → claude/statusline-toggle.js} +8 -8
- package/dist/commands/claude/statusline-toggle.js.map +1 -0
- package/dist/commands/{statusline.d.ts → claude/statusline.d.ts} +1 -1
- package/dist/commands/claude/statusline.d.ts.map +1 -0
- package/dist/commands/{statusline.js → claude/statusline.js} +4 -4
- package/dist/commands/claude/statusline.js.map +1 -0
- package/dist/commands/codex/index.d.ts +11 -0
- package/dist/commands/codex/index.d.ts.map +1 -0
- package/dist/commands/codex/index.js +17 -0
- package/dist/commands/codex/index.js.map +1 -0
- package/dist/commands/codex/process-analytics.d.ts +14 -0
- package/dist/commands/codex/process-analytics.d.ts.map +1 -0
- package/dist/commands/codex/process-analytics.js +111 -0
- package/dist/commands/codex/process-analytics.js.map +1 -0
- package/dist/commands/hook.js +12 -0
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/import.js +3 -3
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/queue.js +3 -1
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +1 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/hooks/core/actions.d.ts +17 -1
- package/dist/hooks/core/actions.d.ts.map +1 -1
- package/dist/hooks/core/actions.js +13 -0
- package/dist/hooks/core/actions.js.map +1 -1
- package/dist/hooks/core/activity-end.d.ts.map +1 -1
- package/dist/hooks/core/activity-end.js +4 -0
- package/dist/hooks/core/activity-end.js.map +1 -1
- package/dist/hooks/core/session-state.d.ts +15 -1
- package/dist/hooks/core/session-state.d.ts.map +1 -1
- package/dist/hooks/core/session-state.js +102 -7
- package/dist/hooks/core/session-state.js.map +1 -1
- package/dist/import/claude/analytics-runner.d.ts +1 -1
- package/dist/import/claude/analytics-runner.d.ts.map +1 -1
- package/dist/import/claude/analytics-runner.js +5 -5
- package/dist/import/claude/analytics-runner.js.map +1 -1
- package/dist/import/claude/auth-mode.d.ts +1 -1
- package/dist/import/claude/auth-mode.d.ts.map +1 -1
- package/dist/import/claude/discovery.js +1 -1
- package/dist/import/claude/discovery.js.map +1 -1
- package/dist/import/claude/encoding.js +1 -1
- package/dist/import/claude/encoding.js.map +1 -1
- package/dist/import/claude/events/file-change.d.ts +10 -1
- package/dist/import/claude/events/file-change.d.ts.map +1 -1
- package/dist/import/claude/events/file-change.js +79 -5
- package/dist/import/claude/events/file-change.js.map +1 -1
- package/dist/import/claude/events/tool-call.d.ts +16 -1
- package/dist/import/claude/events/tool-call.d.ts.map +1 -1
- package/dist/import/claude/events/tool-call.js +122 -15
- package/dist/import/claude/events/tool-call.js.map +1 -1
- package/dist/import/claude/runner.d.ts.map +1 -1
- package/dist/import/claude/runner.js +45 -3
- package/dist/import/claude/runner.js.map +1 -1
- package/dist/import/claude/summary.js +1 -1
- package/dist/import/claude/summary.js.map +1 -1
- package/dist/import/claude/transcript-walk.d.ts +1 -1
- package/dist/import/claude/transcript-walk.d.ts.map +1 -1
- package/dist/import/claude/transcript-walk.js +11 -4
- package/dist/import/claude/transcript-walk.js.map +1 -1
- package/dist/import/codex/analytics-runner.d.ts +46 -0
- package/dist/import/codex/analytics-runner.d.ts.map +1 -0
- package/dist/import/codex/analytics-runner.js +116 -0
- package/dist/import/codex/analytics-runner.js.map +1 -0
- package/dist/import/codex/discovery.d.ts +33 -0
- package/dist/import/codex/discovery.d.ts.map +1 -0
- package/dist/import/codex/discovery.js +202 -0
- package/dist/import/codex/discovery.js.map +1 -0
- package/dist/import/codex/events/file-change.d.ts +42 -0
- package/dist/import/codex/events/file-change.d.ts.map +1 -0
- package/dist/import/codex/events/file-change.js +125 -0
- package/dist/import/codex/events/file-change.js.map +1 -0
- package/dist/import/codex/events/tool-call.d.ts +49 -0
- package/dist/import/codex/events/tool-call.d.ts.map +1 -0
- package/dist/import/codex/events/tool-call.js +151 -0
- package/dist/import/codex/events/tool-call.js.map +1 -0
- package/dist/import/codex/runner.d.ts +34 -0
- package/dist/import/codex/runner.d.ts.map +1 -0
- package/dist/import/codex/runner.js +456 -0
- package/dist/import/codex/runner.js.map +1 -0
- package/dist/import/codex/summary.d.ts +20 -0
- package/dist/import/codex/summary.d.ts.map +1 -0
- package/dist/import/codex/summary.js +206 -0
- package/dist/import/codex/summary.js.map +1 -0
- package/dist/import/events/activity.d.ts.map +1 -1
- package/dist/import/events/activity.js +17 -2
- package/dist/import/events/activity.js.map +1 -1
- package/dist/import/events/session.d.ts +11 -1
- package/dist/import/events/session.d.ts.map +1 -1
- package/dist/import/events/session.js +19 -1
- package/dist/import/events/session.js.map +1 -1
- package/dist/import/ids.js +3 -3
- package/dist/import/ids.js.map +1 -1
- package/dist/import/pipeline.d.ts +22 -15
- package/dist/import/pipeline.d.ts.map +1 -1
- package/dist/import/pipeline.js +99 -18
- package/dist/import/pipeline.js.map +1 -1
- package/dist/import/types.d.ts +4 -0
- package/dist/import/types.d.ts.map +1 -1
- package/dist/import/types.js.map +1 -1
- package/dist/index.js +9 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/collector.d.ts +2 -1
- package/dist/lib/collector.d.ts.map +1 -1
- package/dist/lib/collector.js +28 -3
- package/dist/lib/collector.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/event.d.ts +18 -1
- package/dist/lib/event.d.ts.map +1 -1
- package/dist/lib/event.js +25 -1
- package/dist/lib/event.js.map +1 -1
- package/dist/lib/fs-prune.d.ts +1 -1
- package/dist/lib/fs-prune.js +1 -1
- package/dist/lib/platform-section.d.ts.map +1 -1
- package/dist/lib/platform-section.js +8 -0
- package/dist/lib/platform-section.js.map +1 -1
- package/dist/otel/{context → claude/context}/build.d.ts +1 -1
- package/dist/otel/claude/context/build.d.ts.map +1 -0
- package/dist/otel/{context → claude/context}/build.js +3 -7
- package/dist/otel/claude/context/build.js.map +1 -0
- package/dist/otel/claude/context/classify.d.ts.map +1 -0
- package/dist/otel/claude/context/classify.js.map +1 -0
- package/dist/otel/{context → claude/context}/extract.d.ts +1 -1
- package/dist/otel/claude/context/extract.d.ts.map +1 -0
- package/dist/otel/claude/context/extract.js.map +1 -0
- package/dist/otel/claude/context/markers.d.ts.map +1 -0
- package/dist/otel/{context → claude/context}/markers.js +22 -3
- package/dist/otel/claude/context/markers.js.map +1 -0
- package/dist/otel/claude/context/util.d.ts.map +1 -0
- package/dist/otel/claude/context/util.js.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/ensure.d.ts +1 -1
- package/dist/otel/claude/daemon/ensure.d.ts.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/ensure.js +6 -6
- package/dist/otel/claude/daemon/ensure.js.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/forward.d.ts +1 -1
- package/dist/otel/claude/daemon/forward.d.ts.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/forward.js +0 -0
- package/dist/otel/claude/daemon/forward.js.map +1 -0
- package/dist/otel/claude/daemon/paths.d.ts.map +1 -0
- package/dist/otel/claude/daemon/paths.js.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/process.d.ts +1 -1
- package/dist/otel/claude/daemon/process.d.ts.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/process.js +1 -1
- package/dist/otel/claude/daemon/process.js.map +1 -0
- package/dist/otel/claude/daemon/reprocess.d.ts.map +1 -0
- package/dist/otel/{daemon → claude/daemon}/reprocess.js +2 -2
- package/dist/otel/claude/daemon/reprocess.js.map +1 -0
- package/dist/otel/claude/log-handler.d.ts.map +1 -0
- package/dist/otel/{log-handler.js → claude/log-handler.js} +1 -1
- package/dist/otel/claude/log-handler.js.map +1 -0
- package/dist/otel/collector.js +4 -4
- package/dist/otel/collector.js.map +1 -1
- package/dist/queue/flush.d.ts +23 -0
- package/dist/queue/flush.d.ts.map +1 -1
- package/dist/queue/flush.js +44 -0
- package/dist/queue/flush.js.map +1 -1
- package/dist/queue/handlers/send-event.d.ts.map +1 -1
- package/dist/queue/handlers/send-event.js +5 -4
- package/dist/queue/handlers/send-event.js.map +1 -1
- package/dist/queue/index.d.ts +2 -2
- package/dist/queue/index.d.ts.map +1 -1
- package/dist/queue/index.js +4 -1
- package/dist/queue/index.js.map +1 -1
- package/dist/queue/spawn.d.ts +20 -0
- package/dist/queue/spawn.d.ts.map +1 -1
- package/dist/queue/spawn.js +37 -0
- package/dist/queue/spawn.js.map +1 -1
- package/dist/tui/import/area.js +3 -3
- package/dist/tui/import/area.js.map +1 -1
- package/dist/tui/sessions/area.d.ts.map +1 -1
- package/dist/tui/sessions/area.js +2 -45
- package/dist/tui/sessions/area.js.map +1 -1
- package/package.json +2 -1
- package/dist/analysis/code-changes.d.ts +0 -22
- package/dist/analysis/code-changes.d.ts.map +0 -1
- package/dist/analysis/code-changes.js +0 -141
- package/dist/analysis/code-changes.js.map +0 -1
- package/dist/analysis/cross-session.d.ts +0 -34
- package/dist/analysis/cross-session.d.ts.map +0 -1
- package/dist/analysis/cross-session.js +0 -230
- package/dist/analysis/cross-session.js.map +0 -1
- package/dist/analysis/fix-effectiveness.d.ts +0 -16
- package/dist/analysis/fix-effectiveness.d.ts.map +0 -1
- package/dist/analysis/fix-effectiveness.js +0 -99
- package/dist/analysis/fix-effectiveness.js.map +0 -1
- package/dist/analysis/scoring.d.ts +0 -15
- package/dist/analysis/scoring.d.ts.map +0 -1
- package/dist/analysis/scoring.js +0 -59
- package/dist/analysis/scoring.js.map +0 -1
- package/dist/analysis/time-analysis.d.ts +0 -22
- package/dist/analysis/time-analysis.d.ts.map +0 -1
- package/dist/analysis/time-analysis.js +0 -174
- package/dist/analysis/time-analysis.js.map +0 -1
- package/dist/analysis/verdict-details.d.ts +0 -23
- package/dist/analysis/verdict-details.d.ts.map +0 -1
- package/dist/analysis/verdict-details.js +0 -59
- package/dist/analysis/verdict-details.js.map +0 -1
- package/dist/analysis/verification-quality.d.ts +0 -20
- package/dist/analysis/verification-quality.d.ts.map +0 -1
- package/dist/analysis/verification-quality.js +0 -145
- package/dist/analysis/verification-quality.js.map +0 -1
- package/dist/analytics/classifier.d.ts.map +0 -1
- package/dist/analytics/classifier.js.map +0 -1
- package/dist/analytics/emit.d.ts.map +0 -1
- package/dist/analytics/emit.js.map +0 -1
- package/dist/analytics/errors.d.ts.map +0 -1
- package/dist/analytics/errors.js.map +0 -1
- package/dist/analytics/hook-trigger.d.ts.map +0 -1
- package/dist/analytics/hook-trigger.js.map +0 -1
- package/dist/analytics/log.d.ts.map +0 -1
- package/dist/analytics/log.js.map +0 -1
- package/dist/analytics/merge.d.ts.map +0 -1
- package/dist/analytics/merge.js.map +0 -1
- package/dist/analytics/pricing.d.ts.map +0 -1
- package/dist/analytics/pricing.js.map +0 -1
- package/dist/analytics/projection.d.ts.map +0 -1
- package/dist/analytics/projection.js.map +0 -1
- package/dist/analytics/spawn.d.ts.map +0 -1
- package/dist/analytics/spawn.js.map +0 -1
- package/dist/analytics/state.d.ts.map +0 -1
- package/dist/analytics/state.js.map +0 -1
- package/dist/analytics/transcript.d.ts.map +0 -1
- package/dist/analytics/transcript.js.map +0 -1
- package/dist/analytics/types.d.ts.map +0 -1
- package/dist/analytics/types.js.map +0 -1
- package/dist/clients/claude/commands/ironbee-analyze.md +0 -42
- package/dist/clients/cursor/commands/ironbee-analyze/SKILL.md +0 -48
- package/dist/commands/analyze.d.ts +0 -3
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -329
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/claude.d.ts.map +0 -1
- package/dist/commands/claude.js.map +0 -1
- package/dist/commands/otel.d.ts.map +0 -1
- package/dist/commands/otel.js.map +0 -1
- package/dist/commands/process-analytics.d.ts +0 -18
- package/dist/commands/process-analytics.d.ts.map +0 -1
- package/dist/commands/process-analytics.js.map +0 -1
- package/dist/commands/statusline-toggle.d.ts.map +0 -1
- package/dist/commands/statusline-toggle.js.map +0 -1
- package/dist/commands/statusline.d.ts.map +0 -1
- package/dist/commands/statusline.js.map +0 -1
- package/dist/otel/context/build.d.ts.map +0 -1
- package/dist/otel/context/build.js.map +0 -1
- package/dist/otel/context/classify.d.ts.map +0 -1
- package/dist/otel/context/classify.js.map +0 -1
- package/dist/otel/context/extract.d.ts.map +0 -1
- package/dist/otel/context/extract.js.map +0 -1
- package/dist/otel/context/markers.d.ts.map +0 -1
- package/dist/otel/context/markers.js.map +0 -1
- package/dist/otel/context/util.d.ts.map +0 -1
- package/dist/otel/context/util.js.map +0 -1
- package/dist/otel/daemon/ensure.d.ts.map +0 -1
- package/dist/otel/daemon/ensure.js.map +0 -1
- package/dist/otel/daemon/forward.d.ts.map +0 -1
- package/dist/otel/daemon/forward.js.map +0 -1
- package/dist/otel/daemon/paths.d.ts.map +0 -1
- package/dist/otel/daemon/paths.js.map +0 -1
- package/dist/otel/daemon/process.d.ts.map +0 -1
- package/dist/otel/daemon/process.js.map +0 -1
- package/dist/otel/daemon/reprocess.d.ts.map +0 -1
- package/dist/otel/daemon/reprocess.js.map +0 -1
- package/dist/otel/log-handler.d.ts.map +0 -1
- package/dist/otel/log-handler.js.map +0 -1
- /package/dist/analytics/{log.d.ts → claude/log.d.ts} +0 -0
- /package/dist/analytics/{transcript.d.ts → claude/transcript.d.ts} +0 -0
- /package/dist/analytics/{classifier.d.ts → shared/classifier.d.ts} +0 -0
- /package/dist/analytics/{errors.d.ts → shared/errors.d.ts} +0 -0
- /package/dist/analytics/{errors.js → shared/errors.js} +0 -0
- /package/dist/analytics/{types.js → shared/types.js} +0 -0
- /package/dist/otel/{context → claude/context}/classify.d.ts +0 -0
- /package/dist/otel/{context → claude/context}/classify.js +0 -0
- /package/dist/otel/{context → claude/context}/extract.js +0 -0
- /package/dist/otel/{context → claude/context}/markers.d.ts +0 -0
- /package/dist/otel/{context → claude/context}/util.d.ts +0 -0
- /package/dist/otel/{context → claude/context}/util.js +0 -0
- /package/dist/otel/{daemon → claude/daemon}/paths.d.ts +0 -0
- /package/dist/otel/{daemon → claude/daemon}/paths.js +0 -0
- /package/dist/otel/{daemon → claude/daemon}/reprocess.d.ts +0 -0
- /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
|