@agentdeck/bridge 0.2.2 → 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/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
- package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/assets/fonts/LICENSES.md +22 -0
- package/dist/adapters/claude-code.d.ts +6 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +8 -0
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/openclaw.d.ts +24 -0
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +291 -35
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +59 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -1
- package/dist/adapters/opencode-adapter.js +297 -35
- package/dist/adapters/opencode-adapter.js.map +1 -1
- package/dist/adb-reverse.d.ts +1 -1
- package/dist/adb-reverse.js +1 -1
- package/dist/antigravity-local.d.ts +1 -0
- package/dist/antigravity-local.d.ts.map +1 -1
- package/dist/antigravity-local.js +33 -0
- package/dist/antigravity-local.js.map +1 -1
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
- package/dist/apme/adapters/claude-hook.js +5 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -1
- package/dist/apme/adapters/codex-hook.d.ts +17 -0
- package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
- package/dist/apme/adapters/codex-hook.js +95 -0
- package/dist/apme/adapters/codex-hook.js.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.js +496 -0
- package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
- package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
- package/dist/apme/adapters/openclaw-hook.js +234 -0
- package/dist/apme/adapters/openclaw-hook.js.map +1 -0
- package/dist/apme/adapters/opencode-hook.d.ts +56 -0
- package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
- package/dist/apme/adapters/opencode-hook.js +182 -0
- package/dist/apme/adapters/opencode-hook.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -1
- package/dist/apme/adapters/timeline.js +6 -1
- package/dist/apme/adapters/timeline.js.map +1 -1
- package/dist/apme/antigravity-transcript.d.ts +18 -0
- package/dist/apme/antigravity-transcript.d.ts.map +1 -0
- package/dist/apme/antigravity-transcript.js +94 -0
- package/dist/apme/antigravity-transcript.js.map +1 -0
- package/dist/apme/classify-turn.d.ts +13 -0
- package/dist/apme/classify-turn.d.ts.map +1 -0
- package/dist/apme/classify-turn.js +49 -0
- package/dist/apme/classify-turn.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +10 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +35 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/apme/collector.d.ts +133 -6
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +471 -23
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +117 -11
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +52 -6
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts +26 -5
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +146 -9
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/judge-detect.d.ts +26 -0
- package/dist/apme/judge-detect.d.ts.map +1 -0
- package/dist/apme/judge-detect.js +69 -0
- package/dist/apme/judge-detect.js.map +1 -0
- package/dist/apme/pareto.d.ts +39 -0
- package/dist/apme/pareto.d.ts.map +1 -0
- package/dist/apme/pareto.js +59 -0
- package/dist/apme/pareto.js.map +1 -0
- package/dist/apme/recommend.d.ts +3 -0
- package/dist/apme/recommend.d.ts.map +1 -1
- package/dist/apme/recommend.js +43 -1
- package/dist/apme/recommend.js.map +1 -1
- package/dist/apme/runner.d.ts +99 -4
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +743 -72
- package/dist/apme/runner.js.map +1 -1
- package/dist/apme/sample-to-timeline.d.ts +26 -0
- package/dist/apme/sample-to-timeline.d.ts.map +1 -0
- package/dist/apme/sample-to-timeline.js +95 -0
- package/dist/apme/sample-to-timeline.js.map +1 -0
- package/dist/apme/scorers/index.d.ts +47 -0
- package/dist/apme/scorers/index.d.ts.map +1 -0
- package/dist/apme/scorers/index.js +96 -0
- package/dist/apme/scorers/index.js.map +1 -0
- package/dist/apme/settings.d.ts +17 -11
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +30 -9
- package/dist/apme/settings.js.map +1 -1
- package/dist/apme/store.d.ts +33 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +334 -25
- package/dist/apme/store.js.map +1 -1
- package/dist/awaiting-overlay.d.ts +112 -0
- package/dist/awaiting-overlay.d.ts.map +1 -0
- package/dist/awaiting-overlay.js +179 -0
- package/dist/awaiting-overlay.js.map +1 -0
- package/dist/ble-sync-spawn.d.ts +63 -0
- package/dist/ble-sync-spawn.d.ts.map +1 -0
- package/dist/ble-sync-spawn.js +196 -0
- package/dist/ble-sync-spawn.js.map +1 -0
- package/dist/bridge-core.d.ts +34 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +207 -24
- package/dist/bridge-core.js.map +1 -1
- package/dist/broadcast.d.ts +3 -0
- package/dist/broadcast.d.ts.map +1 -0
- package/dist/broadcast.js +122 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +25 -9
- package/dist/check-deps.js.map +1 -1
- package/dist/claude-permission-rules.d.ts +55 -0
- package/dist/claude-permission-rules.d.ts.map +1 -0
- package/dist/claude-permission-rules.js +197 -0
- package/dist/claude-permission-rules.js.map +1 -0
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1030 -93
- package/dist/cli.js.map +1 -1
- package/dist/codex-auth.d.ts +21 -0
- package/dist/codex-auth.d.ts.map +1 -1
- package/dist/codex-auth.js +52 -1
- package/dist/codex-auth.js.map +1 -1
- package/dist/codex-output-parser.d.ts +8 -0
- package/dist/codex-output-parser.d.ts.map +1 -1
- package/dist/codex-output-parser.js +110 -25
- package/dist/codex-output-parser.js.map +1 -1
- package/dist/codex-rate-limits.d.ts +16 -0
- package/dist/codex-rate-limits.d.ts.map +1 -0
- package/dist/codex-rate-limits.js +197 -0
- package/dist/codex-rate-limits.js.map +1 -0
- package/dist/codex-rollout-response.d.ts +31 -0
- package/dist/codex-rollout-response.d.ts.map +1 -0
- package/dist/codex-rollout-response.js +135 -0
- package/dist/codex-rollout-response.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +4 -16
- package/dist/d200h/image-renderer.d.ts.map +1 -1
- package/dist/d200h/image-renderer.js +46 -212
- package/dist/d200h/image-renderer.js.map +1 -1
- package/dist/daemon-server.d.ts +85 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +1975 -159
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +15 -6
- package/dist/daemon.js.map +1 -1
- package/dist/display-dim.d.ts +9 -0
- package/dist/display-dim.d.ts.map +1 -0
- package/dist/display-dim.js +53 -0
- package/dist/display-dim.js.map +1 -0
- package/dist/display-monitor.d.ts +15 -2
- package/dist/display-monitor.d.ts.map +1 -1
- package/dist/display-monitor.js +104 -26
- package/dist/display-monitor.js.map +1 -1
- package/dist/esp32-serial.d.ts +190 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +1090 -156
- package/dist/esp32-serial.js.map +1 -1
- package/dist/fallback-task-timeline.d.ts +42 -0
- package/dist/fallback-task-timeline.d.ts.map +1 -0
- package/dist/fallback-task-timeline.js +119 -0
- package/dist/fallback-task-timeline.js.map +1 -0
- package/dist/foundation-models-helper.d.ts +11 -0
- package/dist/foundation-models-helper.d.ts.map +1 -0
- package/dist/foundation-models-helper.js +241 -0
- package/dist/foundation-models-helper.js.map +1 -0
- package/dist/hook-migration.js +1 -1
- package/dist/hook-migration.js.map +1 -1
- package/dist/hook-server.d.ts +2 -3
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +23 -10
- package/dist/hook-server.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.js +85 -0
- package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.js +57 -0
- package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +217 -179
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +15 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +18 -96
- package/dist/log-stream.js.map +1 -1
- package/dist/logger.d.ts +3 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +18 -5
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +17 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +52 -18
- package/dist/mdns.js.map +1 -1
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +2 -4
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.d.ts +1 -1
- package/dist/modules/adb-module.d.ts.map +1 -1
- package/dist/modules/adb-module.js +4 -3
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/modules/broadcast-module.d.ts +17 -0
- package/dist/modules/broadcast-module.d.ts.map +1 -0
- package/dist/modules/broadcast-module.js +26 -0
- package/dist/modules/broadcast-module.js.map +1 -0
- package/dist/modules/d200h-module.d.ts +22 -0
- package/dist/modules/d200h-module.d.ts.map +1 -1
- package/dist/modules/d200h-module.js +200 -65
- package/dist/modules/d200h-module.js.map +1 -1
- package/dist/modules/idotmatrix-module.d.ts +9 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -0
- package/dist/modules/idotmatrix-module.js +46 -0
- package/dist/modules/idotmatrix-module.js.map +1 -0
- package/dist/modules/index.d.ts +3 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +9 -3
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/pixoo-module.d.ts.map +1 -1
- package/dist/modules/pixoo-module.js +14 -4
- package/dist/modules/pixoo-module.js.map +1 -1
- package/dist/modules/serial-module.d.ts +4 -0
- package/dist/modules/serial-module.d.ts.map +1 -1
- package/dist/modules/serial-module.js +14 -12
- package/dist/modules/serial-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts +9 -0
- package/dist/modules/timebox-module.d.ts.map +1 -0
- package/dist/modules/timebox-module.js +46 -0
- package/dist/modules/timebox-module.js.map +1 -0
- package/dist/modules/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/modules/types.d.ts +4 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/observed-steering.d.ts +93 -0
- package/dist/observed-steering.d.ts.map +1 -0
- package/dist/observed-steering.js +247 -0
- package/dist/observed-steering.js.map +1 -0
- package/dist/openclaw-session.d.ts +18 -0
- package/dist/openclaw-session.d.ts.map +1 -0
- package/dist/openclaw-session.js +34 -0
- package/dist/openclaw-session.js.map +1 -0
- package/dist/opencode-steering.d.ts +36 -0
- package/dist/opencode-steering.d.ts.map +1 -0
- package/dist/opencode-steering.js +84 -0
- package/dist/opencode-steering.js.map +1 -0
- package/dist/output-parser.d.ts +30 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +172 -18
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +32 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +288 -49
- package/dist/passive-observer.js.map +1 -1
- package/dist/permission-resolver.d.ts +55 -0
- package/dist/permission-resolver.d.ts.map +1 -0
- package/dist/permission-resolver.js +112 -0
- package/dist/permission-resolver.js.map +1 -0
- package/dist/pixoo/micro-glyphs.d.ts +18 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
- package/dist/pixoo/micro-glyphs.js +143 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -0
- package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
- package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
- package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
- package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +16 -8
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +127 -35
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-camera.d.ts +19 -3
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
- package/dist/pixoo/pixoo-camera.js +62 -27
- package/dist/pixoo/pixoo-camera.js.map +1 -1
- package/dist/pixoo/pixoo-client.d.ts +13 -3
- package/dist/pixoo/pixoo-client.d.ts.map +1 -1
- package/dist/pixoo/pixoo-client.js +51 -22
- package/dist/pixoo/pixoo-client.js.map +1 -1
- package/dist/pixoo/pixoo-discover.d.ts +37 -0
- package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
- package/dist/pixoo/pixoo-discover.js +105 -0
- package/dist/pixoo/pixoo-discover.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +27 -3
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +533 -135
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-settings.d.ts +6 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
- package/dist/pixoo/pixoo-settings.js +9 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts +84 -11
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +620 -209
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +8 -2
- package/dist/pty-manager.js.map +1 -1
- package/dist/review-runner.d.ts +113 -0
- package/dist/review-runner.d.ts.map +1 -0
- package/dist/review-runner.js +428 -0
- package/dist/review-runner.js.map +1 -0
- package/dist/session-activity.d.ts +14 -0
- package/dist/session-activity.d.ts.map +1 -0
- package/dist/session-activity.js +160 -0
- package/dist/session-activity.js.map +1 -0
- package/dist/session-aggregator.d.ts +25 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +48 -9
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-focus-relay.d.ts.map +1 -1
- package/dist/session-focus-relay.js +7 -1
- package/dist/session-focus-relay.js.map +1 -1
- package/dist/session-registry.d.ts +74 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +140 -8
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts.map +1 -1
- package/dist/session-timeline-relay.js +13 -4
- package/dist/session-timeline-relay.js.map +1 -1
- package/dist/session-transcript-timeline.d.ts +49 -0
- package/dist/session-transcript-timeline.d.ts.map +1 -0
- package/dist/session-transcript-timeline.js +276 -0
- package/dist/session-transcript-timeline.js.map +1 -0
- package/dist/state-machine.d.ts +6 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +86 -28
- package/dist/state-machine.js.map +1 -1
- package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
- package/dist/timebox/timebox-daemon-sync.js +135 -0
- package/dist/timebox/timebox-daemon-sync.js.map +1 -0
- package/dist/timebox/timebox-discover.d.ts +19 -0
- package/dist/timebox/timebox-discover.d.ts.map +1 -0
- package/dist/timebox/timebox-discover.js +87 -0
- package/dist/timebox/timebox-discover.js.map +1 -0
- package/dist/timebox/timebox-settings.d.ts +32 -0
- package/dist/timebox/timebox-settings.d.ts.map +1 -0
- package/dist/timebox/timebox-settings.js +80 -0
- package/dist/timebox/timebox-settings.js.map +1 -0
- package/dist/timeline-store.d.ts +112 -3
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +460 -14
- package/dist/timeline-store.js.map +1 -1
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +29 -5
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- package/dist/tui/ansi.d.ts +0 -4
- package/dist/tui/ansi.d.ts.map +1 -1
- package/dist/tui/ansi.js +14 -7
- package/dist/tui/ansi.js.map +1 -1
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +38 -11
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/renderer.d.ts +36 -0
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +355 -121
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +44 -51
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +7 -1
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +10 -3
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +98 -13
- package/dist/usage-event.js.map +1 -1
- package/dist/utility-proxy.d.ts.map +1 -1
- package/dist/utility-proxy.js +8 -0
- package/dist/utility-proxy.js.map +1 -1
- package/dist/utils/project-name.d.ts +3 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +44 -2
- package/dist/utils/project-name.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -17
- package/dist/version-check.js.map +1 -1
- package/dist/windows-service.d.ts +26 -0
- package/dist/windows-service.d.ts.map +1 -0
- package/dist/windows-service.js +145 -0
- package/dist/windows-service.js.map +1 -0
- package/dist/ws-server.d.ts +17 -3
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +78 -8
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +112 -0
- package/package.json +10 -4
- package/scripts/build-fm-helper.mjs +26 -0
- package/LICENSE +0 -21
- package/dist/apme/tuner.d.ts +0 -64
- package/dist/apme/tuner.d.ts.map +0 -1
- package/dist/apme/tuner.js +0 -297
- package/dist/apme/tuner.js.map +0 -1
package/dist/apme/collector.js
CHANGED
|
@@ -18,18 +18,38 @@ import { mkdirSync, writeFileSync } from 'fs';
|
|
|
18
18
|
import { homedir } from 'os';
|
|
19
19
|
import { debug } from '../logger.js';
|
|
20
20
|
import { resolveProjectName } from '../utils/project-name.js';
|
|
21
|
-
import {
|
|
21
|
+
import { priceUsd, providerFor } from '@agentdeck/shared';
|
|
22
|
+
import { classifyRunSmart, computeSignals, classify } from './classifier.js';
|
|
23
|
+
/** A turn_start with the same prompt landing on a fresh, still-empty turn
|
|
24
|
+
* within this window is treated as a transport echo (e.g. OpenClaw
|
|
25
|
+
* `chat.send` span + the gateway's `session.message` role=user re-delivery
|
|
26
|
+
* of the same text), not a new turn. */
|
|
27
|
+
const DUPLICATE_TURN_OPEN_WINDOW_MS = 15_000;
|
|
22
28
|
export class ApmeCollector {
|
|
23
29
|
store;
|
|
24
30
|
hwSampler;
|
|
25
31
|
sessionToRun = new Map(); // sessionId → runId
|
|
32
|
+
sessionToAgentType = new Map(); // sessionId → agentType (survives closeRun for late-attributed timeline rows)
|
|
26
33
|
sessionToTurn = new Map(); // sessionId → current turn
|
|
27
34
|
sessionToLastTurnId = new Map(); // survives closeTurn()
|
|
28
35
|
sessionToTask = new Map(); // sessionId → current task
|
|
29
36
|
runTaskCount = new Map(); // runId → next task_index
|
|
37
|
+
sessionToUsage = new Map(); // last cumulative usage
|
|
30
38
|
/** Optional listener fired after `closeTask` persists the row. The runner
|
|
31
39
|
* wires this to enqueue a task-level judge call. */
|
|
32
40
|
onTaskClosed = null;
|
|
41
|
+
/** Optional listener fired after `openTaskIfNone` inserts a fresh task row.
|
|
42
|
+
* The dashboard timeline emitter wires this to push a `task_start` entry. */
|
|
43
|
+
onTaskOpened = null;
|
|
44
|
+
/** Optional listener fired after a typed trajectory event is persisted.
|
|
45
|
+
* The timeline projection (Phase 3) consumes this. */
|
|
46
|
+
onSampleEvent = null;
|
|
47
|
+
/** Optional listener fired on the TodoWrite-all-completed soft hint.
|
|
48
|
+
* The timeline emitter wires this to push a `task_milestone` row. */
|
|
49
|
+
onTaskMilestone = null;
|
|
50
|
+
/** Last milestone key (`taskId:turnIndex`) per session — a turn can carry
|
|
51
|
+
* several all-completed TodoWrite calls; surface only the first. */
|
|
52
|
+
sessionToLastMilestone = new Map();
|
|
33
53
|
constructor(store, hwSampler) {
|
|
34
54
|
this.store = store;
|
|
35
55
|
this.hwSampler = hwSampler;
|
|
@@ -54,6 +74,7 @@ export class ApmeCollector {
|
|
|
54
74
|
try {
|
|
55
75
|
this.store.insertRun(row);
|
|
56
76
|
this.sessionToRun.set(input.sessionId, runId);
|
|
77
|
+
this.sessionToAgentType.set(input.sessionId, input.agentType);
|
|
57
78
|
debug('APME', `openRun ${runId} session=${input.sessionId} agent=${input.agentType} model=${input.modelId ?? '-'}`);
|
|
58
79
|
}
|
|
59
80
|
catch (err) {
|
|
@@ -61,13 +82,23 @@ export class ApmeCollector {
|
|
|
61
82
|
}
|
|
62
83
|
return runId;
|
|
63
84
|
}
|
|
64
|
-
/** Record a hook event as a step + manage turn lifecycle.
|
|
65
|
-
|
|
85
|
+
/** Record a hook event as a step + manage turn lifecycle.
|
|
86
|
+
*
|
|
87
|
+
* `event` is normalized up-front so callers may pass EITHER the PascalCase
|
|
88
|
+
* hook name (`UserPromptSubmit`, managed session bridge + span ingest) OR
|
|
89
|
+
* the snake_case name the daemon's `/hooks/` handler maps to
|
|
90
|
+
* (`user_prompt_submit`, `tool_start`, `tool_end`). Before this, snake_case
|
|
91
|
+
* silently fell through every branch — the daemon's direct-`claude` hooks
|
|
92
|
+
* never opened a turn, so those sessions produced no tasks and the device
|
|
93
|
+
* timeline showed every prompt as a bare top-level chat. Making the matcher
|
|
94
|
+
* case-tolerant means no caller can quietly no-op the segmentation. */
|
|
95
|
+
ingestHook(sessionId, rawEvent, data) {
|
|
66
96
|
if (!this.store.enabled)
|
|
67
97
|
return;
|
|
68
98
|
const runId = this.sessionToRun.get(sessionId);
|
|
69
99
|
if (!runId)
|
|
70
100
|
return;
|
|
101
|
+
const event = normalizeHookEventName(rawEvent);
|
|
71
102
|
const toolName = typeof data.tool_name === 'string' ? data.tool_name : null;
|
|
72
103
|
// ── Turn management ──
|
|
73
104
|
if (event === 'UserPromptSubmit') {
|
|
@@ -77,8 +108,31 @@ export class ApmeCollector {
|
|
|
77
108
|
? data.message?.content
|
|
78
109
|
: undefined);
|
|
79
110
|
const prompt = typeof rawPrompt === 'string' ? rawPrompt.slice(0, 8_000) : null;
|
|
80
|
-
//
|
|
81
|
-
|
|
111
|
+
// Duplicate-open guard: one user prompt can reach the collector more
|
|
112
|
+
// than once within moments. Closing and reopening here would strand an
|
|
113
|
+
// empty phantom turn and shift every later turn_index, so an identical
|
|
114
|
+
// prompt landing on a fresh, still-empty turn is a no-op.
|
|
115
|
+
const openTurn = this.sessionToTurn.get(sessionId);
|
|
116
|
+
if (openTurn && prompt !== null && openTurn.prompt === prompt &&
|
|
117
|
+
openTurn.toolCalls === 0 && !openTurn.hasResponse &&
|
|
118
|
+
Date.now() - openTurn.startedAt < DUPLICATE_TURN_OPEN_WINDOW_MS) {
|
|
119
|
+
debug('APME', `duplicate turn_start ignored (echo) turn=${openTurn.id.slice(0, 8)}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Close previous turn if open. Resolve prevIndex carefully: the active
|
|
123
|
+
// turn may already have been closed by an explicit closeTurnForSession
|
|
124
|
+
// (Codex `codex_stop` hook), in which case sessionToTurn is empty. Fall
|
|
125
|
+
// back to the last closed turn's row in the store so subsequent turns
|
|
126
|
+
// keep monotonically increasing turn_index instead of resetting to 0.
|
|
127
|
+
let prevIndex = this.sessionToTurn.get(sessionId)?.index ?? -1;
|
|
128
|
+
if (prevIndex === -1) {
|
|
129
|
+
const lastTurnId = this.sessionToLastTurnId.get(sessionId);
|
|
130
|
+
if (lastTurnId) {
|
|
131
|
+
const lastIdx = this.store.getTurn(lastTurnId)?.turn_index;
|
|
132
|
+
if (typeof lastIdx === 'number')
|
|
133
|
+
prevIndex = lastIdx;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
82
136
|
this.closeTurn(sessionId);
|
|
83
137
|
// Open new turn
|
|
84
138
|
const turnIndex = prevIndex + 1;
|
|
@@ -90,6 +144,7 @@ export class ApmeCollector {
|
|
|
90
144
|
startedAt: Date.now(), toolCalls: 0,
|
|
91
145
|
filesModified: 0, filesCreated: 0,
|
|
92
146
|
gitBefore: readGitHead(projectPath),
|
|
147
|
+
prompt, hasResponse: false,
|
|
93
148
|
};
|
|
94
149
|
this.sessionToTurn.set(sessionId, turn);
|
|
95
150
|
// Ensure an active task exists so this turn can attach to it. Tasks group
|
|
@@ -97,9 +152,16 @@ export class ApmeCollector {
|
|
|
97
152
|
// /clear, session_end). First turn in a run opens task 0.
|
|
98
153
|
const task = this.openTaskIfNone(sessionId, runId);
|
|
99
154
|
if (task) {
|
|
100
|
-
|
|
155
|
+
const wasFirstTurn = task.firstTurnIndex === null;
|
|
156
|
+
if (wasFirstTurn)
|
|
101
157
|
task.firstTurnIndex = turnIndex;
|
|
102
158
|
task.lastTurnIndex = turnIndex;
|
|
159
|
+
// Second (or later) turn on the same task proves it's a real,
|
|
160
|
+
// multi-turn work unit → promote the deferred TASK header so the
|
|
161
|
+
// follow-up prompts render grouped under one row instead of as
|
|
162
|
+
// separate top-level chats. Single-turn Q&A never reaches here.
|
|
163
|
+
if (!wasFirstTurn)
|
|
164
|
+
this.emitDeferredTaskStartIfNeeded(sessionId);
|
|
103
165
|
}
|
|
104
166
|
try {
|
|
105
167
|
this.store.insertTurn({
|
|
@@ -111,6 +173,10 @@ export class ApmeCollector {
|
|
|
111
173
|
catch (err) {
|
|
112
174
|
debug('APME', `insertTurn failed: ${String(err)}`);
|
|
113
175
|
}
|
|
176
|
+
// Sample trajectory: the user message opens the turn's typed event log.
|
|
177
|
+
if (task) {
|
|
178
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex }, { kind: 'user_message', ts: turn.startedAt, dedupCore: hashCore(prompt ?? `turn${turnIndex}`), payloadObj: { text: prompt ?? '' } });
|
|
179
|
+
}
|
|
114
180
|
// Also set run's task_prompt from first prompt
|
|
115
181
|
try {
|
|
116
182
|
if (run && !run.taskPrompt && prompt) {
|
|
@@ -127,16 +193,70 @@ export class ApmeCollector {
|
|
|
127
193
|
activeTurn.filesModified++;
|
|
128
194
|
if (toolName === 'Write')
|
|
129
195
|
activeTurn.filesCreated++;
|
|
196
|
+
// Sample trajectory: a tool call starts as a pending ToolEvent; its
|
|
197
|
+
// PostToolUse result resolves the SAME row (one row, not two).
|
|
198
|
+
const task = this.sessionToTask.get(sessionId);
|
|
199
|
+
if (task && toolName) {
|
|
200
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex: activeTurn.index }, {
|
|
201
|
+
kind: 'tool', toolName, toolStatus: 'pending',
|
|
202
|
+
dedupCore: `${toolName}:${activeTurn.toolCalls}`,
|
|
203
|
+
payloadObj: { input: extractToolInput(data) },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
130
206
|
}
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
207
|
+
// Sample trajectory: resolve the pending ToolEvent on PostToolUse / tool_result.
|
|
208
|
+
if ((event === 'PostToolUse' || event === 'tool_result') && toolName) {
|
|
209
|
+
const task = this.sessionToTask.get(sessionId);
|
|
210
|
+
const turnIndex = this.sessionToTurn.get(sessionId)?.index;
|
|
211
|
+
if (task && turnIndex !== undefined) {
|
|
212
|
+
const isError = Boolean(data.is_error || data.error);
|
|
213
|
+
const output = extractToolOutput(data);
|
|
214
|
+
const pending = this.store.findPendingToolEvent(task.id, turnIndex, toolName);
|
|
215
|
+
if (pending?.id != null) {
|
|
216
|
+
let payload = pending.payload;
|
|
217
|
+
try {
|
|
218
|
+
const obj = pending.payload ? JSON.parse(pending.payload) : {};
|
|
219
|
+
if (output !== undefined)
|
|
220
|
+
obj.output = output;
|
|
221
|
+
payload = safeStringify(obj);
|
|
222
|
+
}
|
|
223
|
+
catch { /* keep existing payload */ }
|
|
224
|
+
this.store.updateSampleEvent(pending.id, {
|
|
225
|
+
toolStatus: isError ? 'error' : 'success',
|
|
226
|
+
toolError: isError ? String(data.error ?? 'error').slice(0, 500) : null,
|
|
227
|
+
payload,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// No pending row (PostToolUse without a matching PreToolUse) — record
|
|
232
|
+
// a resolved tool event directly.
|
|
233
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex }, {
|
|
234
|
+
kind: 'tool', toolName,
|
|
235
|
+
toolStatus: isError ? 'error' : 'success',
|
|
236
|
+
toolError: isError ? String(data.error ?? 'error').slice(0, 500) : null,
|
|
237
|
+
dedupCore: `${toolName}:resolved:${turnIndex}:${this.store.nextSampleSeq(task.id)}`,
|
|
238
|
+
payloadObj: { output },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ── Task boundary HINT: TodoWrite all-completed ──
|
|
244
|
+
// Demoted from a hard boundary to a non-segmenting hint (2026-06).
|
|
245
|
+
// TodoWrite-all-complete fires unreliably (~18% on Claude Code v2.1) and,
|
|
246
|
+
// when it did fire, fragmented a single logical task into several units.
|
|
247
|
+
// Tasks now segment only on EXPLICIT boundaries (`/task close`, `/clear`)
|
|
248
|
+
// or `session_end` — a stable, user-controlled unit. We still record the
|
|
249
|
+
// milestone in the trajectory so the task rollup can see that the agent
|
|
250
|
+
// declared its todos done, without splitting the task.
|
|
136
251
|
if (event === 'PostToolUse' && toolName === 'TodoWrite') {
|
|
137
252
|
const todos = extractTodos(data);
|
|
138
253
|
if (todos && todos.length > 0 && todos.every((t) => t.status === 'completed')) {
|
|
139
|
-
this.
|
|
254
|
+
const task = this.sessionToTask.get(sessionId);
|
|
255
|
+
const turnIndex = this.sessionToTurn.get(sessionId)?.index;
|
|
256
|
+
if (task && turnIndex !== undefined) {
|
|
257
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex }, { kind: 'state', dedupCore: `todos_complete:${turnIndex}:${todos.length}`, payloadObj: { state: 'todos_completed', count: todos.length } });
|
|
258
|
+
this.fireTaskMilestone(sessionId, task.id, runId, turnIndex, todos.length);
|
|
259
|
+
}
|
|
140
260
|
}
|
|
141
261
|
}
|
|
142
262
|
// Record step
|
|
@@ -150,6 +270,14 @@ export class ApmeCollector {
|
|
|
150
270
|
debug('APME', `ingestHook failed: ${String(err)}`);
|
|
151
271
|
}
|
|
152
272
|
}
|
|
273
|
+
/** Public wrapper for the private `closeTurn`. Used by adapters that
|
|
274
|
+
* see an explicit turn-end signal (Codex `codex_stop` hook) and want
|
|
275
|
+
* to finalize the turn row immediately rather than wait for the next
|
|
276
|
+
* UserPromptSubmit / closeRun to flush endedAt + buffered counters.
|
|
277
|
+
* Idempotent — no-op when no active turn for the session. */
|
|
278
|
+
closeTurnForSession(sessionId) {
|
|
279
|
+
this.closeTurn(sessionId);
|
|
280
|
+
}
|
|
153
281
|
/** Close the current turn for a session (called on new prompt or session end). */
|
|
154
282
|
closeTurn(sessionId) {
|
|
155
283
|
const turn = this.sessionToTurn.get(sessionId);
|
|
@@ -182,9 +310,48 @@ export class ApmeCollector {
|
|
|
182
310
|
getActiveTaskId(sessionId) {
|
|
183
311
|
return this.sessionToTask.get(sessionId)?.id ?? null;
|
|
184
312
|
}
|
|
313
|
+
/** Fire the `onTaskMilestone` listener for a TodoWrite-all-completed hint,
|
|
314
|
+
* at most once per (task, turn) — the agent may rewrite an all-completed
|
|
315
|
+
* todo list several times inside one turn. */
|
|
316
|
+
fireTaskMilestone(sessionId, taskId, runId, turnIndex, todoCount) {
|
|
317
|
+
if (!this.onTaskMilestone)
|
|
318
|
+
return;
|
|
319
|
+
const key = `${taskId}:${turnIndex}`;
|
|
320
|
+
if (this.sessionToLastMilestone.get(sessionId) === key)
|
|
321
|
+
return;
|
|
322
|
+
this.sessionToLastMilestone.set(sessionId, key);
|
|
323
|
+
// A TodoWrite plan implies a task worth showing — promote the deferred
|
|
324
|
+
// task_start first so the milestone row never renders orphaned above a
|
|
325
|
+
// task with no header.
|
|
326
|
+
this.emitDeferredTaskStartIfNeeded(sessionId);
|
|
327
|
+
const run = this.store.getRun(runId);
|
|
328
|
+
try {
|
|
329
|
+
this.onTaskMilestone({
|
|
330
|
+
taskId,
|
|
331
|
+
runId,
|
|
332
|
+
sessionId,
|
|
333
|
+
agentType: (run?.agentType ?? null),
|
|
334
|
+
projectName: run?.projectName ?? null,
|
|
335
|
+
turnIndex,
|
|
336
|
+
todoCount,
|
|
337
|
+
at: Date.now(),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
debug('APME', `onTaskMilestone listener threw: ${String(err)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
185
344
|
/** Open a new task if none is active for this session. Returns the active
|
|
186
345
|
* task (new or existing), or null if no run is open. Idempotent: repeat
|
|
187
|
-
* calls while a task is already active are no-ops.
|
|
346
|
+
* calls while a task is already active are no-ops.
|
|
347
|
+
*
|
|
348
|
+
* The `task_start` timeline row is NOT emitted here — it is deferred to
|
|
349
|
+
* `emitDeferredTaskStartIfNeeded`, fired when a "real task" signal lands
|
|
350
|
+
* (a second turn on the same task, or a TodoWrite plan). Short single-turn
|
|
351
|
+
* conversations therefore never produce a TASK header on the dashboard,
|
|
352
|
+
* keeping the timeline focused on the turn rows worth evaluating. The DB
|
|
353
|
+
* `tasks` row is inserted immediately so eval/analytics see every unit.
|
|
354
|
+
* Mirrors ApmeCollector.swift `openTaskIfNone` + `emitDeferredTaskStartIfNeeded`. */
|
|
188
355
|
openTaskIfNone(sessionId, runId) {
|
|
189
356
|
const existing = this.sessionToTask.get(sessionId);
|
|
190
357
|
if (existing)
|
|
@@ -198,6 +365,7 @@ export class ApmeCollector {
|
|
|
198
365
|
startedAt: Date.now(),
|
|
199
366
|
firstTurnIndex: null,
|
|
200
367
|
lastTurnIndex: null,
|
|
368
|
+
timelineEmitted: false,
|
|
201
369
|
};
|
|
202
370
|
this.sessionToTask.set(sessionId, task);
|
|
203
371
|
try {
|
|
@@ -214,6 +382,57 @@ export class ApmeCollector {
|
|
|
214
382
|
}
|
|
215
383
|
return task;
|
|
216
384
|
}
|
|
385
|
+
/** Emit the deferred `task_start` timeline row for the session's active task,
|
|
386
|
+
* once. Idempotent — repeat calls after the first are no-ops. Uses the
|
|
387
|
+
* task's original `startedAt` as the row ts so the TASK header anchors above
|
|
388
|
+
* the first turn it groups instead of jumping in mid-conversation. Called
|
|
389
|
+
* when a task proves itself "real" (second turn, or TodoWrite plan).
|
|
390
|
+
* Mirrors ApmeCollector.swift `emitDeferredTaskStartIfNeeded`. */
|
|
391
|
+
emitDeferredTaskStartIfNeeded(sessionId) {
|
|
392
|
+
const task = this.sessionToTask.get(sessionId);
|
|
393
|
+
if (!task || task.timelineEmitted)
|
|
394
|
+
return;
|
|
395
|
+
task.timelineEmitted = true;
|
|
396
|
+
if (!this.onTaskOpened)
|
|
397
|
+
return;
|
|
398
|
+
const run = this.store.getRun(task.runId);
|
|
399
|
+
try {
|
|
400
|
+
this.onTaskOpened({
|
|
401
|
+
taskId: task.id,
|
|
402
|
+
runId: task.runId,
|
|
403
|
+
sessionId,
|
|
404
|
+
agentType: (run?.agentType ?? null),
|
|
405
|
+
projectName: run?.projectName ?? null,
|
|
406
|
+
taskIndex: task.index,
|
|
407
|
+
startedAt: task.startedAt,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
debug('APME', `onTaskOpened listener threw: ${String(err)}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** Public wrapper for closeTask. Used by the manual-boundary CLI /
|
|
415
|
+
* HTTP route + the macOS detail-pane button — lets the user declare
|
|
416
|
+
* "this task is done" without `/clear` (which would also split the
|
|
417
|
+
* run). No-op when no task is active. Passing `outcome` overrides the
|
|
418
|
+
* judge's coarse score-derived class — handy for `task cancel` where
|
|
419
|
+
* the user wants the row tagged "abandoned" regardless of partial
|
|
420
|
+
* progress. */
|
|
421
|
+
closeTaskExternal(sessionId, boundarySignal = 'manual', outcome) {
|
|
422
|
+
const task = this.sessionToTask.get(sessionId);
|
|
423
|
+
if (!task)
|
|
424
|
+
return false;
|
|
425
|
+
this.closeTask(sessionId, boundarySignal);
|
|
426
|
+
if (outcome) {
|
|
427
|
+
try {
|
|
428
|
+
this.store.updateTask(task.id, { outcome });
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
debug('APME', `manual outcome write failed: ${String(err)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
217
436
|
/** Close the current task for a session, persisting boundary metadata.
|
|
218
437
|
* No-op if no task is active. Fires `onTaskClosed` so the runner can
|
|
219
438
|
* enqueue a task-level judge call. Tasks that never saw a turn
|
|
@@ -223,6 +442,7 @@ export class ApmeCollector {
|
|
|
223
442
|
if (!task)
|
|
224
443
|
return;
|
|
225
444
|
this.sessionToTask.delete(sessionId);
|
|
445
|
+
this.sessionToLastMilestone.delete(sessionId);
|
|
226
446
|
// Empty task: no turns ever attached. Drop the row so the dashboard
|
|
227
447
|
// doesn't show phantom entries from back-to-back boundary signals.
|
|
228
448
|
if (task.firstTurnIndex === null) {
|
|
@@ -237,10 +457,31 @@ export class ApmeCollector {
|
|
|
237
457
|
debug('APME', `closeTask ${task.id.slice(0, 8)} — empty, dropped`);
|
|
238
458
|
return;
|
|
239
459
|
}
|
|
240
|
-
//
|
|
241
|
-
//
|
|
460
|
+
// Category, present-at-close. Prefer the run's already-resolved category;
|
|
461
|
+
// otherwise classify synchronously from the run's signals so the task row
|
|
462
|
+
// (and its rollup judge rubric) always carries a stable category. The
|
|
463
|
+
// async run-level classifier (classifyRunSmart at closeRun) frequently
|
|
464
|
+
// resolves AFTER the task has already closed — leaving taskCategory null
|
|
465
|
+
// and the judge falling back to the wrong generic rubric.
|
|
242
466
|
const run = this.store.getRun(task.runId);
|
|
243
|
-
|
|
467
|
+
let taskCategory = run?.taskCategory ?? null;
|
|
468
|
+
if (!taskCategory || taskCategory === 'unknown') {
|
|
469
|
+
try {
|
|
470
|
+
const signals = computeSignals(this.store, task.runId);
|
|
471
|
+
// run.endedAt is still null at session_end close (updateRun runs after
|
|
472
|
+
// closeTask), so sessionDurationSec would be 0 and skew the duration
|
|
473
|
+
// rules — derive it from the task span instead.
|
|
474
|
+
if (signals.sessionDurationSec === 0) {
|
|
475
|
+
signals.sessionDurationSec = Math.max(0, Math.round((Date.now() - task.startedAt) / 1000));
|
|
476
|
+
}
|
|
477
|
+
const category = classify(signals);
|
|
478
|
+
if (category && category !== 'unknown')
|
|
479
|
+
taskCategory = category;
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
debug('APME', `closeTask classify failed: ${String(err)}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
244
485
|
try {
|
|
245
486
|
this.store.updateTask(task.id, {
|
|
246
487
|
endedAt: Date.now(),
|
|
@@ -253,14 +494,21 @@ export class ApmeCollector {
|
|
|
253
494
|
debug('APME', `updateTask failed: ${String(err)}`);
|
|
254
495
|
}
|
|
255
496
|
debug('APME', `closeTask ${task.id.slice(0, 8)} signal=${boundarySignal} turns=${task.firstTurnIndex}..${task.lastTurnIndex}`);
|
|
256
|
-
// Notify listeners (runner enqueueTask
|
|
497
|
+
// Notify listeners (runner enqueueTask + timeline emitter wired via apme/index.ts).
|
|
257
498
|
if (this.onTaskClosed) {
|
|
258
499
|
try {
|
|
259
500
|
this.onTaskClosed({
|
|
260
501
|
taskId: task.id,
|
|
261
502
|
runId: task.runId,
|
|
503
|
+
sessionId,
|
|
504
|
+
agentType: (run?.agentType ?? null),
|
|
505
|
+
projectName: run?.projectName ?? null,
|
|
506
|
+
startedAt: task.startedAt,
|
|
507
|
+
endedAt: Date.now(),
|
|
262
508
|
boundarySignal,
|
|
263
509
|
taskCategory,
|
|
510
|
+
turns: (task.lastTurnIndex ?? task.firstTurnIndex ?? 0) - (task.firstTurnIndex ?? 0) + 1,
|
|
511
|
+
timelineEmitted: task.timelineEmitted,
|
|
264
512
|
});
|
|
265
513
|
}
|
|
266
514
|
catch (err) {
|
|
@@ -272,6 +520,92 @@ export class ApmeCollector {
|
|
|
272
520
|
getRunId(sessionId) {
|
|
273
521
|
return this.sessionToRun.get(sessionId) ?? null;
|
|
274
522
|
}
|
|
523
|
+
/** Agent type established for a session at `openRun` (survives closeRun so a
|
|
524
|
+
* late-attributed timeline row still resolves its brand). This is the single
|
|
525
|
+
* authoritative agentType source the timeline attributor backfills from, so
|
|
526
|
+
* every timeline entry ships with an agentType regardless of which emitter
|
|
527
|
+
* produced it (observed hook `chat_start`, relayed managed `tool_exec`, …). */
|
|
528
|
+
getRunAgentType(sessionId) {
|
|
529
|
+
return this.sessionToAgentType.get(sessionId) ?? null;
|
|
530
|
+
}
|
|
531
|
+
// ─── SessionSample trajectory (the normalizer's typed event log) ────────────
|
|
532
|
+
/** Resolve the (taskId, runId, turnIndex) a sample event should attach to.
|
|
533
|
+
* Prefers the active task/turn; falls back to a turn row in the DB when the
|
|
534
|
+
* task already closed (e.g. PTY response captured after session_end). */
|
|
535
|
+
sampleCtxForTurn(sessionId, turnId) {
|
|
536
|
+
const active = this.sessionToTask.get(sessionId);
|
|
537
|
+
if (turnId) {
|
|
538
|
+
const row = this.store.getTurn(turnId);
|
|
539
|
+
const taskId = row?.task_id ?? active?.id;
|
|
540
|
+
const runId = row?.run_id ?? active?.runId;
|
|
541
|
+
const turnIndex = row?.turn_index
|
|
542
|
+
?? this.sessionToTurn.get(sessionId)?.index ?? active?.lastTurnIndex ?? 0;
|
|
543
|
+
if (taskId && runId)
|
|
544
|
+
return { taskId, runId, turnIndex };
|
|
545
|
+
}
|
|
546
|
+
if (active) {
|
|
547
|
+
const turnIndex = this.sessionToTurn.get(sessionId)?.index ?? active.lastTurnIndex ?? 0;
|
|
548
|
+
return { taskId: active.id, runId: active.runId, turnIndex };
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
/** Append one typed trajectory event to the active sample. Storage-time dedup
|
|
553
|
+
* is handled by the UNIQUE (task_id, dedup_key) index. Fires `onSampleEvent`
|
|
554
|
+
* only when a row was actually inserted (not a dup), so the timeline
|
|
555
|
+
* projection never double-emits. */
|
|
556
|
+
appendSampleEvent(ctx, ev) {
|
|
557
|
+
if (!this.store.enabled)
|
|
558
|
+
return;
|
|
559
|
+
const dedupKey = createHash('sha1').update(`${ev.kind}|${ctx.turnIndex}|${ev.dedupCore}`).digest('hex').slice(0, 24);
|
|
560
|
+
const row = {
|
|
561
|
+
taskId: ctx.taskId,
|
|
562
|
+
runId: ctx.runId,
|
|
563
|
+
turnIndex: ctx.turnIndex,
|
|
564
|
+
seq: this.store.nextSampleSeq(ctx.taskId),
|
|
565
|
+
ts: ev.ts ?? Date.now(),
|
|
566
|
+
kind: ev.kind,
|
|
567
|
+
model: ev.model ?? null,
|
|
568
|
+
inputTokens: ev.inputTokens ?? null,
|
|
569
|
+
outputTokens: ev.outputTokens ?? null,
|
|
570
|
+
costUsd: ev.costUsd ?? null,
|
|
571
|
+
latencyMs: ev.latencyMs ?? null,
|
|
572
|
+
toolName: ev.toolName ?? null,
|
|
573
|
+
toolStatus: ev.toolStatus ?? null,
|
|
574
|
+
toolError: ev.toolError ?? null,
|
|
575
|
+
payload: ev.payloadObj ? safeStringify(ev.payloadObj) : null,
|
|
576
|
+
dedupKey,
|
|
577
|
+
};
|
|
578
|
+
let inserted = false;
|
|
579
|
+
try {
|
|
580
|
+
inserted = this.store.insertSampleEvent(row);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
debug('APME', `appendSampleEvent failed: ${String(err)}`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (inserted && this.onSampleEvent) {
|
|
587
|
+
const run = this.store.getRun(ctx.runId);
|
|
588
|
+
try {
|
|
589
|
+
this.onSampleEvent({
|
|
590
|
+
taskId: ctx.taskId, runId: ctx.runId,
|
|
591
|
+
sessionId: this.runToSessionId(ctx.runId),
|
|
592
|
+
agentType: (run?.agentType ?? null),
|
|
593
|
+
projectName: run?.projectName ?? null,
|
|
594
|
+
event: row,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
debug('APME', `onSampleEvent listener threw: ${String(err)}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Reverse-lookup sessionId for a runId (best-effort; only the live map). */
|
|
603
|
+
runToSessionId(runId) {
|
|
604
|
+
for (const [sid, rid] of this.sessionToRun)
|
|
605
|
+
if (rid === runId)
|
|
606
|
+
return sid;
|
|
607
|
+
return '';
|
|
608
|
+
}
|
|
275
609
|
/** Store Claude's response text on the current turn.
|
|
276
610
|
* Falls back to the last closed turn if closeTurn() already ran (race with session exit).
|
|
277
611
|
* Tags turns.efficiency_json.response_kind so the runner can skip tool-only / empty
|
|
@@ -298,10 +632,21 @@ export class ApmeCollector {
|
|
|
298
632
|
response: response.slice(0, 10_000),
|
|
299
633
|
efficiencyJson,
|
|
300
634
|
});
|
|
635
|
+
if (turn)
|
|
636
|
+
turn.hasResponse = true;
|
|
301
637
|
}
|
|
302
638
|
catch (err) {
|
|
303
639
|
debug('APME', `setTurnResponse failed: ${String(err)}`);
|
|
304
640
|
}
|
|
641
|
+
// Sample trajectory: the assistant response closes the turn's event arc.
|
|
642
|
+
const ctx = this.sampleCtxForTurn(sessionId, turnId);
|
|
643
|
+
if (ctx) {
|
|
644
|
+
this.appendSampleEvent(ctx, {
|
|
645
|
+
kind: 'assistant_message',
|
|
646
|
+
dedupCore: hashCore(response.slice(0, 400)),
|
|
647
|
+
payloadObj: { text: response.slice(0, 10_000), responseKind: kind },
|
|
648
|
+
});
|
|
649
|
+
}
|
|
305
650
|
}
|
|
306
651
|
/** Apply response to the last closed turn if it has no response yet.
|
|
307
652
|
* Used as fallback when Stop hook doesn't fire (PTY output capture). */
|
|
@@ -327,6 +672,14 @@ export class ApmeCollector {
|
|
|
327
672
|
});
|
|
328
673
|
}
|
|
329
674
|
catch { /* ignore */ }
|
|
675
|
+
const ctx = this.sampleCtxForTurn(sessionId, turnId);
|
|
676
|
+
if (ctx) {
|
|
677
|
+
this.appendSampleEvent(ctx, {
|
|
678
|
+
kind: 'assistant_message',
|
|
679
|
+
dedupCore: hashCore(response.slice(0, 400)),
|
|
680
|
+
payloadObj: { text: response.slice(0, 10_000), responseKind: kind },
|
|
681
|
+
});
|
|
682
|
+
}
|
|
330
683
|
}
|
|
331
684
|
/** Single-entrypoint ingestion using the shared TelemetrySpan envelope.
|
|
332
685
|
*
|
|
@@ -381,11 +734,27 @@ export class ApmeCollector {
|
|
|
381
734
|
this.splitRun(sessionId, a['agentdeck.cwd']);
|
|
382
735
|
return;
|
|
383
736
|
}
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
737
|
+
// Adapter-emitted boundaries (OpenClaw chat.aborted → 'manual',
|
|
738
|
+
// OpenClaw idle-gap timer → 'idle_gap') close the active task.
|
|
739
|
+
// `session_end` is intentionally excluded: closeRun fires that
|
|
740
|
+
// path itself, and a duplicate here would double-emit onTaskClosed.
|
|
741
|
+
if (signal === 'manual' || signal === 'idle_gap') {
|
|
742
|
+
this.closeTask(sessionId, signal);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// `todo_complete` is a soft hint, not a boundary (see ingestHook
|
|
746
|
+
// TodoWrite handling) — record it in the trajectory without splitting
|
|
747
|
+
// the task. OpenCode's TodoWrite-all-completed routes here.
|
|
748
|
+
if (signal === 'todo_complete') {
|
|
749
|
+
const task = this.sessionToTask.get(sessionId);
|
|
750
|
+
const turnIndex = this.sessionToTurn.get(sessionId)?.index;
|
|
751
|
+
if (task && turnIndex !== undefined) {
|
|
752
|
+
this.appendSampleEvent({ taskId: task.id, runId: task.runId, turnIndex }, { kind: 'state', dedupCore: `todos_complete:${turnIndex}`, payloadObj: { state: 'todos_completed' } });
|
|
753
|
+
this.fireTaskMilestone(sessionId, task.id, task.runId, turnIndex, null);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
debug('APME', `task_boundary span dropped: unknown signal=${signal ?? '<none>'}`);
|
|
389
758
|
return;
|
|
390
759
|
}
|
|
391
760
|
case 'session_meta': {
|
|
@@ -429,7 +798,10 @@ export class ApmeCollector {
|
|
|
429
798
|
}
|
|
430
799
|
catch { /* ignore */ }
|
|
431
800
|
}
|
|
432
|
-
/** Update token / cost columns from the bridge's UsageTracker snapshot.
|
|
801
|
+
/** Update token / cost columns from the bridge's UsageTracker snapshot.
|
|
802
|
+
* Snapshots carry CUMULATIVE session totals, so we emit a priced ModelEvent
|
|
803
|
+
* for the delta and attribute it to the active task (the SessionSample cost
|
|
804
|
+
* is the sum of its ModelEvents). */
|
|
433
805
|
updateUsage(sessionId, snapshot) {
|
|
434
806
|
if (!this.store.enabled)
|
|
435
807
|
return;
|
|
@@ -444,6 +816,35 @@ export class ApmeCollector {
|
|
|
444
816
|
});
|
|
445
817
|
}
|
|
446
818
|
catch { /* ignore */ }
|
|
819
|
+
// ── Per-task ModelEvent from the cumulative delta ──
|
|
820
|
+
const curIn = snapshot.inputTokens ?? 0;
|
|
821
|
+
const curOut = snapshot.outputTokens ?? 0;
|
|
822
|
+
const prev = this.sessionToUsage.get(sessionId) ?? { in: 0, out: 0 };
|
|
823
|
+
const dIn = Math.max(0, curIn - prev.in);
|
|
824
|
+
const dOut = Math.max(0, curOut - prev.out);
|
|
825
|
+
this.sessionToUsage.set(sessionId, { in: curIn, out: curOut });
|
|
826
|
+
if (dIn === 0 && dOut === 0)
|
|
827
|
+
return;
|
|
828
|
+
const task = this.sessionToTask.get(sessionId);
|
|
829
|
+
if (!task)
|
|
830
|
+
return;
|
|
831
|
+
const run = this.store.getRun(runId);
|
|
832
|
+
const model = run?.modelId ?? null;
|
|
833
|
+
const turnIndex = this.sessionToTurn.get(sessionId)?.index ?? task.lastTurnIndex ?? 0;
|
|
834
|
+
// Prefer the agent-reported marginal cost when available, else price the delta.
|
|
835
|
+
const cost = priceUsd(model, dIn, dOut);
|
|
836
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex }, {
|
|
837
|
+
kind: 'model', model: model ?? undefined, inputTokens: dIn, outputTokens: dOut,
|
|
838
|
+
costUsd: cost, latencyMs: 0, dedupCore: `${curIn}:${curOut}`,
|
|
839
|
+
});
|
|
840
|
+
try {
|
|
841
|
+
this.store.updateTask(task.id, {
|
|
842
|
+
modelId: model ?? undefined,
|
|
843
|
+
modelConfig: JSON.stringify({ modelId: model ?? 'unknown', provider: providerFor(model) }),
|
|
844
|
+
});
|
|
845
|
+
this.store.recomputeSampleCost(task.id);
|
|
846
|
+
}
|
|
847
|
+
catch { /* ignore */ }
|
|
447
848
|
}
|
|
448
849
|
/** Split the current run — closes the active run and opens a fresh one.
|
|
449
850
|
* Triggered on `/clear` or other context-reset events so each logical
|
|
@@ -500,6 +901,7 @@ export class ApmeCollector {
|
|
|
500
901
|
this.sessionToRun.delete(sessionId);
|
|
501
902
|
this.sessionToLastTurnId.delete(sessionId);
|
|
502
903
|
this.runTaskCount.delete(runId);
|
|
904
|
+
this.sessionToUsage.delete(sessionId);
|
|
503
905
|
// Mark empty runs so the dashboard can filter them out.
|
|
504
906
|
// Don't delete — FK constraints and concurrent access make deletion risky.
|
|
505
907
|
const run = this.store.getRun(runId);
|
|
@@ -593,6 +995,21 @@ export class ApmeCollector {
|
|
|
593
995
|
/** Extract the todos array from a TodoWrite PostToolUse payload. Returns null
|
|
594
996
|
* if the shape doesn't match (payload malformed, older CC versions). Accepts
|
|
595
997
|
* `tool_input.todos` (hook standard) and `todos` (legacy flat shape). */
|
|
998
|
+
/** Canonicalize a hook event name to the PascalCase form `ingestHook` matches
|
|
999
|
+
* on. Accepts the daemon's snake_case (`user_prompt_submit`, `tool_start`,
|
|
1000
|
+
* `tool_end`, `pre/post_tool_use`) and passes PascalCase / span aliases
|
|
1001
|
+
* (`tool_result`) through unchanged. Keeps segmentation working regardless of
|
|
1002
|
+
* which surface (managed bridge, daemon `/hooks/`, OTel span) drives it. */
|
|
1003
|
+
function normalizeHookEventName(event) {
|
|
1004
|
+
switch (event) {
|
|
1005
|
+
case 'user_prompt_submit': return 'UserPromptSubmit';
|
|
1006
|
+
case 'tool_start':
|
|
1007
|
+
case 'pre_tool_use': return 'PreToolUse';
|
|
1008
|
+
case 'tool_end':
|
|
1009
|
+
case 'post_tool_use': return 'PostToolUse';
|
|
1010
|
+
default: return event;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
596
1013
|
function extractTodos(data) {
|
|
597
1014
|
const fromToolInput = data.tool_input?.todos;
|
|
598
1015
|
const fromFlat = data.todos;
|
|
@@ -615,6 +1032,37 @@ function extractTodos(data) {
|
|
|
615
1032
|
}
|
|
616
1033
|
return items;
|
|
617
1034
|
}
|
|
1035
|
+
/** Short stable hash for sample-event dedup keys (content-derived). */
|
|
1036
|
+
function hashCore(s) {
|
|
1037
|
+
return createHash('sha1').update(s).digest('hex').slice(0, 16);
|
|
1038
|
+
}
|
|
1039
|
+
/** Best-effort tool-input extraction from a hook/span payload. Claude Code
|
|
1040
|
+
* hooks carry `tool_input`; spans pass the raw payload directly. Capped to
|
|
1041
|
+
* keep the trajectory row small. */
|
|
1042
|
+
function extractToolInput(data) {
|
|
1043
|
+
const ti = data.tool_input ?? data.input ?? null;
|
|
1044
|
+
return clampPayload(ti);
|
|
1045
|
+
}
|
|
1046
|
+
/** Best-effort tool-output extraction. Claude Code PostToolUse carries
|
|
1047
|
+
* `tool_response`; other sources use `output` / `result`. */
|
|
1048
|
+
function extractToolOutput(data) {
|
|
1049
|
+
const out = data.tool_response ?? data.output ?? data.result;
|
|
1050
|
+
return out === undefined ? undefined : clampPayload(out);
|
|
1051
|
+
}
|
|
1052
|
+
/** Trim large tool payloads so a single event row stays bounded (≤4KB JSON). */
|
|
1053
|
+
function clampPayload(v) {
|
|
1054
|
+
if (v == null)
|
|
1055
|
+
return v;
|
|
1056
|
+
try {
|
|
1057
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
1058
|
+
if (s.length <= 4_000)
|
|
1059
|
+
return v;
|
|
1060
|
+
return (typeof v === 'string' ? v : s).slice(0, 4_000) + '…';
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
618
1066
|
function readGitHead(cwd) {
|
|
619
1067
|
if (!cwd)
|
|
620
1068
|
return null;
|