@newrelic/preflight 0.0.1-pre.1 → 1.0.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/LICENSE +183 -0
- package/README.md +498 -0
- package/dist/alerts/alert-log.d.ts +24 -0
- package/dist/alerts/alert-log.d.ts.map +1 -0
- package/dist/alerts/alert-log.js +159 -0
- package/dist/alerts/alert-log.js.map +1 -0
- package/dist/alerts/alert-snapshot-collector.d.ts +168 -0
- package/dist/alerts/alert-snapshot-collector.d.ts.map +1 -0
- package/dist/alerts/alert-snapshot-collector.js +243 -0
- package/dist/alerts/alert-snapshot-collector.js.map +1 -0
- package/dist/alerts/local-alert-engine.d.ts +86 -0
- package/dist/alerts/local-alert-engine.d.ts.map +1 -0
- package/dist/alerts/local-alert-engine.js +466 -0
- package/dist/alerts/local-alert-engine.js.map +1 -0
- package/dist/alerts/local-alert-rule.d.ts +439 -0
- package/dist/alerts/local-alert-rule.d.ts.map +1 -0
- package/dist/alerts/local-alert-rule.js +139 -0
- package/dist/alerts/local-alert-rule.js.map +1 -0
- package/dist/alerts/os-notifier.d.ts +39 -0
- package/dist/alerts/os-notifier.d.ts.map +1 -0
- package/dist/alerts/os-notifier.js +170 -0
- package/dist/alerts/os-notifier.js.map +1 -0
- package/dist/alerts/types.d.ts +35 -0
- package/dist/alerts/types.d.ts.map +1 -0
- package/dist/alerts/types.js +8 -0
- package/dist/alerts/types.js.map +1 -0
- package/dist/config.d.ts +169 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +860 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/dashboard-server.d.ts +38 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard/dashboard-server.js +207 -0
- package/dist/dashboard/dashboard-server.js.map +1 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/live-event-bus.d.ts +99 -0
- package/dist/dashboard/live-event-bus.d.ts.map +1 -0
- package/dist/dashboard/live-event-bus.js +56 -0
- package/dist/dashboard/live-event-bus.js.map +1 -0
- package/dist/dashboard/routes/api-handler.d.ts +122 -0
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/api-handler.js +1414 -0
- package/dist/dashboard/routes/api-handler.js.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts +15 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.js +227 -0
- package/dist/dashboard/routes/replay-analyzer.js.map +1 -0
- package/dist/dashboard/routes/sse-handler.d.ts +4 -0
- package/dist/dashboard/routes/sse-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/sse-handler.js +122 -0
- package/dist/dashboard/routes/sse-handler.js.map +1 -0
- package/dist/dashboard/routes/static-handler.d.ts +3 -0
- package/dist/dashboard/routes/static-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/static-handler.js +103 -0
- package/dist/dashboard/routes/static-handler.js.map +1 -0
- package/dist/data/alerts/conditions/01-daily-cost-spike.json +16 -0
- package/dist/data/alerts/conditions/02-low-efficiency-score.json +16 -0
- package/dist/data/alerts/conditions/03-stuck-loop-rate.json +16 -0
- package/dist/data/alerts/conditions/04-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions/05-session-cost-budget.json +16 -0
- package/dist/data/alerts/conditions-personal/01-personal-daily-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/02-personal-session-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/03-personal-low-efficiency.json +16 -0
- package/dist/data/alerts/conditions-personal/04-personal-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions-personal/05-personal-stuck-loop.json +16 -0
- package/dist/data/alerts/policy.json +4 -0
- package/dist/data/dashboards/ai-coding-assistant-manager-view.json +103 -0
- package/dist/data/dashboards/ai-coding-assistant-overview.json +239 -0
- package/dist/data/dashboards/ai-coding-assistant-personal.json +442 -0
- package/dist/data/dashboards/ai-coding-assistant-platform-comparison.json +320 -0
- package/dist/data/dashboards/ai-coding-assistant-security.json +275 -0
- package/dist/data/dashboards/ai-coding-assistant-session-detail.json +296 -0
- package/dist/data/dashboards/ai-coding-assistant-team-view.json +345 -0
- package/dist/deploy/data-paths.d.ts +22 -0
- package/dist/deploy/data-paths.d.ts.map +1 -0
- package/dist/deploy/data-paths.js +69 -0
- package/dist/deploy/data-paths.js.map +1 -0
- package/dist/deploy/deploy-alerts.d.ts +58 -0
- package/dist/deploy/deploy-alerts.d.ts.map +1 -0
- package/dist/deploy/deploy-alerts.js +371 -0
- package/dist/deploy/deploy-alerts.js.map +1 -0
- package/dist/deploy/deploy-dashboards.d.ts +92 -0
- package/dist/deploy/deploy-dashboards.d.ts.map +1 -0
- package/dist/deploy/deploy-dashboards.js +282 -0
- package/dist/deploy/deploy-dashboards.js.map +1 -0
- package/dist/digest/digest-formatter.d.ts +3 -0
- package/dist/digest/digest-formatter.d.ts.map +1 -0
- package/dist/digest/digest-formatter.js +37 -0
- package/dist/digest/digest-formatter.js.map +1 -0
- package/dist/digest/digest-sender.d.ts +2 -0
- package/dist/digest/digest-sender.d.ts.map +1 -0
- package/dist/digest/digest-sender.js +29 -0
- package/dist/digest/digest-sender.js.map +1 -0
- package/dist/hooks/bash-classifier.d.ts +26 -0
- package/dist/hooks/bash-classifier.d.ts.map +1 -0
- package/dist/hooks/bash-classifier.js +409 -0
- package/dist/hooks/bash-classifier.js.map +1 -0
- package/dist/hooks/collector-script.d.ts +47 -0
- package/dist/hooks/collector-script.d.ts.map +1 -0
- package/dist/hooks/collector-script.js +662 -0
- package/dist/hooks/collector-script.js.map +1 -0
- package/dist/hooks/event-processor.d.ts +65 -0
- package/dist/hooks/event-processor.d.ts.map +1 -0
- package/dist/hooks/event-processor.js +342 -0
- package/dist/hooks/event-processor.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/session-resolver.d.ts +66 -0
- package/dist/hooks/session-resolver.d.ts.map +1 -0
- package/dist/hooks/session-resolver.js +196 -0
- package/dist/hooks/session-resolver.js.map +1 -0
- package/dist/hooks/tool-parsers.d.ts +19 -0
- package/dist/hooks/tool-parsers.d.ts.map +1 -0
- package/dist/hooks/tool-parsers.js +260 -0
- package/dist/hooks/tool-parsers.js.map +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1505 -0
- package/dist/index.js.map +1 -0
- package/dist/install/cli.d.ts +11 -0
- package/dist/install/cli.d.ts.map +1 -0
- package/dist/install/cli.js +365 -0
- package/dist/install/cli.js.map +1 -0
- package/dist/install/index.d.ts +4 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +3 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/install-helper.d.ts +35 -0
- package/dist/install/install-helper.d.ts.map +1 -0
- package/dist/install/install-helper.js +227 -0
- package/dist/install/install-helper.js.map +1 -0
- package/dist/install/key-validator.d.ts +19 -0
- package/dist/install/key-validator.d.ts.map +1 -0
- package/dist/install/key-validator.js +122 -0
- package/dist/install/key-validator.js.map +1 -0
- package/dist/install/migrate.d.ts +12 -0
- package/dist/install/migrate.d.ts.map +1 -0
- package/dist/install/migrate.js +115 -0
- package/dist/install/migrate.js.map +1 -0
- package/dist/install/schedule.d.ts +11 -0
- package/dist/install/schedule.d.ts.map +1 -0
- package/dist/install/schedule.js +114 -0
- package/dist/install/schedule.js.map +1 -0
- package/dist/install/setup-wizard.d.ts +40 -0
- package/dist/install/setup-wizard.d.ts.map +1 -0
- package/dist/install/setup-wizard.js +489 -0
- package/dist/install/setup-wizard.js.map +1 -0
- package/dist/lib/date.d.ts +54 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +85 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/metrics/anti-patterns.d.ts +62 -0
- package/dist/metrics/anti-patterns.d.ts.map +1 -0
- package/dist/metrics/anti-patterns.js +301 -0
- package/dist/metrics/anti-patterns.js.map +1 -0
- package/dist/metrics/api-failure-tracker.d.ts +82 -0
- package/dist/metrics/api-failure-tracker.d.ts.map +1 -0
- package/dist/metrics/api-failure-tracker.js +202 -0
- package/dist/metrics/api-failure-tracker.js.map +1 -0
- package/dist/metrics/budget-tracker.d.ts +60 -0
- package/dist/metrics/budget-tracker.d.ts.map +1 -0
- package/dist/metrics/budget-tracker.js +130 -0
- package/dist/metrics/budget-tracker.js.map +1 -0
- package/dist/metrics/claudemd-tracker.d.ts +108 -0
- package/dist/metrics/claudemd-tracker.d.ts.map +1 -0
- package/dist/metrics/claudemd-tracker.js +337 -0
- package/dist/metrics/claudemd-tracker.js.map +1 -0
- package/dist/metrics/collaboration-profile.d.ts +65 -0
- package/dist/metrics/collaboration-profile.d.ts.map +1 -0
- package/dist/metrics/collaboration-profile.js +231 -0
- package/dist/metrics/collaboration-profile.js.map +1 -0
- package/dist/metrics/context-composition-tracker.d.ts +74 -0
- package/dist/metrics/context-composition-tracker.d.ts.map +1 -0
- package/dist/metrics/context-composition-tracker.js +202 -0
- package/dist/metrics/context-composition-tracker.js.map +1 -0
- package/dist/metrics/context-tracker.d.ts +78 -0
- package/dist/metrics/context-tracker.d.ts.map +1 -0
- package/dist/metrics/context-tracker.js +222 -0
- package/dist/metrics/context-tracker.js.map +1 -0
- package/dist/metrics/context-window-tracker.d.ts +18 -0
- package/dist/metrics/context-window-tracker.d.ts.map +1 -0
- package/dist/metrics/context-window-tracker.js +35 -0
- package/dist/metrics/context-window-tracker.js.map +1 -0
- package/dist/metrics/cost-forecast.d.ts +36 -0
- package/dist/metrics/cost-forecast.d.ts.map +1 -0
- package/dist/metrics/cost-forecast.js +91 -0
- package/dist/metrics/cost-forecast.js.map +1 -0
- package/dist/metrics/cost-per-outcome.d.ts +102 -0
- package/dist/metrics/cost-per-outcome.d.ts.map +1 -0
- package/dist/metrics/cost-per-outcome.js +266 -0
- package/dist/metrics/cost-per-outcome.js.map +1 -0
- package/dist/metrics/cost-tracker.d.ts +78 -0
- package/dist/metrics/cost-tracker.d.ts.map +1 -0
- package/dist/metrics/cost-tracker.js +169 -0
- package/dist/metrics/cost-tracker.js.map +1 -0
- package/dist/metrics/decision-tracker.d.ts +49 -0
- package/dist/metrics/decision-tracker.d.ts.map +1 -0
- package/dist/metrics/decision-tracker.js +161 -0
- package/dist/metrics/decision-tracker.js.map +1 -0
- package/dist/metrics/efficiency-score.d.ts +80 -0
- package/dist/metrics/efficiency-score.d.ts.map +1 -0
- package/dist/metrics/efficiency-score.js +219 -0
- package/dist/metrics/efficiency-score.js.map +1 -0
- package/dist/metrics/git-efficiency-tracker.d.ts +165 -0
- package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -0
- package/dist/metrics/git-efficiency-tracker.js +1056 -0
- package/dist/metrics/git-efficiency-tracker.js.map +1 -0
- package/dist/metrics/index.d.ts +26 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +14 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instruction-drift-tracker.d.ts +69 -0
- package/dist/metrics/instruction-drift-tracker.d.ts.map +1 -0
- package/dist/metrics/instruction-drift-tracker.js +213 -0
- package/dist/metrics/instruction-drift-tracker.js.map +1 -0
- package/dist/metrics/latency-decomposition.d.ts +50 -0
- package/dist/metrics/latency-decomposition.d.ts.map +1 -0
- package/dist/metrics/latency-decomposition.js +112 -0
- package/dist/metrics/latency-decomposition.js.map +1 -0
- package/dist/metrics/latency-tracker.d.ts +33 -0
- package/dist/metrics/latency-tracker.d.ts.map +1 -0
- package/dist/metrics/latency-tracker.js +93 -0
- package/dist/metrics/latency-tracker.js.map +1 -0
- package/dist/metrics/live-session-registry.d.ts +29 -0
- package/dist/metrics/live-session-registry.d.ts.map +1 -0
- package/dist/metrics/live-session-registry.js +103 -0
- package/dist/metrics/live-session-registry.js.map +1 -0
- package/dist/metrics/model-usage-tracker.d.ts +21 -0
- package/dist/metrics/model-usage-tracker.d.ts.map +1 -0
- package/dist/metrics/model-usage-tracker.js +53 -0
- package/dist/metrics/model-usage-tracker.js.map +1 -0
- package/dist/metrics/percentile.d.ts +5 -0
- package/dist/metrics/percentile.d.ts.map +1 -0
- package/dist/metrics/percentile.js +10 -0
- package/dist/metrics/percentile.js.map +1 -0
- package/dist/metrics/personal-coach.d.ts +47 -0
- package/dist/metrics/personal-coach.d.ts.map +1 -0
- package/dist/metrics/personal-coach.js +241 -0
- package/dist/metrics/personal-coach.js.map +1 -0
- package/dist/metrics/prompt-feedback.d.ts +75 -0
- package/dist/metrics/prompt-feedback.d.ts.map +1 -0
- package/dist/metrics/prompt-feedback.js +286 -0
- package/dist/metrics/prompt-feedback.js.map +1 -0
- package/dist/metrics/proxy-metrics.d.ts +54 -0
- package/dist/metrics/proxy-metrics.d.ts.map +1 -0
- package/dist/metrics/proxy-metrics.js +228 -0
- package/dist/metrics/proxy-metrics.js.map +1 -0
- package/dist/metrics/quality-proxy-tracker.d.ts +51 -0
- package/dist/metrics/quality-proxy-tracker.d.ts.map +1 -0
- package/dist/metrics/quality-proxy-tracker.js +162 -0
- package/dist/metrics/quality-proxy-tracker.js.map +1 -0
- package/dist/metrics/recommendation-engine.d.ts +72 -0
- package/dist/metrics/recommendation-engine.d.ts.map +1 -0
- package/dist/metrics/recommendation-engine.js +207 -0
- package/dist/metrics/recommendation-engine.js.map +1 -0
- package/dist/metrics/retry-detector.d.ts +43 -0
- package/dist/metrics/retry-detector.d.ts.map +1 -0
- package/dist/metrics/retry-detector.js +179 -0
- package/dist/metrics/retry-detector.js.map +1 -0
- package/dist/metrics/session-tracker.d.ts +75 -0
- package/dist/metrics/session-tracker.d.ts.map +1 -0
- package/dist/metrics/session-tracker.js +249 -0
- package/dist/metrics/session-tracker.js.map +1 -0
- package/dist/metrics/task-completion-tracker.d.ts +15 -0
- package/dist/metrics/task-completion-tracker.d.ts.map +1 -0
- package/dist/metrics/task-completion-tracker.js +27 -0
- package/dist/metrics/task-completion-tracker.js.map +1 -0
- package/dist/metrics/task-detector.d.ts +84 -0
- package/dist/metrics/task-detector.d.ts.map +1 -0
- package/dist/metrics/task-detector.js +302 -0
- package/dist/metrics/task-detector.js.map +1 -0
- package/dist/metrics/tool-selection-scorer.d.ts +39 -0
- package/dist/metrics/tool-selection-scorer.d.ts.map +1 -0
- package/dist/metrics/tool-selection-scorer.js +193 -0
- package/dist/metrics/tool-selection-scorer.js.map +1 -0
- package/dist/metrics/trend-analyzer.d.ts +92 -0
- package/dist/metrics/trend-analyzer.d.ts.map +1 -0
- package/dist/metrics/trend-analyzer.js +293 -0
- package/dist/metrics/trend-analyzer.js.map +1 -0
- package/dist/metrics/turn-cost-attributor.d.ts +41 -0
- package/dist/metrics/turn-cost-attributor.d.ts.map +1 -0
- package/dist/metrics/turn-cost-attributor.js +118 -0
- package/dist/metrics/turn-cost-attributor.js.map +1 -0
- package/dist/metrics/turn-tracker.d.ts +49 -0
- package/dist/metrics/turn-tracker.d.ts.map +1 -0
- package/dist/metrics/turn-tracker.js +192 -0
- package/dist/metrics/turn-tracker.js.map +1 -0
- package/dist/platforms/amazon-q-adapter.d.ts +10 -0
- package/dist/platforms/amazon-q-adapter.d.ts.map +1 -0
- package/dist/platforms/amazon-q-adapter.js +75 -0
- package/dist/platforms/amazon-q-adapter.js.map +1 -0
- package/dist/platforms/claude-code-adapter.d.ts +10 -0
- package/dist/platforms/claude-code-adapter.d.ts.map +1 -0
- package/dist/platforms/claude-code-adapter.js +48 -0
- package/dist/platforms/claude-code-adapter.js.map +1 -0
- package/dist/platforms/continue-adapter.d.ts +10 -0
- package/dist/platforms/continue-adapter.d.ts.map +1 -0
- package/dist/platforms/continue-adapter.js +73 -0
- package/dist/platforms/continue-adapter.js.map +1 -0
- package/dist/platforms/copilot-adapter.d.ts +37 -0
- package/dist/platforms/copilot-adapter.d.ts.map +1 -0
- package/dist/platforms/copilot-adapter.js +66 -0
- package/dist/platforms/copilot-adapter.js.map +1 -0
- package/dist/platforms/cursor-adapter.d.ts +10 -0
- package/dist/platforms/cursor-adapter.d.ts.map +1 -0
- package/dist/platforms/cursor-adapter.js +60 -0
- package/dist/platforms/cursor-adapter.js.map +1 -0
- package/dist/platforms/generic-mcp-adapter.d.ts +113 -0
- package/dist/platforms/generic-mcp-adapter.d.ts.map +1 -0
- package/dist/platforms/generic-mcp-adapter.js +139 -0
- package/dist/platforms/generic-mcp-adapter.js.map +1 -0
- package/dist/platforms/index.d.ts +15 -0
- package/dist/platforms/index.d.ts.map +1 -0
- package/dist/platforms/index.js +12 -0
- package/dist/platforms/index.js.map +1 -0
- package/dist/platforms/platform-registry.d.ts +11 -0
- package/dist/platforms/platform-registry.d.ts.map +1 -0
- package/dist/platforms/platform-registry.js +54 -0
- package/dist/platforms/platform-registry.js.map +1 -0
- package/dist/platforms/types.d.ts +36 -0
- package/dist/platforms/types.d.ts.map +1 -0
- package/dist/platforms/types.js +2 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/platforms/windsurf-adapter.d.ts +10 -0
- package/dist/platforms/windsurf-adapter.d.ts.map +1 -0
- package/dist/platforms/windsurf-adapter.js +63 -0
- package/dist/platforms/windsurf-adapter.js.map +1 -0
- package/dist/platforms/zed-adapter.d.ts +10 -0
- package/dist/platforms/zed-adapter.d.ts.map +1 -0
- package/dist/platforms/zed-adapter.js +72 -0
- package/dist/platforms/zed-adapter.js.map +1 -0
- package/dist/proxy/index.d.ts +7 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +5 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/otlp-receiver.d.ts +28 -0
- package/dist/proxy/otlp-receiver.d.ts.map +1 -0
- package/dist/proxy/otlp-receiver.js +319 -0
- package/dist/proxy/otlp-receiver.js.map +1 -0
- package/dist/proxy/proxy-manager.d.ts +47 -0
- package/dist/proxy/proxy-manager.d.ts.map +1 -0
- package/dist/proxy/proxy-manager.js +338 -0
- package/dist/proxy/proxy-manager.js.map +1 -0
- package/dist/proxy/types.d.ts +72 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +33 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/proxy/upstream-http.d.ts +26 -0
- package/dist/proxy/upstream-http.d.ts.map +1 -0
- package/dist/proxy/upstream-http.js +209 -0
- package/dist/proxy/upstream-http.js.map +1 -0
- package/dist/proxy/upstream-stdio.d.ts +25 -0
- package/dist/proxy/upstream-stdio.d.ts.map +1 -0
- package/dist/proxy/upstream-stdio.js +256 -0
- package/dist/proxy/upstream-stdio.js.map +1 -0
- package/dist/security/audit-trail.d.ts +74 -0
- package/dist/security/audit-trail.d.ts.map +1 -0
- package/dist/security/audit-trail.js +338 -0
- package/dist/security/audit-trail.js.map +1 -0
- package/dist/security/index.d.ts +5 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +4 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/ssrf.d.ts +2 -0
- package/dist/security/ssrf.d.ts.map +1 -0
- package/dist/security/ssrf.js +126 -0
- package/dist/security/ssrf.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +117 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/__test-utils__/log-output.d.ts +49 -0
- package/dist/shared/__test-utils__/log-output.d.ts.map +1 -0
- package/dist/shared/__test-utils__/log-output.js +38 -0
- package/dist/shared/__test-utils__/log-output.js.map +1 -0
- package/dist/shared/config.d.ts +56 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +290 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/errors.d.ts +139 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +406 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/events/factory.d.ts +143 -0
- package/dist/shared/events/factory.d.ts.map +1 -0
- package/dist/shared/events/factory.js +351 -0
- package/dist/shared/events/factory.js.map +1 -0
- package/dist/shared/events/index.d.ts +6 -0
- package/dist/shared/events/index.d.ts.map +1 -0
- package/dist/shared/events/index.js +3 -0
- package/dist/shared/events/index.js.map +1 -0
- package/dist/shared/events/serialize.d.ts +87 -0
- package/dist/shared/events/serialize.d.ts.map +1 -0
- package/dist/shared/events/serialize.js +510 -0
- package/dist/shared/events/serialize.js.map +1 -0
- package/dist/shared/events/types.d.ts +139 -0
- package/dist/shared/events/types.d.ts.map +1 -0
- package/dist/shared/events/types.js +2 -0
- package/dist/shared/events/types.js.map +1 -0
- package/dist/shared/harvest/event-buffer.d.ts +59 -0
- package/dist/shared/harvest/event-buffer.d.ts.map +1 -0
- package/dist/shared/harvest/event-buffer.js +100 -0
- package/dist/shared/harvest/event-buffer.js.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts +200 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.js +647 -0
- package/dist/shared/harvest/harvest-scheduler.js.map +1 -0
- package/dist/shared/harvest/index.d.ts +7 -0
- package/dist/shared/harvest/index.d.ts.map +1 -0
- package/dist/shared/harvest/index.js +4 -0
- package/dist/shared/harvest/index.js.map +1 -0
- package/dist/shared/harvest/metric-aggregator.d.ts +115 -0
- package/dist/shared/harvest/metric-aggregator.d.ts.map +1 -0
- package/dist/shared/harvest/metric-aggregator.js +247 -0
- package/dist/shared/harvest/metric-aggregator.js.map +1 -0
- package/dist/shared/index.d.ts +22 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +13 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/logger.d.ts +57 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +166 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/pricing-data.d.ts +4 -0
- package/dist/shared/pricing-data.d.ts.map +1 -0
- package/dist/shared/pricing-data.js +473 -0
- package/dist/shared/pricing-data.js.map +1 -0
- package/dist/shared/pricing.d.ts +148 -0
- package/dist/shared/pricing.d.ts.map +1 -0
- package/dist/shared/pricing.js +528 -0
- package/dist/shared/pricing.js.map +1 -0
- package/dist/shared/redact.d.ts +33 -0
- package/dist/shared/redact.d.ts.map +1 -0
- package/dist/shared/redact.js +110 -0
- package/dist/shared/redact.js.map +1 -0
- package/dist/shared/timing.d.ts +96 -0
- package/dist/shared/timing.d.ts.map +1 -0
- package/dist/shared/timing.js +173 -0
- package/dist/shared/timing.js.map +1 -0
- package/dist/shared/tokens.d.ts +145 -0
- package/dist/shared/tokens.d.ts.map +1 -0
- package/dist/shared/tokens.js +492 -0
- package/dist/shared/tokens.js.map +1 -0
- package/dist/shared/transport/events-api.d.ts +14 -0
- package/dist/shared/transport/events-api.d.ts.map +1 -0
- package/dist/shared/transport/events-api.js +29 -0
- package/dist/shared/transport/events-api.js.map +1 -0
- package/dist/shared/transport/http-client.d.ts +49 -0
- package/dist/shared/transport/http-client.d.ts.map +1 -0
- package/dist/shared/transport/http-client.js +381 -0
- package/dist/shared/transport/http-client.js.map +1 -0
- package/dist/shared/transport/index.d.ts +10 -0
- package/dist/shared/transport/index.d.ts.map +1 -0
- package/dist/shared/transport/index.js +6 -0
- package/dist/shared/transport/index.js.map +1 -0
- package/dist/shared/transport/logs-api.d.ts +29 -0
- package/dist/shared/transport/logs-api.d.ts.map +1 -0
- package/dist/shared/transport/logs-api.js +40 -0
- package/dist/shared/transport/logs-api.js.map +1 -0
- package/dist/shared/transport/metric-api.d.ts +9 -0
- package/dist/shared/transport/metric-api.d.ts.map +1 -0
- package/dist/shared/transport/metric-api.js +39 -0
- package/dist/shared/transport/metric-api.js.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts +22 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.js +50 -0
- package/dist/shared/transport/otlp-event-bridge.js.map +1 -0
- package/dist/shared/transport/otlp-shared.d.ts +14 -0
- package/dist/shared/transport/otlp-shared.d.ts.map +1 -0
- package/dist/shared/transport/otlp-shared.js +49 -0
- package/dist/shared/transport/otlp-shared.js.map +1 -0
- package/dist/shared/transport/otlp-transport.d.ts +58 -0
- package/dist/shared/transport/otlp-transport.d.ts.map +1 -0
- package/dist/shared/transport/otlp-transport.js +236 -0
- package/dist/shared/transport/otlp-transport.js.map +1 -0
- package/dist/shared/transport/types.d.ts +129 -0
- package/dist/shared/transport/types.d.ts.map +1 -0
- package/dist/shared/transport/types.js +2 -0
- package/dist/shared/transport/types.js.map +1 -0
- package/dist/shared/version.d.ts +2 -0
- package/dist/shared/version.d.ts.map +1 -0
- package/dist/shared/version.js +2 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-store.d.ts +153 -0
- package/dist/storage/local-store.d.ts.map +1 -0
- package/dist/storage/local-store.js +719 -0
- package/dist/storage/local-store.js.map +1 -0
- package/dist/storage/retention.d.ts +2 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/retention.js +53 -0
- package/dist/storage/retention.js.map +1 -0
- package/dist/storage/session-store.d.ts +97 -0
- package/dist/storage/session-store.d.ts.map +1 -0
- package/dist/storage/session-store.js +391 -0
- package/dist/storage/session-store.js.map +1 -0
- package/dist/storage/types.d.ts +64 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/weekly-summary.d.ts +61 -0
- package/dist/storage/weekly-summary.d.ts.map +1 -0
- package/dist/storage/weekly-summary.js +243 -0
- package/dist/storage/weekly-summary.js.map +1 -0
- package/dist/tools/analytics-tools.d.ts +101 -0
- package/dist/tools/analytics-tools.d.ts.map +1 -0
- package/dist/tools/analytics-tools.js +71 -0
- package/dist/tools/analytics-tools.js.map +1 -0
- package/dist/tools/cost-tools.d.ts +121 -0
- package/dist/tools/cost-tools.d.ts.map +1 -0
- package/dist/tools/cost-tools.js +174 -0
- package/dist/tools/cost-tools.js.map +1 -0
- package/dist/tools/cross-session-tools.d.ts +376 -0
- package/dist/tools/cross-session-tools.d.ts.map +1 -0
- package/dist/tools/cross-session-tools.js +820 -0
- package/dist/tools/cross-session-tools.js.map +1 -0
- package/dist/tools/extended-analytics-tools.d.ts +164 -0
- package/dist/tools/extended-analytics-tools.d.ts.map +1 -0
- package/dist/tools/extended-analytics-tools.js +121 -0
- package/dist/tools/extended-analytics-tools.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/session-stats.d.ts +162 -0
- package/dist/tools/session-stats.d.ts.map +1 -0
- package/dist/tools/session-stats.js +1054 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/workflow-tools.d.ts +126 -0
- package/dist/tools/workflow-tools.d.ts.map +1 -0
- package/dist/tools/workflow-tools.js +274 -0
- package/dist/tools/workflow-tools.js.map +1 -0
- package/dist/tracing/mcp-tracer.d.ts +4 -0
- package/dist/tracing/mcp-tracer.d.ts.map +1 -0
- package/dist/tracing/mcp-tracer.js +14 -0
- package/dist/tracing/mcp-tracer.js.map +1 -0
- package/dist/tracing/session-span.d.ts +14 -0
- package/dist/tracing/session-span.d.ts.map +1 -0
- package/dist/tracing/session-span.js +53 -0
- package/dist/tracing/session-span.js.map +1 -0
- package/dist/tracing/task-span-tracker.d.ts +11 -0
- package/dist/tracing/task-span-tracker.d.ts.map +1 -0
- package/dist/tracing/task-span-tracker.js +59 -0
- package/dist/tracing/task-span-tracker.js.map +1 -0
- package/dist/tracing/tool-call-span.d.ts +4 -0
- package/dist/tracing/tool-call-span.d.ts.map +1 -0
- package/dist/tracing/tool-call-span.js +60 -0
- package/dist/tracing/tool-call-span.js.map +1 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/log-ingest.d.ts +42 -0
- package/dist/transport/log-ingest.d.ts.map +1 -0
- package/dist/transport/log-ingest.js +151 -0
- package/dist/transport/log-ingest.js.map +1 -0
- package/dist/transport/nr-ingest.d.ts +171 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -0
- package/dist/transport/nr-ingest.js +659 -0
- package/dist/transport/nr-ingest.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web/assets/index-BrL281N-.css +2 -0
- package/dist/web/assets/index-CcaYZzXm.js +42 -0
- package/dist/web/favicon.svg +15 -0
- package/dist/web/index.html +15 -0
- package/examples/local-alert-rules.json +106 -0
- package/package.json +125 -1
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { localDateKey, localStartOfDay, todayPortionOfSessionCost } from '../../lib/date.js';
|
|
3
|
+
import { redactSensitive, normalizeDeveloperName } from '../../config.js';
|
|
4
|
+
import { handleSendDigest } from '../../tools/cross-session-tools.js';
|
|
5
|
+
import { attributeSessionCosts, } from '../../metrics/cost-per-outcome.js';
|
|
6
|
+
import { getIsoWeekId } from '../../storage/weekly-summary.js';
|
|
7
|
+
import { analyzeReplayTimeline } from './replay-analyzer.js';
|
|
8
|
+
import { isSyntheticSessionId } from '../../hooks/session-resolver.js';
|
|
9
|
+
function aggregateQualityFromHistory(sessions) {
|
|
10
|
+
let diffApplied = 0;
|
|
11
|
+
let diffFailed = 0;
|
|
12
|
+
let testPass = 0;
|
|
13
|
+
let testFail = 0;
|
|
14
|
+
let backtrackCount = 0;
|
|
15
|
+
let selfCorrectionCount = 0;
|
|
16
|
+
for (const raw of sessions) {
|
|
17
|
+
const session = raw;
|
|
18
|
+
const testRuns = session.testRunCount ?? 0;
|
|
19
|
+
const testPasses = session.testPassCount ?? 0;
|
|
20
|
+
// Derive diff success/failure from timeline when available
|
|
21
|
+
if (session.timeline && session.timeline.length > 0) {
|
|
22
|
+
let lastEditFile = null;
|
|
23
|
+
let lastEditIdx = -1;
|
|
24
|
+
for (let i = 0; i < session.timeline.length; i++) {
|
|
25
|
+
const entry = session.timeline[i];
|
|
26
|
+
if (entry.toolName === 'Edit' || entry.toolName === 'Write') {
|
|
27
|
+
if (entry.success)
|
|
28
|
+
diffApplied++;
|
|
29
|
+
else
|
|
30
|
+
diffFailed++;
|
|
31
|
+
// Detect self-correction: re-edit same file within 3 turns after a test failure
|
|
32
|
+
if (lastEditFile && entry.filePath === lastEditFile && i - lastEditIdx <= 3) {
|
|
33
|
+
const recentFail = session.timeline
|
|
34
|
+
.slice(lastEditIdx + 1, i)
|
|
35
|
+
.some((e) => e.isTestCommand && !e.success);
|
|
36
|
+
if (recentFail)
|
|
37
|
+
selfCorrectionCount++;
|
|
38
|
+
}
|
|
39
|
+
lastEditFile = entry.filePath ?? null;
|
|
40
|
+
lastEditIdx = i;
|
|
41
|
+
}
|
|
42
|
+
// Detect backtrack: Read of a recently edited file
|
|
43
|
+
if (entry.toolName === 'Read' &&
|
|
44
|
+
lastEditFile &&
|
|
45
|
+
entry.filePath === lastEditFile &&
|
|
46
|
+
i - lastEditIdx <= 2) {
|
|
47
|
+
backtrackCount++;
|
|
48
|
+
}
|
|
49
|
+
if (entry.isTestCommand) {
|
|
50
|
+
if (entry.success)
|
|
51
|
+
testPass++;
|
|
52
|
+
else
|
|
53
|
+
testFail++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// No timeline — use summary counts for test pass/fail only.
|
|
59
|
+
// Edit/Write counts from toolBreakdown have no success/failure split,
|
|
60
|
+
// so including them would always produce 100% diffApplyRate; skip them.
|
|
61
|
+
testPass += testPasses;
|
|
62
|
+
testFail += Math.max(0, testRuns - testPasses);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const totalDiffs = diffApplied + diffFailed;
|
|
66
|
+
const totalTests = testPass + testFail;
|
|
67
|
+
const totalSignals = totalDiffs + totalTests + backtrackCount + selfCorrectionCount;
|
|
68
|
+
return {
|
|
69
|
+
totalSignals,
|
|
70
|
+
diffApplyRate: totalDiffs > 0 ? Math.round((diffApplied / totalDiffs) * 1000) / 1000 : null,
|
|
71
|
+
testPassRate: totalTests > 0 ? Math.round((testPass / totalTests) * 1000) / 1000 : null,
|
|
72
|
+
backtrackCount,
|
|
73
|
+
selfCorrectionCount,
|
|
74
|
+
qualityByTurnBucket: [],
|
|
75
|
+
degradationDetected: false,
|
|
76
|
+
events: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function toAuditEntry(entry) {
|
|
80
|
+
const r = (entry ?? {});
|
|
81
|
+
const target = typeof r.detail === 'string' ? redactSensitive(r.detail) : '';
|
|
82
|
+
// Prefer the explicit security classification when present; fall back to
|
|
83
|
+
// 'other' for routine tool calls so the "All" filter still shows them
|
|
84
|
+
// while specific filters (sensitive_file/destructive_command/external_network)
|
|
85
|
+
// surface only flagged entries.
|
|
86
|
+
const classification = r.securityAlert?.alertType ?? 'other';
|
|
87
|
+
return {
|
|
88
|
+
ts: r.timestamp,
|
|
89
|
+
sessionId: r.sessionId ?? null,
|
|
90
|
+
tool: r.tool,
|
|
91
|
+
target,
|
|
92
|
+
classification,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function jsonOk(res, body) {
|
|
96
|
+
const payload = JSON.stringify(body);
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
'content-type': 'application/json; charset=utf-8',
|
|
99
|
+
'content-length': String(Buffer.byteLength(payload)),
|
|
100
|
+
});
|
|
101
|
+
res.end(payload);
|
|
102
|
+
}
|
|
103
|
+
function unavailable(res, what) {
|
|
104
|
+
const payload = JSON.stringify({ error: 'unavailable', what });
|
|
105
|
+
res.writeHead(503, {
|
|
106
|
+
'content-type': 'application/json; charset=utf-8',
|
|
107
|
+
'content-length': String(Buffer.byteLength(payload)),
|
|
108
|
+
});
|
|
109
|
+
res.end(payload);
|
|
110
|
+
}
|
|
111
|
+
const MAX_BODY_BYTES = 64 * 1024; // 64 KB — generous for any settings payload
|
|
112
|
+
function readBody(req) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let total = 0;
|
|
116
|
+
req.on('data', (c) => {
|
|
117
|
+
total += c.byteLength;
|
|
118
|
+
if (total > MAX_BODY_BYTES) {
|
|
119
|
+
req.destroy();
|
|
120
|
+
reject(new Error('Request body too large'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
chunks.push(c);
|
|
124
|
+
});
|
|
125
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
126
|
+
req.on('error', reject);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const ACTIVITY_WINDOW_MS = 180_000; // 3 minutes — matches LiveSessionRegistry staleness
|
|
130
|
+
function mergeActivityWindows(timeline) {
|
|
131
|
+
const sorted = [...timeline].sort((a, b) => a.timestamp - b.timestamp);
|
|
132
|
+
const windows = [];
|
|
133
|
+
for (const entry of sorted) {
|
|
134
|
+
const start = entry.timestamp;
|
|
135
|
+
const end = start + ACTIVITY_WINDOW_MS;
|
|
136
|
+
if (windows.length > 0 && start <= windows[windows.length - 1].end) {
|
|
137
|
+
windows[windows.length - 1].end = Math.max(windows[windows.length - 1].end, end);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
windows.push({ start, end });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return windows;
|
|
144
|
+
}
|
|
145
|
+
// 96 × 15-minute buckets covering today (00:00 → 24:00 local). Each bucket
|
|
146
|
+
// holds the peak concurrent session count observed within its window — see
|
|
147
|
+
// computeTodayConcurrencyBuckets() below.
|
|
148
|
+
const CONCURRENCY_BUCKET_SIZE_MS = 15 * 60 * 1000;
|
|
149
|
+
const CONCURRENCY_BUCKET_COUNT = 96;
|
|
150
|
+
// Build [startTime, endTime] intervals for every session that has any activity
|
|
151
|
+
// today. Persisted sessions contribute their stored startTime/endTime.
|
|
152
|
+
// Currently-live sessions (in the registry but not yet persisted) derive
|
|
153
|
+
// startTime from the in-memory tool-call buffer (oldest observed timestamp)
|
|
154
|
+
// and use `now` as endTime — matching the convention used by /api/sessions/live.
|
|
155
|
+
// Sessions that appear in both the persisted store and the live registry are
|
|
156
|
+
// deduped by sessionId; the live (extended-to-now) interval wins.
|
|
157
|
+
function collectTodaySessionIntervals(todaySessions, liveSessionIds, bufferRecords, now) {
|
|
158
|
+
const byId = new Map();
|
|
159
|
+
for (const raw of todaySessions) {
|
|
160
|
+
const s = raw;
|
|
161
|
+
if (typeof s.startTime !== 'number')
|
|
162
|
+
continue;
|
|
163
|
+
const end = typeof s.endTime === 'number' ? s.endTime : now;
|
|
164
|
+
if (end <= s.startTime)
|
|
165
|
+
continue;
|
|
166
|
+
if (typeof s.sessionId === 'string' && s.sessionId.length > 0) {
|
|
167
|
+
byId.set(s.sessionId, { startTime: s.startTime, endTime: end });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// No id — use a synthetic key so it still contributes.
|
|
171
|
+
byId.set(`__anon_${byId.size}`, { startTime: s.startTime, endTime: end });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (liveSessionIds.length > 0) {
|
|
175
|
+
const firstTsBySession = new Map();
|
|
176
|
+
for (const r of bufferRecords) {
|
|
177
|
+
const sid = r.sessionId;
|
|
178
|
+
if (!sid)
|
|
179
|
+
continue;
|
|
180
|
+
const ts = r.timestamp ?? 0;
|
|
181
|
+
if (!ts)
|
|
182
|
+
continue;
|
|
183
|
+
const prev = firstTsBySession.get(sid);
|
|
184
|
+
if (prev === undefined || ts < prev)
|
|
185
|
+
firstTsBySession.set(sid, ts);
|
|
186
|
+
}
|
|
187
|
+
for (const sid of liveSessionIds) {
|
|
188
|
+
const existing = byId.get(sid);
|
|
189
|
+
const bufferStart = firstTsBySession.get(sid);
|
|
190
|
+
// Prefer the EARLIER startTime: a persisted entry's startTime is
|
|
191
|
+
// authoritative (e.g. 09:00) and would be clobbered if we keyed solely
|
|
192
|
+
// off the buffer's earliest record (which may be much later if the
|
|
193
|
+
// buffer was drained between flushes). Extend endTime to `now` —
|
|
194
|
+
// that's the original intent of this loop (a live session is, by
|
|
195
|
+
// definition, ongoing).
|
|
196
|
+
const candidates = [];
|
|
197
|
+
if (existing !== undefined)
|
|
198
|
+
candidates.push(existing.startTime);
|
|
199
|
+
if (bufferStart !== undefined)
|
|
200
|
+
candidates.push(bufferStart);
|
|
201
|
+
if (candidates.length === 0)
|
|
202
|
+
continue;
|
|
203
|
+
const startTime = Math.min(...candidates);
|
|
204
|
+
if (now <= startTime)
|
|
205
|
+
continue;
|
|
206
|
+
byId.set(sid, { startTime, endTime: now });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return Array.from(byId.values());
|
|
210
|
+
}
|
|
211
|
+
function computeTodayConcurrencyBuckets(intervals, startTimestamp) {
|
|
212
|
+
// Single pass O(N log N) — build delta events from every interval,
|
|
213
|
+
// clamped to today's window, then sort once and sweep across the 96
|
|
214
|
+
// buckets in lockstep with the event cursor. Buckets containing no
|
|
215
|
+
// events still need a count: the value carried over from the previous
|
|
216
|
+
// bucket's tail (a session that spans many buckets without a delta in
|
|
217
|
+
// between must propagate as count=1+ in those buckets).
|
|
218
|
+
const dayEnd = startTimestamp + CONCURRENCY_BUCKET_COUNT * CONCURRENCY_BUCKET_SIZE_MS;
|
|
219
|
+
const events = [];
|
|
220
|
+
for (const interval of intervals) {
|
|
221
|
+
const start = Math.max(interval.startTime, startTimestamp);
|
|
222
|
+
const end = Math.min(interval.endTime, dayEnd);
|
|
223
|
+
if (start < end) {
|
|
224
|
+
events.push({ ts: start, delta: 1 }, { ts: end, delta: -1 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Sort by ts ascending; at ties, +1 deltas precede -1 (open-before-close)
|
|
228
|
+
// so that exact-touch session boundaries (one ends as another starts)
|
|
229
|
+
// count as overlap for one instant. Matches computeTodayPeakConcurrency's
|
|
230
|
+
// sort and the headline `peak` semantics — without this, a bucket peak
|
|
231
|
+
// can fall 1 below the day peak at boundary conditions.
|
|
232
|
+
events.sort((a, b) => a.ts - b.ts || b.delta - a.delta);
|
|
233
|
+
const buckets = new Array(CONCURRENCY_BUCKET_COUNT);
|
|
234
|
+
let current = 0;
|
|
235
|
+
let cursor = 0;
|
|
236
|
+
for (let b = 0; b < CONCURRENCY_BUCKET_COUNT; b++) {
|
|
237
|
+
const bucketStart = startTimestamp + b * CONCURRENCY_BUCKET_SIZE_MS;
|
|
238
|
+
const bucketEnd = bucketStart + CONCURRENCY_BUCKET_SIZE_MS;
|
|
239
|
+
// Flush any boundary events whose ts falls AT OR BEFORE bucketStart.
|
|
240
|
+
// Without this, a session ending at exactly t=bucketStart (e.g. a
|
|
241
|
+
// 15-min-aligned endTime) would still be carried as current=1 into
|
|
242
|
+
// this bucket before the -1 fires, inflating the bucket's initial peak
|
|
243
|
+
// by 1. Flushing first means peak is initialised from the correct
|
|
244
|
+
// post-close concurrency level.
|
|
245
|
+
while (cursor < events.length && events[cursor].ts <= bucketStart) {
|
|
246
|
+
current += events[cursor].delta;
|
|
247
|
+
cursor++;
|
|
248
|
+
}
|
|
249
|
+
// Peak starts at the carried-over (post-flush) level. Sessions spanning
|
|
250
|
+
// many buckets with no mid-bucket events propagate as count=N+ here.
|
|
251
|
+
let peak = current;
|
|
252
|
+
while (cursor < events.length && events[cursor].ts < bucketEnd) {
|
|
253
|
+
current += events[cursor].delta;
|
|
254
|
+
if (current > peak)
|
|
255
|
+
peak = current;
|
|
256
|
+
cursor++;
|
|
257
|
+
}
|
|
258
|
+
buckets[b] = { timestamp: bucketStart, count: peak };
|
|
259
|
+
}
|
|
260
|
+
return buckets;
|
|
261
|
+
}
|
|
262
|
+
function computeTodayPeakConcurrency(sessions) {
|
|
263
|
+
const events = [];
|
|
264
|
+
for (const s of sessions) {
|
|
265
|
+
const session = s;
|
|
266
|
+
if (!session.timeline || session.timeline.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
const windows = mergeActivityWindows(session.timeline);
|
|
269
|
+
for (const w of windows) {
|
|
270
|
+
events.push({ ts: w.start, delta: 1 }, { ts: w.end, delta: -1 });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (events.length === 0)
|
|
274
|
+
return 0;
|
|
275
|
+
events.sort((a, b) => a.ts - b.ts || b.delta - a.delta);
|
|
276
|
+
let current = 0;
|
|
277
|
+
let peak = 0;
|
|
278
|
+
for (const e of events) {
|
|
279
|
+
current += e.delta;
|
|
280
|
+
if (current > peak)
|
|
281
|
+
peak = current;
|
|
282
|
+
}
|
|
283
|
+
return peak;
|
|
284
|
+
}
|
|
285
|
+
function computeDailyPeakConcurrency(sessions, days) {
|
|
286
|
+
const now = new Date();
|
|
287
|
+
const result = [];
|
|
288
|
+
for (let d = days - 1; d >= 0; d--) {
|
|
289
|
+
const dayStart = new Date(now);
|
|
290
|
+
dayStart.setUTCDate(dayStart.getUTCDate() - d);
|
|
291
|
+
dayStart.setUTCHours(0, 0, 0, 0);
|
|
292
|
+
const dayEnd = new Date(dayStart);
|
|
293
|
+
dayEnd.setUTCDate(dayEnd.getUTCDate() + 1);
|
|
294
|
+
const dayStartMs = dayStart.getTime();
|
|
295
|
+
const dayEndMs = dayEnd.getTime();
|
|
296
|
+
const dateKey = dayStart.toISOString().slice(0, 10);
|
|
297
|
+
// Find sessions that overlap with this day and have timeline data
|
|
298
|
+
const events = [];
|
|
299
|
+
for (const s of sessions) {
|
|
300
|
+
const session = s;
|
|
301
|
+
if (!session.timeline || session.timeline.length === 0)
|
|
302
|
+
continue;
|
|
303
|
+
// Only include tool calls within this day
|
|
304
|
+
const dayEntries = session.timeline.filter((e) => e.timestamp >= dayStartMs && e.timestamp < dayEndMs);
|
|
305
|
+
if (dayEntries.length === 0)
|
|
306
|
+
continue;
|
|
307
|
+
const windows = mergeActivityWindows(dayEntries);
|
|
308
|
+
for (const w of windows) {
|
|
309
|
+
events.push({ ts: w.start, delta: 1 }, { ts: w.end, delta: -1 });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (events.length === 0) {
|
|
313
|
+
result.push({ date: dateKey, peak: 0 });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
events.sort((a, b) => a.ts - b.ts || b.delta - a.delta);
|
|
317
|
+
let concurrent = 0;
|
|
318
|
+
let peak = 0;
|
|
319
|
+
for (const e of events) {
|
|
320
|
+
concurrent += e.delta;
|
|
321
|
+
if (concurrent > peak)
|
|
322
|
+
peak = concurrent;
|
|
323
|
+
}
|
|
324
|
+
result.push({ date: dateKey, peak });
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
function toolCallToTimelineEntry(tc) {
|
|
329
|
+
return {
|
|
330
|
+
timestamp: tc.timestamp,
|
|
331
|
+
toolName: tc.toolName,
|
|
332
|
+
durationMs: tc.durationMs,
|
|
333
|
+
success: tc.success,
|
|
334
|
+
filePath: tc.filePath ? redactSensitive(String(tc.filePath)) : undefined,
|
|
335
|
+
command: tc.command ? redactSensitive(String(tc.command)) : undefined,
|
|
336
|
+
isTestCommand: tc.isTestCommand || undefined,
|
|
337
|
+
isBuildCommand: tc.isBuildCommand || undefined,
|
|
338
|
+
isLintCommand: tc.isLintCommand || undefined,
|
|
339
|
+
errorType: tc.errorType || undefined,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function buildReplayResponse(sessionId, deps) {
|
|
343
|
+
// Try persisted session first
|
|
344
|
+
if (deps.sessionStore) {
|
|
345
|
+
const session = deps.sessionStore.loadSession(sessionId);
|
|
346
|
+
if (session && Array.isArray(session['timeline'])) {
|
|
347
|
+
const rawTimeline = session['timeline'];
|
|
348
|
+
// Redact sensitive fields before sending to the browser
|
|
349
|
+
const timeline = rawTimeline.map((e) => ({
|
|
350
|
+
...e,
|
|
351
|
+
filePath: e.filePath ? redactSensitive(String(e.filePath)) : undefined,
|
|
352
|
+
command: e.command ? redactSensitive(String(e.command)) : undefined,
|
|
353
|
+
}));
|
|
354
|
+
const analysis = analyzeReplayTimeline(timeline);
|
|
355
|
+
return {
|
|
356
|
+
sessionId,
|
|
357
|
+
timeline,
|
|
358
|
+
segments: analysis.segments,
|
|
359
|
+
worstSegment: analysis.worstSegment,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Try live session from TaskDetector — filter to the requested sessionId
|
|
364
|
+
if (deps.taskDetector) {
|
|
365
|
+
const completed = deps.taskDetector.getCompletedTasks();
|
|
366
|
+
const current = deps.taskDetector.getCurrentTask();
|
|
367
|
+
const allCalls = [];
|
|
368
|
+
for (const task of completed) {
|
|
369
|
+
allCalls.push(...task.toolCalls);
|
|
370
|
+
}
|
|
371
|
+
if (current) {
|
|
372
|
+
allCalls.push(...current.toolCalls);
|
|
373
|
+
}
|
|
374
|
+
const sessionCalls = allCalls.filter((c) => c.sessionId === sessionId);
|
|
375
|
+
if (sessionCalls.length > 0) {
|
|
376
|
+
sessionCalls.sort((a, b) => a.timestamp - b.timestamp);
|
|
377
|
+
const timeline = sessionCalls.map(toolCallToTimelineEntry);
|
|
378
|
+
const analysis = analyzeReplayTimeline(timeline);
|
|
379
|
+
return {
|
|
380
|
+
sessionId,
|
|
381
|
+
timeline,
|
|
382
|
+
segments: analysis.segments,
|
|
383
|
+
worstSegment: analysis.worstSegment,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Final fallback: scan the in-memory tool call buffer for events matching
|
|
388
|
+
// sessionId. TaskDetector only emits records once they're attributed to a
|
|
389
|
+
// task; tool calls that fire before a task starts (or for sessions other
|
|
390
|
+
// than the dashboard owner's) live in the buffer but not in TaskDetector.
|
|
391
|
+
// Today's live tail reads via SSE so it sees these immediately; without
|
|
392
|
+
// this fallback the Sessions detail view shows "No tool calls" for a
|
|
393
|
+
// newly-live session that hasn't yet completed a task.
|
|
394
|
+
if (deps.toolCallBuffer) {
|
|
395
|
+
const records = deps.toolCallBuffer.getRecords();
|
|
396
|
+
const sessionCalls = records.filter((c) => c.sessionId === sessionId);
|
|
397
|
+
if (sessionCalls.length > 0) {
|
|
398
|
+
sessionCalls.sort((a, b) => a.timestamp - b.timestamp);
|
|
399
|
+
const timeline = sessionCalls.map(toolCallToTimelineEntry);
|
|
400
|
+
const analysis = analyzeReplayTimeline(timeline);
|
|
401
|
+
return {
|
|
402
|
+
sessionId,
|
|
403
|
+
timeline,
|
|
404
|
+
segments: analysis.segments,
|
|
405
|
+
worstSegment: analysis.worstSegment,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
export function createApiHandler(deps) {
|
|
412
|
+
const routes = new Map();
|
|
413
|
+
routes.set('GET /api/session/current', (_req, res) => {
|
|
414
|
+
if (!deps.sessionTracker)
|
|
415
|
+
return unavailable(res, 'sessionTracker');
|
|
416
|
+
// surface the rolling efficiency score as a sibling field so the
|
|
417
|
+
// SPA Today KPI can render it without a second round-trip. `null` when
|
|
418
|
+
// no tasks have been scored yet (or when the scorer wasn't wired in).
|
|
419
|
+
const efficiencyScore = deps.efficiencyScorer?.getSessionAverage()?.score ?? null;
|
|
420
|
+
const liveSessions = deps.liveSessionRegistry?.getLiveSessions() ?? [];
|
|
421
|
+
jsonOk(res, { ...deps.sessionTracker.getMetrics(), efficiencyScore, liveSessions });
|
|
422
|
+
});
|
|
423
|
+
routes.set('GET /api/session/today', (_req, res) => {
|
|
424
|
+
if (!deps.sessionStore)
|
|
425
|
+
return unavailable(res, 'sessionStore');
|
|
426
|
+
jsonOk(res, deps.sessionStore.loadTodaySessions());
|
|
427
|
+
});
|
|
428
|
+
routes.set('GET /api/sessions', (req, res) => {
|
|
429
|
+
if (!deps.sessionStore)
|
|
430
|
+
return unavailable(res, 'sessionStore');
|
|
431
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
432
|
+
const limitStr = url.searchParams.get('limit') ?? '';
|
|
433
|
+
let limit = 50;
|
|
434
|
+
const parsed = parseInt(limitStr, 10);
|
|
435
|
+
if (!Number.isNaN(parsed)) {
|
|
436
|
+
limit = Math.min(Math.max(parsed, 1), 500);
|
|
437
|
+
}
|
|
438
|
+
const allSessions = deps.sessionStore.loadAllSessions
|
|
439
|
+
? deps.sessionStore.loadAllSessions()
|
|
440
|
+
: deps.sessionStore.listSessions();
|
|
441
|
+
// Filter synthetic IDs (`local-<ts>`, `proxy-<ts>`) from the historical
|
|
442
|
+
// list. They're MCP-internal bookkeeping IDs persisted incidentally by
|
|
443
|
+
// older builds; they appear as confusing "duplicate" rows next to real
|
|
444
|
+
// Claude Code sessions. The synthetic-shutdown filter in src/index.ts
|
|
445
|
+
// prevents new ones from being persisted, but stale ones from before
|
|
446
|
+
// that filter shipped need to be hidden at read time.
|
|
447
|
+
const withActivity = allSessions.filter((s) => {
|
|
448
|
+
const sid = s.sessionId;
|
|
449
|
+
const calls = s.toolCallCount ?? 0;
|
|
450
|
+
return calls > 0 && (!sid || !isSyntheticSessionId(sid));
|
|
451
|
+
});
|
|
452
|
+
const sliced = withActivity.slice(-limit);
|
|
453
|
+
// Append the current live session so it appears in the list before
|
|
454
|
+
// shutdown — but skip synthetic sessionTraceIds from --local / proxy
|
|
455
|
+
// modes (the same filter as for persisted entries above).
|
|
456
|
+
if (deps.sessionTracker) {
|
|
457
|
+
const live = deps.sessionTracker.getMetrics();
|
|
458
|
+
const alreadyPersisted = sliced.some((s) => s.sessionId === live.sessionId);
|
|
459
|
+
if (!alreadyPersisted && live.toolCallCount > 0 && !isSyntheticSessionId(live.sessionId)) {
|
|
460
|
+
sliced.push({
|
|
461
|
+
sessionId: live.sessionId,
|
|
462
|
+
sessionName: live.sessionName ?? null,
|
|
463
|
+
startTime: live.sessionStartTime,
|
|
464
|
+
durationMs: live.sessionDurationMs,
|
|
465
|
+
toolCallCount: live.toolCallCount,
|
|
466
|
+
estimatedCostUsd: deps.costTracker?.getMetrics().sessionTotalCostUsd ?? null,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Inject stub entries for live sessions not yet persisted to disk.
|
|
471
|
+
// Derive toolCallCount and startTime from the in-memory tool call buffer
|
|
472
|
+
// so concurrent sessions show real activity counts on the badges.
|
|
473
|
+
if (deps.liveSessionRegistry) {
|
|
474
|
+
const knownIds = new Set(sliced.map((s) => s.sessionId));
|
|
475
|
+
const records = deps.toolCallBuffer?.getRecords() ?? [];
|
|
476
|
+
const perSession = new Map();
|
|
477
|
+
for (const r of records) {
|
|
478
|
+
const sid = r.sessionId;
|
|
479
|
+
if (!sid)
|
|
480
|
+
continue;
|
|
481
|
+
const ts = r.timestamp ?? 0;
|
|
482
|
+
const entry = perSession.get(sid);
|
|
483
|
+
if (entry) {
|
|
484
|
+
entry.count++;
|
|
485
|
+
if (ts && ts < entry.firstTs)
|
|
486
|
+
entry.firstTs = ts;
|
|
487
|
+
if (ts && ts > entry.lastTs)
|
|
488
|
+
entry.lastTs = ts;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
perSession.set(sid, { count: 1, firstTs: ts || Date.now(), lastTs: ts || Date.now() });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const id of deps.liveSessionRegistry.getLiveSessions()) {
|
|
495
|
+
if (!knownIds.has(id)) {
|
|
496
|
+
const stats = perSession.get(id);
|
|
497
|
+
// getLastActivity is registry-maintained and survives buffer drains,
|
|
498
|
+
// so use it as the stable upper bound for durationMs. Without this,
|
|
499
|
+
// durationMs collapses to 0 every 5s when the harvest scheduler drains
|
|
500
|
+
// the buffer and perSession rebuilds from an empty set of records.
|
|
501
|
+
const lastActivityTs = deps.liveSessionRegistry.getLastActivity?.(id) ?? stats?.lastTs;
|
|
502
|
+
const sessionStart = stats?.firstTs ?? lastActivityTs ?? Date.now();
|
|
503
|
+
sliced.push({
|
|
504
|
+
sessionId: id,
|
|
505
|
+
sessionName: deps.liveSessionRegistry.getSessionName(id),
|
|
506
|
+
startTime: sessionStart,
|
|
507
|
+
durationMs: lastActivityTs != null ? Math.max(0, lastActivityTs - sessionStart) : 0,
|
|
508
|
+
toolCallCount: stats?.count ?? 0,
|
|
509
|
+
estimatedCostUsd: null,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Strip heavy per-session fields the list view doesn't render. Without
|
|
515
|
+
// this, /api/sessions returns ~90KB per session × N sessions; first
|
|
516
|
+
// paint blocks on parsing 200KB+ of JSON the list never reads.
|
|
517
|
+
// The detail endpoint /api/sessions/:id returns the full session.
|
|
518
|
+
const HEAVY_FIELDS = new Set(['timeline', 'filesRead', 'filesModified']);
|
|
519
|
+
const slimmed = sliced.map((s) => {
|
|
520
|
+
const o = s;
|
|
521
|
+
const out = {};
|
|
522
|
+
for (const k of Object.keys(o)) {
|
|
523
|
+
if (!HEAVY_FIELDS.has(k))
|
|
524
|
+
out[k] = o[k];
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
});
|
|
528
|
+
jsonOk(res, slimmed.length > limit ? slimmed.slice(-limit) : slimmed);
|
|
529
|
+
});
|
|
530
|
+
// Task #17 (D3): currently-live session list with metadata. Sourced from
|
|
531
|
+
// LiveSessionRegistry (touch-based liveness within a 3-minute window) so
|
|
532
|
+
// the Today selector can default to the most-recently-active session even
|
|
533
|
+
// when nothing has been persisted to disk yet. The dashboard owner sees
|
|
534
|
+
// every live session — Fix 3 partitioned per-session buffer files but the
|
|
535
|
+
// registry is in-memory and shared via this MCP, so the data is one
|
|
536
|
+
// authoritative read.
|
|
537
|
+
//
|
|
538
|
+
// Falls back to the in-memory tool call buffer for `startTime` (oldest
|
|
539
|
+
// observed timestamp for that session) since the registry only tracks
|
|
540
|
+
// last-activity. When a session's first event hasn't reached the buffer
|
|
541
|
+
// yet, startTime defaults to lastActivity (or now() if both are missing).
|
|
542
|
+
routes.set('GET /api/sessions/live', (_req, res) => {
|
|
543
|
+
if (!deps.liveSessionRegistry)
|
|
544
|
+
return unavailable(res, 'liveSessionRegistry');
|
|
545
|
+
// Filter out synthetic session identities used by --local / proxy modes
|
|
546
|
+
// (`local-<ts>`, `proxy-<ts>`). They're MCP-internal bookkeeping IDs,
|
|
547
|
+
// not real Claude Code sessions, so they shouldn't appear as clickable
|
|
548
|
+
// rows in the dashboard's live-sessions selector. The real user sessions
|
|
549
|
+
// (with proper session_ids resolved via the breadcrumb / CLAUDE_JOB_DIR
|
|
550
|
+
// path) are what users want to see and click.
|
|
551
|
+
const ids = deps.liveSessionRegistry
|
|
552
|
+
.getLiveSessions()
|
|
553
|
+
.filter((id) => !isSyntheticSessionId(id));
|
|
554
|
+
const records = deps.toolCallBuffer?.getRecords() ?? [];
|
|
555
|
+
const perSession = new Map();
|
|
556
|
+
for (const r of records) {
|
|
557
|
+
const sid = r.sessionId;
|
|
558
|
+
if (!sid)
|
|
559
|
+
continue;
|
|
560
|
+
const ts = r.timestamp ?? 0;
|
|
561
|
+
if (!ts)
|
|
562
|
+
continue;
|
|
563
|
+
const entry = perSession.get(sid);
|
|
564
|
+
if (entry) {
|
|
565
|
+
if (ts < entry.firstTs)
|
|
566
|
+
entry.firstTs = ts;
|
|
567
|
+
if (ts > entry.lastTs)
|
|
568
|
+
entry.lastTs = ts;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
perSession.set(sid, { firstTs: ts, lastTs: ts });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const sessions = ids.map((id) => {
|
|
575
|
+
const stats = perSession.get(id);
|
|
576
|
+
const lastActivity = deps.liveSessionRegistry?.getLastActivity?.(id) ?? stats?.lastTs ?? Date.now();
|
|
577
|
+
return {
|
|
578
|
+
sessionId: id,
|
|
579
|
+
sessionName: deps.liveSessionRegistry?.getSessionName(id) ?? null,
|
|
580
|
+
startTime: stats?.firstTs ?? lastActivity,
|
|
581
|
+
lastActivity,
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
// Most-recently-active first so the Today selector can default to
|
|
585
|
+
// sessions[0] without re-sorting on the client.
|
|
586
|
+
sessions.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
587
|
+
jsonOk(res, sessions);
|
|
588
|
+
});
|
|
589
|
+
// Task #17 (D3): cross-session aggregate KPIs for the Today view. Reads:
|
|
590
|
+
// 1. every per-session buffer-*.jsonl in read-only mode (post-Fix-3
|
|
591
|
+
// each MCP only drains its own; the dashboard owner needs the union)
|
|
592
|
+
// 2. completed session JSONs at ~/.newrelic-preflight/sessions/ (loaded via
|
|
593
|
+
// sessionStore.loadTodaySessions())
|
|
594
|
+
// 3. the in-memory tool call buffer (events from this MCP that have
|
|
595
|
+
// already been processed but not yet persisted)
|
|
596
|
+
//
|
|
597
|
+
// The minute-bucketed sparkline starts at 00:00 local and runs through
|
|
598
|
+
// the current minute. Aggregate `avgDurationMs` is the simple mean over
|
|
599
|
+
// every observed durationMs across all three sources.
|
|
600
|
+
//
|
|
601
|
+
// Caching: dashboards poll this endpoint every 5–10s; both reads (peek
|
|
602
|
+
// every buffer-*.jsonl + load every today-session JSON) hit disk and
|
|
603
|
+
// walk the full result. Cache the response in a 5-second bucket so a
|
|
604
|
+
// single tab burst plus a couple of mirror tabs collapses to one
|
|
605
|
+
// computation per bucket. TTL is short enough that the live-feel KPI
|
|
606
|
+
// (sparkline tail, tool-call count) lags by at most ~5s.
|
|
607
|
+
const AGGREGATE_TTL_MS = 5_000;
|
|
608
|
+
let aggregateCache = null;
|
|
609
|
+
routes.set('GET /api/sessions/today/aggregate', (_req, res) => {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
const currentBucket = Math.floor(now / AGGREGATE_TTL_MS);
|
|
612
|
+
if (aggregateCache && aggregateCache.bucket === currentBucket) {
|
|
613
|
+
jsonOk(res, aggregateCache.payload);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const startMs = localStartOfDay(now);
|
|
617
|
+
const minuteBuckets = Math.max(1, Math.ceil((now - startMs) / 60_000));
|
|
618
|
+
const sparkline = new Array(minuteBuckets).fill(0);
|
|
619
|
+
let toolCallCount = 0;
|
|
620
|
+
let totalDurationMs = 0;
|
|
621
|
+
let durationSamples = 0;
|
|
622
|
+
let totalCostUsd = 0;
|
|
623
|
+
let antiPatternCount = 0;
|
|
624
|
+
const sessionsSeen = new Set();
|
|
625
|
+
// (1) live, undrained per-session buffer events
|
|
626
|
+
const peeked = deps.localStore?.peekAllBuffers() ?? [];
|
|
627
|
+
// Per-live-session earliest buffer timestamp (today only). Used as a
|
|
628
|
+
// cutoff against the persisted timeline below: persisted timeline
|
|
629
|
+
// entries strictly OLDER than the buffer's earliest event for the same
|
|
630
|
+
// sessionId are pre-resume (drained before persistence and replayed via
|
|
631
|
+
// saved JSON); entries at-or-after that cutoff are live buffer events
|
|
632
|
+
// we already counted in step (1) and must not double-count.
|
|
633
|
+
//
|
|
634
|
+
// In practice the resume-after-shutdown flow doesn't keep the buffer
|
|
635
|
+
// alive across restarts (each MCP starts fresh), so this dedup is a
|
|
636
|
+
// belt-and-suspenders safety net rather than a load-bearing invariant.
|
|
637
|
+
const bufferStartBySession = new Map();
|
|
638
|
+
for (const ev of peeked) {
|
|
639
|
+
const ts = typeof ev.timestamp === 'number' ? ev.timestamp : 0;
|
|
640
|
+
if (ts < startMs)
|
|
641
|
+
continue;
|
|
642
|
+
const sid = ev.sessionId;
|
|
643
|
+
if (typeof sid === 'string' && sid.length > 0) {
|
|
644
|
+
sessionsSeen.add(sid);
|
|
645
|
+
const prev = bufferStartBySession.get(sid);
|
|
646
|
+
if (prev === undefined || ts < prev)
|
|
647
|
+
bufferStartBySession.set(sid, ts);
|
|
648
|
+
}
|
|
649
|
+
// Hook events come through as either `pre`/`post`/`token`. We only
|
|
650
|
+
// count `post` events as completed tool calls — `pre` is the start
|
|
651
|
+
// marker and `token` is a cost event with no tool-call semantics.
|
|
652
|
+
if (ev.mode === 'post') {
|
|
653
|
+
toolCallCount++;
|
|
654
|
+
const idx = Math.floor((ts - startMs) / 60_000);
|
|
655
|
+
if (idx >= 0 && idx < sparkline.length)
|
|
656
|
+
sparkline[idx]++;
|
|
657
|
+
const dur = typeof ev.durationMs === 'number' ? ev.durationMs : null;
|
|
658
|
+
if (dur !== null) {
|
|
659
|
+
totalDurationMs += dur;
|
|
660
|
+
durationSamples++;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Hoist the live-session set out of the inner loop so we don't pay
|
|
665
|
+
// O(timeline × liveSessions) lookups per aggregate request.
|
|
666
|
+
const liveSet = new Set(deps.liveSessionRegistry?.getLiveSessions() ?? []);
|
|
667
|
+
// (2) completed sessions persisted today
|
|
668
|
+
const todaySessions = deps.sessionStore?.loadTodaySessions() ?? [];
|
|
669
|
+
for (const raw of todaySessions) {
|
|
670
|
+
const session = raw;
|
|
671
|
+
if (typeof session.sessionId === 'string')
|
|
672
|
+
sessionsSeen.add(session.sessionId);
|
|
673
|
+
// Cost is summed in a separate pass below so cross-midnight sessions
|
|
674
|
+
// (loaded via loadSessionsOverlappingToday) contribute their today
|
|
675
|
+
// portion only. See "(2b) cost from sessions overlapping today" below.
|
|
676
|
+
antiPatternCount += session.antiPatterns?.length ?? 0;
|
|
677
|
+
// Walk timeline entries within today. Persisted timelines and the
|
|
678
|
+
// buffer cover disjoint time ranges by construction (persistence
|
|
679
|
+
// runs at shutdown, after all events have been processed; buffer
|
|
680
|
+
// contents are undrained events). Even in the resume-after-shutdown
|
|
681
|
+
// case, the persisted JSON holds pre-shutdown events while the
|
|
682
|
+
// buffer holds post-resume events — disjoint timestamps. Use a
|
|
683
|
+
// per-session timestamp cutoff (earliest buffer event for that
|
|
684
|
+
// sessionId) as a defensive dedup so we never double-count if the
|
|
685
|
+
// ranges ever DO overlap.
|
|
686
|
+
if (session.timeline) {
|
|
687
|
+
const sid = session.sessionId ?? '';
|
|
688
|
+
const bufferCutoff = liveSet.has(sid) ? bufferStartBySession.get(sid) : undefined;
|
|
689
|
+
for (const entry of session.timeline) {
|
|
690
|
+
if (entry.timestamp < startMs)
|
|
691
|
+
continue;
|
|
692
|
+
if (bufferCutoff !== undefined && entry.timestamp >= bufferCutoff)
|
|
693
|
+
continue;
|
|
694
|
+
toolCallCount++;
|
|
695
|
+
const idx = Math.floor((entry.timestamp - startMs) / 60_000);
|
|
696
|
+
if (idx >= 0 && idx < sparkline.length)
|
|
697
|
+
sparkline[idx]++;
|
|
698
|
+
if (entry.durationMs !== null) {
|
|
699
|
+
totalDurationMs += entry.durationMs;
|
|
700
|
+
durationSamples++;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// (2b) cost from sessions overlapping today (separate pass so cross-
|
|
706
|
+
// midnight sessions started yesterday but ending after midnight contribute
|
|
707
|
+
// their today portion to spend, while NOT inflating tool-call or
|
|
708
|
+
// anti-pattern counts above which are already today-bounded by timeline).
|
|
709
|
+
//
|
|
710
|
+
// Why a separate loader: loadTodaySessions() filters by file-name date
|
|
711
|
+
// (= start date), so it drops sessions that started yesterday and ended
|
|
712
|
+
// today. The cost path needs them; the tool-call path doesn't (its
|
|
713
|
+
// timeline filter would skip pre-midnight entries anyway).
|
|
714
|
+
const liveSid = deps.sessionTracker?.getMetrics().sessionId;
|
|
715
|
+
// Prefer overlapping-today loader (catches yesterday→today sessions).
|
|
716
|
+
// When the store doesn't implement it (older tests/fakes), reuse the
|
|
717
|
+
// already-loaded `todaySessions` rather than re-invoking
|
|
718
|
+
// loadTodaySessions — keeps disk reads at one per request and preserves
|
|
719
|
+
// the cache-hit assertions in api-handler.test.ts.
|
|
720
|
+
const overlappingTodaySessions = deps.sessionStore?.loadSessionsOverlappingToday?.() ?? todaySessions;
|
|
721
|
+
for (const raw of overlappingTodaySessions) {
|
|
722
|
+
const s = raw;
|
|
723
|
+
// Skip the live session here — its today-portion is added below from
|
|
724
|
+
// costTracker.getCostForDay(), which is more accurate (per-token-event)
|
|
725
|
+
// than pro-rating from a periodically-persisted snapshot.
|
|
726
|
+
if (s.sessionId === liveSid)
|
|
727
|
+
continue;
|
|
728
|
+
totalCostUsd += todayPortionOfSessionCost(s, now);
|
|
729
|
+
// Count anti-patterns and session for cross-midnight sessions not already
|
|
730
|
+
// captured by the todaySessions loop (which filtered by start-date).
|
|
731
|
+
if (typeof s.sessionId === 'string' && !sessionsSeen.has(s.sessionId)) {
|
|
732
|
+
sessionsSeen.add(s.sessionId);
|
|
733
|
+
antiPatternCount += s.antiPatterns?.length ?? 0;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// (3) include this MCP's live session today-portion. Per-day attribution
|
|
737
|
+
// comes from CostTracker, which buckets each token event by local-day at
|
|
738
|
+
// record time (see CostTracker.accumulateTokens). Falls back to session
|
|
739
|
+
// total if no per-day data is available (older deployments / first event).
|
|
740
|
+
const liveAlreadyPersisted = todaySessions.some((s) => s.sessionId === liveSid);
|
|
741
|
+
if (!liveAlreadyPersisted) {
|
|
742
|
+
const todayKey = localDateKey(now);
|
|
743
|
+
const liveTodayUsd = deps.costTracker?.getCostForDay?.(todayKey) ?? null;
|
|
744
|
+
if (typeof liveTodayUsd === 'number') {
|
|
745
|
+
totalCostUsd += liveTodayUsd;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// Fallback for older deployments without per-day API. This is the
|
|
749
|
+
// pre-fix behavior; only hit when getCostForDay is missing.
|
|
750
|
+
const sessionCost = deps.costTracker?.getMetrics().sessionTotalCostUsd ?? null;
|
|
751
|
+
if (typeof sessionCost === 'number')
|
|
752
|
+
totalCostUsd += sessionCost;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Live session anti-patterns (in-memory, not yet persisted).
|
|
756
|
+
// Mirror the alreadyPersisted guard used for cost above: if this MCP's
|
|
757
|
+
// session is already in todaySessions, its anti-patterns were counted in
|
|
758
|
+
// the loop above — don't double-count.
|
|
759
|
+
if (deps.antiPatternDetector && !liveAlreadyPersisted) {
|
|
760
|
+
const live = deps.antiPatternDetector.getCurrentPatterns();
|
|
761
|
+
antiPatternCount += live.length;
|
|
762
|
+
}
|
|
763
|
+
const avgDurationMs = durationSamples > 0 ? totalDurationMs / durationSamples : 0;
|
|
764
|
+
const payload = {
|
|
765
|
+
toolCallCount,
|
|
766
|
+
totalCostUsd: Math.round(totalCostUsd * 1000) / 1000,
|
|
767
|
+
antiPatternCount,
|
|
768
|
+
avgDurationMs: Math.round(avgDurationMs),
|
|
769
|
+
sessionCount: sessionsSeen.size,
|
|
770
|
+
sparkline: { startTimestamp: startMs, bucketSizeMs: 60_000, points: sparkline },
|
|
771
|
+
};
|
|
772
|
+
aggregateCache = { bucket: currentBucket, payload };
|
|
773
|
+
jsonOk(res, payload);
|
|
774
|
+
});
|
|
775
|
+
routes.set('GET /api/cost', (_req, res) => {
|
|
776
|
+
if (!deps.costTracker)
|
|
777
|
+
return unavailable(res, 'costTracker');
|
|
778
|
+
const cost = deps.costTracker.getMetrics();
|
|
779
|
+
const forecast = deps.costForecast?.() ?? null;
|
|
780
|
+
// sessionTodayUsd lets the client compute the correct EoD forecast fallback:
|
|
781
|
+
// todayTotal + (forecastEndOfDayUsd − sessionTodayUsd) = todayTotal + projected
|
|
782
|
+
// Without it, the client would add persistedTodaySpend to forecastEndOfDayUsd and
|
|
783
|
+
// risk double-counting the live session when its snapshot is already in persisted data.
|
|
784
|
+
const sessionTodayUsd = deps.costTracker.getCostForDay?.(localDateKey(Date.now())) ?? null;
|
|
785
|
+
jsonOk(res, { cost, forecast, sessionTodayUsd });
|
|
786
|
+
});
|
|
787
|
+
routes.set('GET /api/anti-patterns', (_req, res) => {
|
|
788
|
+
if (!deps.antiPatternDetector)
|
|
789
|
+
return unavailable(res, 'antiPatternDetector');
|
|
790
|
+
jsonOk(res, deps.antiPatternDetector.getCurrentPatterns());
|
|
791
|
+
});
|
|
792
|
+
routes.set('GET /api/audit', (_req, res) => {
|
|
793
|
+
if (!deps.auditTrailManager)
|
|
794
|
+
return unavailable(res, 'auditTrailManager');
|
|
795
|
+
const log = deps.auditTrailManager.getAuditLog();
|
|
796
|
+
jsonOk(res, log.map(toAuditEntry));
|
|
797
|
+
});
|
|
798
|
+
routes.set('GET /api/weekly', (req, res) => {
|
|
799
|
+
if (!deps.weeklySummaryGenerator)
|
|
800
|
+
return unavailable(res, 'weeklySummaryGenerator');
|
|
801
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
802
|
+
const countStr = url.searchParams.get('count') ?? '';
|
|
803
|
+
let count = 12;
|
|
804
|
+
const parsed = parseInt(countStr, 10);
|
|
805
|
+
if (!Number.isNaN(parsed)) {
|
|
806
|
+
count = Math.min(Math.max(parsed, 1), 52);
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
deps.weeklySummaryGenerator.generate(getIsoWeekId(new Date()));
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
// best-effort — failure here means stale weekly data is returned, not a 500
|
|
813
|
+
console.error('Weekly summary generation failed', err);
|
|
814
|
+
}
|
|
815
|
+
jsonOk(res, deps.weeklySummaryGenerator.loadRecentWeeks(count));
|
|
816
|
+
});
|
|
817
|
+
routes.set('GET /api/budget', (_req, res) => {
|
|
818
|
+
if (!deps.budgetTracker)
|
|
819
|
+
return unavailable(res, 'budgetTracker');
|
|
820
|
+
jsonOk(res, deps.budgetTracker.getStatus());
|
|
821
|
+
});
|
|
822
|
+
routes.set('GET /api/latency', (_req, res) => {
|
|
823
|
+
if (!deps.latencyTracker)
|
|
824
|
+
return unavailable(res, 'latencyTracker');
|
|
825
|
+
jsonOk(res, deps.latencyTracker.getMetrics());
|
|
826
|
+
});
|
|
827
|
+
routes.set('GET /api/model-usage', (_req, res) => {
|
|
828
|
+
if (!deps.modelUsageTracker)
|
|
829
|
+
return unavailable(res, 'modelUsageTracker');
|
|
830
|
+
jsonOk(res, deps.modelUsageTracker.getMetrics());
|
|
831
|
+
});
|
|
832
|
+
routes.set('GET /api/cost-per-outcome', (req, res) => {
|
|
833
|
+
if (!deps.sessionStore?.loadAllSessions)
|
|
834
|
+
return unavailable(res, 'sessionStore.loadAllSessions');
|
|
835
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
836
|
+
const daysStr = url.searchParams.get('days') ?? '';
|
|
837
|
+
let days = 30;
|
|
838
|
+
const parsedDays = parseInt(daysStr, 10);
|
|
839
|
+
if (!Number.isNaN(parsedDays)) {
|
|
840
|
+
days = Math.min(Math.max(parsedDays, 1), 365);
|
|
841
|
+
}
|
|
842
|
+
const since = new Date(Date.now() - days * 86_400_000);
|
|
843
|
+
const sessions = deps.sessionStore.loadAllSessions({ since });
|
|
844
|
+
jsonOk(res, attributeSessionCosts(sessions));
|
|
845
|
+
});
|
|
846
|
+
routes.set('GET /api/personal-coach', (_req, res) => {
|
|
847
|
+
if (!deps.personalCoach)
|
|
848
|
+
return unavailable(res, 'personalCoach');
|
|
849
|
+
jsonOk(res, deps.personalCoach.generate());
|
|
850
|
+
});
|
|
851
|
+
routes.set('GET /api/alerts/recent', async (_req, res) => {
|
|
852
|
+
// 404 (not 503) when alerts are not configured — the route does not
|
|
853
|
+
// exist as a logical resource in cloud-only mode or when alerts are
|
|
854
|
+
// disabled. Plan §8 acceptance criterion calls for 404.
|
|
855
|
+
if (!deps.alertLog) {
|
|
856
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
857
|
+
res.end(JSON.stringify({ error: 'not_found' }));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Fixed limit (50) — matches the dashboard panel cap.
|
|
861
|
+
try {
|
|
862
|
+
const entries = await deps.alertLog.readRecent(50);
|
|
863
|
+
jsonOk(res, entries);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
// Log full error details server-side; never echo to the HTTP client.
|
|
867
|
+
// Stringifying the raw Error leaks file paths, env-var names, and
|
|
868
|
+
// potential connection-string fragments via stack frames.
|
|
869
|
+
console.error('alertLog.readRecent failed', err);
|
|
870
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
871
|
+
res.end(JSON.stringify({ error: 'internal' }));
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
routes.set('GET /api/quality-proxy', (_req, res) => {
|
|
875
|
+
if (!deps.qualityProxyTracker)
|
|
876
|
+
return unavailable(res, 'qualityProxyTracker');
|
|
877
|
+
const live = deps.qualityProxyTracker.getMetrics();
|
|
878
|
+
if (live.totalSignals > 0 || !deps.sessionStore) {
|
|
879
|
+
jsonOk(res, live);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
jsonOk(res, aggregateQualityFromHistory(deps.sessionStore.loadTodaySessions()));
|
|
883
|
+
});
|
|
884
|
+
routes.set('GET /api/tool-selection-score', (_req, res) => {
|
|
885
|
+
if (!deps.toolSelectionScorer)
|
|
886
|
+
return unavailable(res, 'toolSelectionScorer');
|
|
887
|
+
const calls = deps.toolCallBuffer?.getRecords() ?? [];
|
|
888
|
+
jsonOk(res, deps.toolSelectionScorer.scoreSession(calls));
|
|
889
|
+
});
|
|
890
|
+
routes.set('GET /api/git-efficiency', (_req, res) => {
|
|
891
|
+
if (!deps.gitEfficiencyTracker)
|
|
892
|
+
return unavailable(res, 'gitEfficiencyTracker');
|
|
893
|
+
jsonOk(res, deps.gitEfficiencyTracker.getMetrics());
|
|
894
|
+
});
|
|
895
|
+
routes.set('GET /api/context', (req, res) => {
|
|
896
|
+
if (!deps.contextTracker)
|
|
897
|
+
return unavailable(res, 'contextTracker');
|
|
898
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
899
|
+
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
900
|
+
jsonOk(res, deps.contextTracker.getMetrics(sessionId));
|
|
901
|
+
});
|
|
902
|
+
routes.set('GET /api/concurrency', (req, res) => {
|
|
903
|
+
if (!deps.concurrencyTracker)
|
|
904
|
+
return unavailable(res, 'concurrencyTracker');
|
|
905
|
+
try {
|
|
906
|
+
const todaySessions = deps.sessionStore?.loadTodaySessions() ?? [];
|
|
907
|
+
const historicalPeak = computeTodayPeakConcurrency(todaySessions);
|
|
908
|
+
const livePeak = deps.concurrencyTracker.getPeakConcurrent();
|
|
909
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
910
|
+
const view = url.searchParams.get('view');
|
|
911
|
+
if (view === 'history') {
|
|
912
|
+
const daysParam = url.searchParams.get('days');
|
|
913
|
+
const days = daysParam ? Math.min(Math.max(parseInt(daysParam, 10) || 30, 1), 90) : 30;
|
|
914
|
+
const since = new Date(localStartOfDay());
|
|
915
|
+
since.setDate(since.getDate() - days);
|
|
916
|
+
const allSessions = deps.sessionStore?.loadAllSessions?.({ since }) ?? [];
|
|
917
|
+
const dailyPeaks = computeDailyPeakConcurrency(allSessions, days);
|
|
918
|
+
// Override today's bucket with the live peak — disk-derived
|
|
919
|
+
// concurrency only sees persisted (completed) sessions, so a
|
|
920
|
+
// dashboard with active concurrent sessions but nothing flushed
|
|
921
|
+
// to disk yet would otherwise report peak=0 for today.
|
|
922
|
+
if (dailyPeaks.length > 0 && livePeak > 0) {
|
|
923
|
+
const today = dailyPeaks[dailyPeaks.length - 1];
|
|
924
|
+
if (today.peak < livePeak) {
|
|
925
|
+
dailyPeaks[dailyPeaks.length - 1] = { ...today, peak: livePeak };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
jsonOk(res, { dailyPeaks });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const allSessions = deps.sessionStore?.loadAllSessions?.() ?? [];
|
|
932
|
+
const allTimePeak = computeTodayPeakConcurrency(allSessions);
|
|
933
|
+
// 96 × 15-minute fixed-grid buckets covering today (local midnight →
|
|
934
|
+
// local midnight + 24h). Each bucket holds the peak concurrent
|
|
935
|
+
// session count within its window. Bounded payload (~10 KB) and
|
|
936
|
+
// renders cleanly at any zoom level. Replaces the prior unbounded
|
|
937
|
+
// 30-second rolling timeSeries.
|
|
938
|
+
const startTimestamp = localStartOfDay();
|
|
939
|
+
const liveIds = deps.liveSessionRegistry?.getLiveSessions() ?? [];
|
|
940
|
+
const bufferRecords = deps.toolCallBuffer?.getRecords() ?? [];
|
|
941
|
+
// Synthetic session ids (`local-*`, `proxy-*`) are hidden from
|
|
942
|
+
// /api/sessions and /api/sessions/live. Apply the same filter here
|
|
943
|
+
// so persisted synthetic records on disk don't contribute to bucket
|
|
944
|
+
// counts either — otherwise the buckets disagree with the session
|
|
945
|
+
// list shown alongside the chart.
|
|
946
|
+
const filteredTodaySessions = todaySessions.filter((s) => {
|
|
947
|
+
const sid = s.sessionId;
|
|
948
|
+
return !isSyntheticSessionId(sid);
|
|
949
|
+
});
|
|
950
|
+
const intervals = collectTodaySessionIntervals(filteredTodaySessions, liveIds.filter((id) => !isSyntheticSessionId(id)), bufferRecords, Date.now());
|
|
951
|
+
const buckets = computeTodayConcurrencyBuckets(intervals, startTimestamp);
|
|
952
|
+
jsonOk(res, {
|
|
953
|
+
current: deps.concurrencyTracker.getConcurrentCount(),
|
|
954
|
+
peak: Math.max(livePeak, historicalPeak),
|
|
955
|
+
allTimePeak: Math.max(livePeak, historicalPeak, allTimePeak),
|
|
956
|
+
bucketSizeMs: CONCURRENCY_BUCKET_SIZE_MS,
|
|
957
|
+
startTimestamp,
|
|
958
|
+
buckets,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
963
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
routes.set('GET /api/activity-heatmap', (req, res) => {
|
|
967
|
+
try {
|
|
968
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
969
|
+
const view = url.searchParams.get('view') ?? 'today';
|
|
970
|
+
if (view === 'today') {
|
|
971
|
+
const now = Date.now();
|
|
972
|
+
const startMs = localStartOfDay(now);
|
|
973
|
+
const bucketSizeMs = 900_000;
|
|
974
|
+
const bucketCount = Math.ceil((now - startMs) / bucketSizeMs) || 1;
|
|
975
|
+
const buckets = new Array(bucketCount).fill(0);
|
|
976
|
+
const bufferRecords = deps.toolCallBuffer?.getRecords() ?? [];
|
|
977
|
+
for (const r of bufferRecords) {
|
|
978
|
+
if (r.timestamp >= startMs) {
|
|
979
|
+
const idx = Math.floor((r.timestamp - startMs) / bucketSizeMs);
|
|
980
|
+
if (idx >= 0 && idx < bucketCount) {
|
|
981
|
+
buckets[idx]++;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const todaySessions = deps.sessionStore?.loadTodaySessions() ?? [];
|
|
986
|
+
for (const s of todaySessions) {
|
|
987
|
+
const session = s;
|
|
988
|
+
if (session.timeline) {
|
|
989
|
+
for (const entry of session.timeline) {
|
|
990
|
+
if (entry.timestamp >= startMs) {
|
|
991
|
+
const idx = Math.floor((entry.timestamp - startMs) / bucketSizeMs);
|
|
992
|
+
if (idx >= 0 && idx < bucketCount) {
|
|
993
|
+
buckets[idx]++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const maxCount = Math.max(...buckets, 1);
|
|
1000
|
+
jsonOk(res, { buckets, bucketSizeMs, startTimestamp: startMs, maxCount });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (view === 'history') {
|
|
1004
|
+
const weeksParam = url.searchParams.get('weeks');
|
|
1005
|
+
const weeks = weeksParam ? Math.min(Math.max(parseInt(weeksParam, 10) || 12, 1), 52) : 12;
|
|
1006
|
+
const now = new Date();
|
|
1007
|
+
const startDate = new Date(now);
|
|
1008
|
+
startDate.setUTCDate(startDate.getUTCDate() - weeks * 7);
|
|
1009
|
+
startDate.setUTCHours(0, 0, 0, 0);
|
|
1010
|
+
const sessions = deps.sessionStore?.loadAllSessions?.({ since: startDate }) ?? [];
|
|
1011
|
+
const dayMap = new Map();
|
|
1012
|
+
const cursor = new Date(startDate);
|
|
1013
|
+
while (cursor <= now) {
|
|
1014
|
+
dayMap.set(cursor.toISOString().slice(0, 10), 0);
|
|
1015
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
1016
|
+
}
|
|
1017
|
+
for (const s of sessions) {
|
|
1018
|
+
const session = s;
|
|
1019
|
+
if (!session.startTime)
|
|
1020
|
+
continue;
|
|
1021
|
+
const d = new Date(typeof session.startTime === 'number' ? session.startTime : session.startTime);
|
|
1022
|
+
if (d < startDate)
|
|
1023
|
+
continue;
|
|
1024
|
+
const key = d.toISOString().slice(0, 10);
|
|
1025
|
+
if (dayMap.has(key)) {
|
|
1026
|
+
dayMap.set(key, (dayMap.get(key) ?? 0) + (session.toolCallCount ?? 0));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const days = Array.from(dayMap.entries()).map(([date, count]) => ({ date, count }));
|
|
1030
|
+
const maxCount = Math.max(...days.map((d) => d.count), 1);
|
|
1031
|
+
jsonOk(res, { days, maxCount });
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
1035
|
+
res.end(JSON.stringify({ error: 'invalid_view', message: 'Use view=today or view=history' }));
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
1039
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
routes.set('GET /api/git-efficiency/repos', (_req, res) => {
|
|
1043
|
+
if (!deps.sessionStore)
|
|
1044
|
+
return unavailable(res, 'sessionStore');
|
|
1045
|
+
const todaySessions = deps.sessionStore.loadTodaySessions();
|
|
1046
|
+
const repoSet = new Set();
|
|
1047
|
+
for (const session of todaySessions) {
|
|
1048
|
+
if (typeof session.repoName === 'string' && session.repoName) {
|
|
1049
|
+
repoSet.add(session.repoName);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// Include the current repo from git efficiency tracker if available
|
|
1053
|
+
let currentRepo = null;
|
|
1054
|
+
if (deps.gitEfficiencyTracker) {
|
|
1055
|
+
const metrics = deps.gitEfficiencyTracker.getMetrics();
|
|
1056
|
+
const trackerRepo = metrics.repoContext?.repoName ?? null;
|
|
1057
|
+
if (trackerRepo) {
|
|
1058
|
+
currentRepo = trackerRepo;
|
|
1059
|
+
repoSet.add(trackerRepo);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
jsonOk(res, { repos: [...repoSet].sort(), currentRepo });
|
|
1063
|
+
});
|
|
1064
|
+
// ── Settings endpoints ──────────────────────────────────────────────────
|
|
1065
|
+
routes.set('GET /api/settings', (_req, res) => {
|
|
1066
|
+
if (!deps.config)
|
|
1067
|
+
return unavailable(res, 'config');
|
|
1068
|
+
const c = deps.config;
|
|
1069
|
+
// Read editable fields from disk so the UI reflects the latest saved
|
|
1070
|
+
// values after a PATCH (deps.config is frozen at startup and never
|
|
1071
|
+
// updated in memory).
|
|
1072
|
+
let disk = {};
|
|
1073
|
+
if (deps.configFilePath) {
|
|
1074
|
+
try {
|
|
1075
|
+
disk = JSON.parse(readFileSync(deps.configFilePath, 'utf-8'));
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
/* config file may not exist yet — fall through to startup defaults */
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const diskAlerts = (disk.alerts ?? {});
|
|
1082
|
+
const diskPersonal = (diskAlerts['personal'] ?? {});
|
|
1083
|
+
jsonOk(res, {
|
|
1084
|
+
// Editable fields: prefer disk, fall back to startup config
|
|
1085
|
+
developer: typeof disk.developer === 'string' ? disk.developer : c.developer,
|
|
1086
|
+
teamId: 'teamId' in disk ? disk.teamId : c.teamId,
|
|
1087
|
+
sessionBudgetUsd: 'sessionBudgetUsd' in disk ? disk.sessionBudgetUsd : c.sessionBudgetUsd,
|
|
1088
|
+
dailyBudgetUsd: 'dailyBudgetUsd' in disk ? disk.dailyBudgetUsd : c.dailyBudgetUsd,
|
|
1089
|
+
weeklyBudgetUsd: 'weeklyBudgetUsd' in disk ? disk.weeklyBudgetUsd : c.weeklyBudgetUsd,
|
|
1090
|
+
retainSessionsDays: 'retainSessionsDays' in disk
|
|
1091
|
+
? disk.retainSessionsDays
|
|
1092
|
+
: c.retainSessionsDays,
|
|
1093
|
+
digestWebhookUrl: 'digestWebhookUrl' in disk ? disk.digestWebhookUrl : c.digestWebhookUrl,
|
|
1094
|
+
digestSchedule: typeof disk.digestSchedule === 'string' ? disk.digestSchedule : c.digestSchedule,
|
|
1095
|
+
alerts: {
|
|
1096
|
+
personal: {
|
|
1097
|
+
dailyCostUsd: typeof diskPersonal['dailyCostUsd'] === 'number'
|
|
1098
|
+
? diskPersonal['dailyCostUsd']
|
|
1099
|
+
: c.personalAlertThresholds.dailyCostUsd,
|
|
1100
|
+
sessionCostUsd: typeof diskPersonal['sessionCostUsd'] === 'number'
|
|
1101
|
+
? diskPersonal['sessionCostUsd']
|
|
1102
|
+
: c.personalAlertThresholds.sessionCostUsd,
|
|
1103
|
+
efficiencyScoreMin: typeof diskPersonal['efficiencyScoreMin'] === 'number'
|
|
1104
|
+
? diskPersonal['efficiencyScoreMin']
|
|
1105
|
+
: c.personalAlertThresholds.efficiencyScoreMin,
|
|
1106
|
+
stuckLoopCountMax: typeof diskPersonal['stuckLoopCountMax'] === 'number'
|
|
1107
|
+
? diskPersonal['stuckLoopCountMax']
|
|
1108
|
+
: c.personalAlertThresholds.stuckLoopCountMax,
|
|
1109
|
+
antiPatternCountMax: typeof diskPersonal['antiPatternCountMax'] === 'number'
|
|
1110
|
+
? diskPersonal['antiPatternCountMax']
|
|
1111
|
+
: c.personalAlertThresholds.antiPatternCountMax,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
// Read-only fields always from startup config
|
|
1115
|
+
accountId: c.accountId ?? null,
|
|
1116
|
+
appName: c.appName,
|
|
1117
|
+
mode: c.mode,
|
|
1118
|
+
storagePath: c.storagePath,
|
|
1119
|
+
highSecurity: c.highSecurity,
|
|
1120
|
+
licenseKey: c.licenseKey ? '••••' + c.licenseKey.slice(-4) : null,
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
routes.set('PATCH /api/settings', async (req, res) => {
|
|
1124
|
+
if (!deps.configFilePath)
|
|
1125
|
+
return unavailable(res, 'configFilePath');
|
|
1126
|
+
let body;
|
|
1127
|
+
try {
|
|
1128
|
+
body = JSON.parse(await readBody(req));
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
1132
|
+
res.end(JSON.stringify({ error: 'invalid_json' }));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
let existing = {};
|
|
1136
|
+
try {
|
|
1137
|
+
existing = JSON.parse(readFileSync(deps.configFilePath, 'utf-8'));
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
/* no existing config — start fresh */
|
|
1141
|
+
}
|
|
1142
|
+
const errors = [];
|
|
1143
|
+
let digestUrlOnly = true; // tracks whether only digest URL changed
|
|
1144
|
+
if ('developer' in body) {
|
|
1145
|
+
if (typeof body.developer !== 'string') {
|
|
1146
|
+
errors.push('developer must be a string');
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
existing.developer = normalizeDeveloperName(body.developer);
|
|
1150
|
+
digestUrlOnly = false;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if ('teamId' in body) {
|
|
1154
|
+
if (body.teamId !== null && typeof body.teamId !== 'string') {
|
|
1155
|
+
errors.push('teamId must be string or null');
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
existing.teamId = body.teamId;
|
|
1159
|
+
digestUrlOnly = false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if ('sessionBudgetUsd' in body) {
|
|
1163
|
+
if (body.sessionBudgetUsd !== null &&
|
|
1164
|
+
(typeof body.sessionBudgetUsd !== 'number' || body.sessionBudgetUsd <= 0)) {
|
|
1165
|
+
errors.push('sessionBudgetUsd must be a positive number or null');
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
existing.sessionBudgetUsd = body.sessionBudgetUsd;
|
|
1169
|
+
digestUrlOnly = false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if ('dailyBudgetUsd' in body) {
|
|
1173
|
+
if (body.dailyBudgetUsd !== null &&
|
|
1174
|
+
(typeof body.dailyBudgetUsd !== 'number' || body.dailyBudgetUsd <= 0)) {
|
|
1175
|
+
errors.push('dailyBudgetUsd must be a positive number or null');
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
existing.dailyBudgetUsd = body.dailyBudgetUsd;
|
|
1179
|
+
digestUrlOnly = false;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if ('weeklyBudgetUsd' in body) {
|
|
1183
|
+
if (body.weeklyBudgetUsd !== null &&
|
|
1184
|
+
(typeof body.weeklyBudgetUsd !== 'number' || body.weeklyBudgetUsd <= 0)) {
|
|
1185
|
+
errors.push('weeklyBudgetUsd must be a positive number or null');
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
existing.weeklyBudgetUsd = body.weeklyBudgetUsd;
|
|
1189
|
+
digestUrlOnly = false;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if ('retainSessionsDays' in body) {
|
|
1193
|
+
if (body.retainSessionsDays !== null &&
|
|
1194
|
+
(!Number.isInteger(body.retainSessionsDays) ||
|
|
1195
|
+
body.retainSessionsDays < 1 ||
|
|
1196
|
+
body.retainSessionsDays > 365)) {
|
|
1197
|
+
errors.push('retainSessionsDays must be integer 1-365 or null');
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
existing.retainSessionsDays = body.retainSessionsDays;
|
|
1201
|
+
digestUrlOnly = false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if ('digestWebhookUrl' in body) {
|
|
1205
|
+
if (body.digestWebhookUrl !== null &&
|
|
1206
|
+
(typeof body.digestWebhookUrl !== 'string' ||
|
|
1207
|
+
!body.digestWebhookUrl.startsWith('https://hooks.slack.com/'))) {
|
|
1208
|
+
errors.push('digestWebhookUrl must be a Slack incoming webhook URL (https://hooks.slack.com/...) or null');
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
existing.digestWebhookUrl = body.digestWebhookUrl ?? undefined;
|
|
1212
|
+
if (existing.digestWebhookUrl === undefined) {
|
|
1213
|
+
delete existing.digestWebhookUrl;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if ('digestSchedule' in body) {
|
|
1218
|
+
if (typeof body.digestSchedule !== 'string') {
|
|
1219
|
+
errors.push('digestSchedule must be a string');
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
existing.digestSchedule = body.digestSchedule;
|
|
1223
|
+
digestUrlOnly = false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if ('alerts' in body) {
|
|
1227
|
+
const alertsBody = body.alerts;
|
|
1228
|
+
const personal = alertsBody?.['personal'];
|
|
1229
|
+
if (personal) {
|
|
1230
|
+
const existingAlerts = (existing.alerts ?? {});
|
|
1231
|
+
const existingPersonal = (existingAlerts['personal'] ?? {});
|
|
1232
|
+
if ('dailyCostUsd' in personal) {
|
|
1233
|
+
if (typeof personal.dailyCostUsd !== 'number' || personal.dailyCostUsd < 0) {
|
|
1234
|
+
errors.push('alerts.personal.dailyCostUsd must be a non-negative number');
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
existingPersonal.dailyCostUsd = personal.dailyCostUsd;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if ('sessionCostUsd' in personal) {
|
|
1241
|
+
if (typeof personal.sessionCostUsd !== 'number' || personal.sessionCostUsd < 0) {
|
|
1242
|
+
errors.push('alerts.personal.sessionCostUsd must be a non-negative number');
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
existingPersonal.sessionCostUsd = personal.sessionCostUsd;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if ('efficiencyScoreMin' in personal) {
|
|
1249
|
+
if (typeof personal.efficiencyScoreMin !== 'number' ||
|
|
1250
|
+
personal.efficiencyScoreMin < 0 ||
|
|
1251
|
+
personal.efficiencyScoreMin > 1) {
|
|
1252
|
+
errors.push('alerts.personal.efficiencyScoreMin must be 0-1');
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
existingPersonal.efficiencyScoreMin = personal.efficiencyScoreMin;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if ('stuckLoopCountMax' in personal) {
|
|
1259
|
+
if (!Number.isInteger(personal.stuckLoopCountMax) ||
|
|
1260
|
+
personal.stuckLoopCountMax < 0) {
|
|
1261
|
+
errors.push('alerts.personal.stuckLoopCountMax must be a non-negative integer');
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
existingPersonal.stuckLoopCountMax = personal.stuckLoopCountMax;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if ('antiPatternCountMax' in personal) {
|
|
1268
|
+
if (!Number.isInteger(personal.antiPatternCountMax) ||
|
|
1269
|
+
personal.antiPatternCountMax < 0) {
|
|
1270
|
+
errors.push('alerts.personal.antiPatternCountMax must be a non-negative integer');
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
existingPersonal.antiPatternCountMax = personal.antiPatternCountMax;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
existingAlerts['personal'] = existingPersonal;
|
|
1277
|
+
existing.alerts = existingAlerts;
|
|
1278
|
+
digestUrlOnly = false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (errors.length > 0) {
|
|
1282
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
1283
|
+
res.end(JSON.stringify({ error: 'validation_failed', errors }));
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
writeFileSync(deps.configFilePath, JSON.stringify(existing, null, 2), { mode: 0o600 });
|
|
1287
|
+
jsonOk(res, { ok: true, restartRequired: !digestUrlOnly });
|
|
1288
|
+
});
|
|
1289
|
+
routes.set('POST /api/digest/send', async (_req, res) => {
|
|
1290
|
+
if (!deps.weeklySummaryGenerator || !deps.configFilePath)
|
|
1291
|
+
return unavailable(res, 'digest');
|
|
1292
|
+
const result = await handleSendDigest(deps.weeklySummaryGenerator, deps.configFilePath);
|
|
1293
|
+
jsonOk(res, result);
|
|
1294
|
+
});
|
|
1295
|
+
return async (req, res) => {
|
|
1296
|
+
try {
|
|
1297
|
+
const path = (req.url ?? '/').split('?')[0] ?? '/';
|
|
1298
|
+
const key = `${req.method ?? 'GET'} ${path}`;
|
|
1299
|
+
const fn = routes.get(key);
|
|
1300
|
+
if (fn) {
|
|
1301
|
+
await fn(req, res);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// Try dynamic routes
|
|
1305
|
+
const replayMatch = /^\/api\/sessions\/([A-Za-z0-9_-]{1,128})\/replay$/.exec(path);
|
|
1306
|
+
if (req.method === 'GET' && replayMatch) {
|
|
1307
|
+
const sessionId = replayMatch[1];
|
|
1308
|
+
const replay = buildReplayResponse(sessionId, deps);
|
|
1309
|
+
if (replay === null) {
|
|
1310
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1311
|
+
res.end(JSON.stringify({ error: 'no_replay_data' }));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
jsonOk(res, replay);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const sessionIdMatch = /^\/api\/sessions\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
1318
|
+
if (req.method === 'GET' && sessionIdMatch) {
|
|
1319
|
+
const sessionId = sessionIdMatch[1];
|
|
1320
|
+
if (!deps.sessionStore)
|
|
1321
|
+
return unavailable(res, 'sessionStore');
|
|
1322
|
+
const session = deps.sessionStore.loadSession(sessionId);
|
|
1323
|
+
if (session != null) {
|
|
1324
|
+
jsonOk(res, session);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
// Not persisted — check if it's the current live session
|
|
1328
|
+
if (deps.sessionTracker) {
|
|
1329
|
+
const live = deps.sessionTracker.getMetrics();
|
|
1330
|
+
if (live.sessionId === sessionId) {
|
|
1331
|
+
const costMetrics = deps.costTracker?.getMetrics();
|
|
1332
|
+
const costUsd = costMetrics?.sessionTotalCostUsd ?? null;
|
|
1333
|
+
const model = costMetrics?.model ?? null;
|
|
1334
|
+
const antiPatterns = deps.antiPatternDetector
|
|
1335
|
+
? deps.antiPatternDetector.getCurrentPatterns()
|
|
1336
|
+
: [];
|
|
1337
|
+
jsonOk(res, {
|
|
1338
|
+
sessionId: live.sessionId,
|
|
1339
|
+
sessionName: live.sessionName ?? null,
|
|
1340
|
+
startTime: live.sessionStartTime,
|
|
1341
|
+
durationMs: live.sessionDurationMs,
|
|
1342
|
+
toolCallCount: live.toolCallCount,
|
|
1343
|
+
estimatedCostUsd: costUsd,
|
|
1344
|
+
model,
|
|
1345
|
+
outcome: 'in progress',
|
|
1346
|
+
toolBreakdown: live.toolCallCountByTool,
|
|
1347
|
+
antiPatterns,
|
|
1348
|
+
// Use the same `timeline` shape as persisted sessions so the
|
|
1349
|
+
// Sessions and Replay views can consume one type. See
|
|
1350
|
+
// src/storage/types.ts ReplayTimelineEntry.
|
|
1351
|
+
timeline: live.toolCallTimeline.map((t) => ({
|
|
1352
|
+
timestamp: t.timestamp,
|
|
1353
|
+
toolName: t.toolName,
|
|
1354
|
+
durationMs: t.durationMs,
|
|
1355
|
+
success: t.success ?? true,
|
|
1356
|
+
})),
|
|
1357
|
+
});
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// Concurrent live session tracked by the registry but not this server's
|
|
1362
|
+
// own session — synthesize from tool call buffer records.
|
|
1363
|
+
if (deps.liveSessionRegistry?.getLiveSessions().includes(sessionId)) {
|
|
1364
|
+
const allRecords = deps.toolCallBuffer?.getRecords() ?? [];
|
|
1365
|
+
const records = allRecords.filter((r) => r.sessionId === sessionId);
|
|
1366
|
+
const timeline = records
|
|
1367
|
+
.map((r) => ({
|
|
1368
|
+
timestamp: r.timestamp,
|
|
1369
|
+
toolName: r.toolName,
|
|
1370
|
+
durationMs: r.durationMs ?? null,
|
|
1371
|
+
success: r.success,
|
|
1372
|
+
filePath: r.filePath ? redactSensitive(String(r.filePath)) : undefined,
|
|
1373
|
+
command: r.command ? redactSensitive(String(r.command)) : undefined,
|
|
1374
|
+
}))
|
|
1375
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
1376
|
+
const breakdown = Object.create(null);
|
|
1377
|
+
for (const r of records) {
|
|
1378
|
+
breakdown[r.toolName] = (breakdown[r.toolName] ?? 0) + 1;
|
|
1379
|
+
}
|
|
1380
|
+
const startTime = timeline.length > 0 ? timeline[0].timestamp : Date.now();
|
|
1381
|
+
const lastTs = timeline.length > 0 ? timeline[timeline.length - 1].timestamp : startTime;
|
|
1382
|
+
jsonOk(res, {
|
|
1383
|
+
sessionId,
|
|
1384
|
+
sessionName: deps.liveSessionRegistry.getSessionName(sessionId),
|
|
1385
|
+
startTime,
|
|
1386
|
+
durationMs: lastTs - startTime,
|
|
1387
|
+
toolCallCount: records.length,
|
|
1388
|
+
estimatedCostUsd: null,
|
|
1389
|
+
model: null,
|
|
1390
|
+
outcome: 'in progress',
|
|
1391
|
+
toolBreakdown: breakdown,
|
|
1392
|
+
antiPatterns: [],
|
|
1393
|
+
timeline,
|
|
1394
|
+
});
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1398
|
+
res.end(JSON.stringify({ error: 'not_found' }));
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1402
|
+
res.end(JSON.stringify({ error: 'not_found' }));
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
const logger = (await import('../../shared/index.js')).createLogger('api-handler');
|
|
1406
|
+
logger.error('Unhandled error in API route handler', { error: String(err) });
|
|
1407
|
+
if (!res.headersSent) {
|
|
1408
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
1409
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
//# sourceMappingURL=api-handler.js.map
|