pi-crew 0.9.53 → 0.9.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/package.json +6 -3
- package/scripts/clean-strip-types.mjs +60 -0
- package/src/adapters/claude-adapter.ts +23 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +143 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +194 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -0
- package/src/agents/discover-agents.ts +623 -0
- package/src/benchmark/benchmark-runner.ts +313 -0
- package/src/benchmark/feedback-loop.ts +73 -0
- package/src/config/config.ts +1270 -0
- package/src/config/defaults.ts +193 -0
- package/src/config/drift-detector.ts +290 -0
- package/src/config/markers.ts +330 -0
- package/src/config/resilient-parser.ts +117 -0
- package/src/config/role-tools.ts +118 -0
- package/src/config/suggestions.ts +75 -0
- package/src/config/types.ts +280 -0
- package/src/errors.ts +194 -0
- package/src/extension/action-suggestions.ts +117 -0
- package/src/extension/async-notifier.ts +179 -0
- package/src/extension/autonomous-policy.ts +209 -0
- package/src/extension/command-completions.ts +120 -0
- package/src/extension/context-status-injection.ts +185 -0
- package/src/extension/crew-autocomplete.ts +131 -0
- package/src/extension/crew-cleanup.ts +168 -0
- package/src/extension/crew-input-router.ts +93 -0
- package/src/extension/crew-shortcuts.ts +87 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +101 -0
- package/src/extension/crew-vibes/font-detect.ts +71 -0
- package/src/extension/crew-vibes/footer.ts +292 -0
- package/src/extension/crew-vibes/index.ts +432 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/cross-extension-rpc.ts +350 -0
- package/src/extension/help.ts +62 -0
- package/src/extension/import-index.ts +74 -0
- package/src/extension/knowledge-injection.ts +402 -0
- package/src/extension/management.ts +571 -0
- package/src/extension/message-renderers.ts +108 -0
- package/src/extension/notification-router.ts +152 -0
- package/src/extension/notification-sink.ts +54 -0
- package/src/extension/pi-api.ts +56 -0
- package/src/extension/plan-orchestrate.ts +302 -0
- package/src/extension/project-init.ts +170 -0
- package/src/extension/register.ts +113 -0
- package/src/extension/registration/artifact-cleanup.ts +20 -0
- package/src/extension/registration/command-registration.ts +58 -0
- package/src/extension/registration/command-utils.ts +58 -0
- package/src/extension/registration/commands.ts +1228 -0
- package/src/extension/registration/compaction-guard.ts +333 -0
- package/src/extension/registration/context-builder.ts +135 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +923 -0
- package/src/extension/registration/lifecycle.ts +259 -0
- package/src/extension/registration/observability.ts +322 -0
- package/src/extension/registration/registration-types.ts +158 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-helpers.ts +125 -0
- package/src/extension/registration/subagent-manager-setup.ts +330 -0
- package/src/extension/registration/subagent-tools.ts +456 -0
- package/src/extension/registration/team-tool.ts +225 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/ui.ts +172 -0
- package/src/extension/registration/viewers.ts +121 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/result-watcher.ts +139 -0
- package/src/extension/rpc-hmac.ts +256 -0
- package/src/extension/run-bundle-schema.ts +104 -0
- package/src/extension/run-export.ts +97 -0
- package/src/extension/run-import.ts +168 -0
- package/src/extension/run-index.ts +131 -0
- package/src/extension/run-maintenance.ts +190 -0
- package/src/extension/session-summary.ts +18 -0
- package/src/extension/team-manager-command.ts +153 -0
- package/src/extension/team-onboard.ts +180 -0
- package/src/extension/team-recommendation.ts +288 -0
- package/src/extension/team-tool/anchor.ts +172 -0
- package/src/extension/team-tool/api.ts +1244 -0
- package/src/extension/team-tool/auto-summarize.ts +142 -0
- package/src/extension/team-tool/cache-control.ts +19 -0
- package/src/extension/team-tool/cancel.ts +337 -0
- package/src/extension/team-tool/chain-dispatch.ts +95 -0
- package/src/extension/team-tool/chain-executor.ts +379 -0
- package/src/extension/team-tool/config-patch.ts +50 -0
- package/src/extension/team-tool/context.ts +149 -0
- package/src/extension/team-tool/destructive-gate.ts +52 -0
- package/src/extension/team-tool/dispatch/automate.ts +80 -0
- package/src/extension/team-tool/dispatch/control.ts +42 -0
- package/src/extension/team-tool/dispatch/index.ts +95 -0
- package/src/extension/team-tool/dispatch/manage.ts +161 -0
- package/src/extension/team-tool/dispatch/run.ts +53 -0
- package/src/extension/team-tool/dispatch/status.ts +187 -0
- package/src/extension/team-tool/doctor.ts +431 -0
- package/src/extension/team-tool/explain.ts +282 -0
- package/src/extension/team-tool/failure-patterns.ts +118 -0
- package/src/extension/team-tool/goal-wrap.ts +330 -0
- package/src/extension/team-tool/goal.ts +556 -0
- package/src/extension/team-tool/handle-schedule.ts +332 -0
- package/src/extension/team-tool/handle-settings.ts +520 -0
- package/src/extension/team-tool/health-monitor.ts +481 -0
- package/src/extension/team-tool/inspect.ts +88 -0
- package/src/extension/team-tool/intent-policy.ts +45 -0
- package/src/extension/team-tool/lifecycle-actions.ts +615 -0
- package/src/extension/team-tool/orchestrate.ts +98 -0
- package/src/extension/team-tool/parallel-dispatch.ts +183 -0
- package/src/extension/team-tool/plan.ts +50 -0
- package/src/extension/team-tool/respond.ts +150 -0
- package/src/extension/team-tool/run-deadline.ts +66 -0
- package/src/extension/team-tool/run-not-found.ts +49 -0
- package/src/extension/team-tool/run.ts +1051 -0
- package/src/extension/team-tool/status.ts +260 -0
- package/src/extension/team-tool/workflow-manage.ts +261 -0
- package/src/extension/team-tool-types.ts +26 -0
- package/src/extension/team-tool.ts +750 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +108 -0
- package/src/hooks/registry.ts +200 -0
- package/src/hooks/types.ts +69 -0
- package/src/i18n.ts +212 -0
- package/src/observability/correlation.ts +50 -0
- package/src/observability/event-bus.ts +86 -0
- package/src/observability/event-to-metric.ts +170 -0
- package/src/observability/exporters/adapter.ts +27 -0
- package/src/observability/exporters/otlp-exporter.ts +356 -0
- package/src/observability/exporters/prometheus-exporter.ts +54 -0
- package/src/observability/metric-registry.ts +100 -0
- package/src/observability/metric-retention.ts +64 -0
- package/src/observability/metric-sink.ts +99 -0
- package/src/observability/metrics-primitives.ts +219 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +10 -0
- package/src/plugins/plugins/vitest.ts +9 -0
- package/src/prompt/prompt-runtime.ts +380 -0
- package/src/runtime/adaptive-plan.ts +563 -0
- package/src/runtime/agent-control.ts +215 -0
- package/src/runtime/agent-memory.ts +81 -0
- package/src/runtime/agent-observability.ts +122 -0
- package/src/runtime/anchor-manager.ts +475 -0
- package/src/runtime/async-marker.ts +32 -0
- package/src/runtime/async-runner.ts +381 -0
- package/src/runtime/attention-events.ts +28 -0
- package/src/runtime/auto-summarize.ts +344 -0
- package/src/runtime/background-runner.ts +840 -0
- package/src/runtime/batch-barrier.ts +147 -0
- package/src/runtime/broker-issuer.ts +39 -0
- package/src/runtime/cancellation-token.ts +99 -0
- package/src/runtime/cancellation.ts +99 -0
- package/src/runtime/capability-inventory.ts +137 -0
- package/src/runtime/chain-parser.ts +237 -0
- package/src/runtime/chain-runner.ts +606 -0
- package/src/runtime/checkpoint.ts +291 -0
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi-spawn.ts +287 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +1105 -0
- package/src/runtime/coalesce-tasks.ts +268 -0
- package/src/runtime/code-summary.ts +292 -0
- package/src/runtime/command-trace.ts +105 -0
- package/src/runtime/compact-pipeline.ts +56 -0
- package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
- package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
- package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
- package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
- package/src/runtime/compact-stages/index.ts +23 -0
- package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
- package/src/runtime/compact-stages/truncation-stage.ts +74 -0
- package/src/runtime/compaction-summary.ts +278 -0
- package/src/runtime/completion-guard.ts +207 -0
- package/src/runtime/concurrency.ts +58 -0
- package/src/runtime/crash-classification.ts +229 -0
- package/src/runtime/crash-recovery.ts +594 -0
- package/src/runtime/crew-agent-records.ts +595 -0
- package/src/runtime/crew-agent-runtime.ts +62 -0
- package/src/runtime/crew-broker-child.ts +88 -0
- package/src/runtime/crew-broker-client.ts +673 -0
- package/src/runtime/crew-broker-tokens.ts +159 -0
- package/src/runtime/crew-broker.ts +1304 -0
- package/src/runtime/crew-hooks.ts +227 -0
- package/src/runtime/cross-extension-rpc.ts +143 -0
- package/src/runtime/custom-tools/irc-tool.ts +282 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
- package/src/runtime/deadletter.ts +47 -0
- package/src/runtime/delivery-coordinator.ts +211 -0
- package/src/runtime/delta-conflict.ts +348 -0
- package/src/runtime/deterministic-ast.ts +161 -0
- package/src/runtime/diagnostic-export.ts +171 -0
- package/src/runtime/direct-run.ts +45 -0
- package/src/runtime/dwf-state-store.ts +102 -0
- package/src/runtime/dynamic-workflow-context.ts +1013 -0
- package/src/runtime/dynamic-workflow-runner.ts +325 -0
- package/src/runtime/effectiveness.ts +90 -0
- package/src/runtime/errors/crew-errors.ts +162 -0
- package/src/runtime/event-stream-bridge.ts +98 -0
- package/src/runtime/foreground-control.ts +193 -0
- package/src/runtime/foreground-watchdog.ts +132 -0
- package/src/runtime/global-worker-cap.ts +96 -0
- package/src/runtime/goal-achievement.ts +148 -0
- package/src/runtime/goal-evaluator.ts +365 -0
- package/src/runtime/goal-loop-runner.ts +825 -0
- package/src/runtime/goal-state-store.ts +219 -0
- package/src/runtime/green-contract.ts +55 -0
- package/src/runtime/group-join.ts +267 -0
- package/src/runtime/handoff-manager.ts +598 -0
- package/src/runtime/heartbeat-gradient.ts +36 -0
- package/src/runtime/heartbeat-watcher.ts +239 -0
- package/src/runtime/hidden-handoff.ts +407 -0
- package/src/runtime/important-line-classifier.ts +140 -0
- package/src/runtime/intercom-bridge.ts +187 -0
- package/src/runtime/iteration-hooks.ts +305 -0
- package/src/runtime/live-agent-control.ts +136 -0
- package/src/runtime/live-agent-manager.ts +704 -0
- package/src/runtime/live-control-realtime.ts +56 -0
- package/src/runtime/live-extension-bridge.ts +148 -0
- package/src/runtime/live-irc.ts +97 -0
- package/src/runtime/live-session-health.ts +108 -0
- package/src/runtime/live-session-runtime.ts +1146 -0
- package/src/runtime/loop-gates.ts +128 -0
- package/src/runtime/manifest-cache.ts +375 -0
- package/src/runtime/mcp-proxy.ts +105 -0
- package/src/runtime/metric-parser.ts +36 -0
- package/src/runtime/model-fallback.ts +409 -0
- package/src/runtime/model-resolver.ts +126 -0
- package/src/runtime/model-scope.ts +158 -0
- package/src/runtime/orphan-worker-registry.ts +465 -0
- package/src/runtime/output-validator.ts +202 -0
- package/src/runtime/overflow-recovery.ts +206 -0
- package/src/runtime/parallel-research.ts +68 -0
- package/src/runtime/parallel-utils.ts +160 -0
- package/src/runtime/parent-guard.ts +134 -0
- package/src/runtime/path-overlap.ts +150 -0
- package/src/runtime/peer-dep.ts +292 -0
- package/src/runtime/per-write-validator.ts +181 -0
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/phase-tracker.ts +385 -0
- package/src/runtime/pi-args.ts +655 -0
- package/src/runtime/pi-json-output.ts +170 -0
- package/src/runtime/pi-spawn.ts +273 -0
- package/src/runtime/pipeline-runner.ts +523 -0
- package/src/runtime/plan-templates.ts +201 -0
- package/src/runtime/policy-engine.ts +115 -0
- package/src/runtime/post-checks.ts +142 -0
- package/src/runtime/post-exit-stdio-guard.ts +109 -0
- package/src/runtime/process-lifecycle.ts +491 -0
- package/src/runtime/process-status.ts +154 -0
- package/src/runtime/progress-event-coalescer.ts +44 -0
- package/src/runtime/progress-tracker.ts +124 -0
- package/src/runtime/prose-compressor.ts +162 -0
- package/src/runtime/recovery-recipes.ts +203 -0
- package/src/runtime/replace.ts +570 -0
- package/src/runtime/resilient-edit.ts +153 -0
- package/src/runtime/result-extractor.ts +217 -0
- package/src/runtime/retry-executor.ts +109 -0
- package/src/runtime/retry-runner.ts +336 -0
- package/src/runtime/role-permission.ts +59 -0
- package/src/runtime/run-coalesced-task-group.ts +308 -0
- package/src/runtime/run-drift.ts +219 -0
- package/src/runtime/run-tracker.ts +116 -0
- package/src/runtime/run-worker.ts +78 -0
- package/src/runtime/runtime-policy.ts +29 -0
- package/src/runtime/runtime-resolver.ts +172 -0
- package/src/runtime/runtime-warmup.ts +160 -0
- package/src/runtime/scheduler.ts +353 -0
- package/src/runtime/semaphore.ts +141 -0
- package/src/runtime/sensitive-paths.ts +93 -0
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -0
- package/src/runtime/settings-store.ts +155 -0
- package/src/runtime/sidechain-output.ts +35 -0
- package/src/runtime/single-agent-compose.ts +84 -0
- package/src/runtime/skill-effectiveness.ts +524 -0
- package/src/runtime/skill-instructions.ts +394 -0
- package/src/runtime/stale-reconciler.ts +680 -0
- package/src/runtime/stream-preview.ts +184 -0
- package/src/runtime/streaming-output.ts +50 -0
- package/src/runtime/subagent-manager.ts +561 -0
- package/src/runtime/subprocess-tool-registry.ts +70 -0
- package/src/runtime/supervisor-contact.ts +64 -0
- package/src/runtime/task-display.ts +52 -0
- package/src/runtime/task-graph-scheduler.ts +227 -0
- package/src/runtime/task-graph.ts +290 -0
- package/src/runtime/task-health.ts +83 -0
- package/src/runtime/task-id.ts +161 -0
- package/src/runtime/task-output-context.ts +602 -0
- package/src/runtime/task-packet.ts +268 -0
- package/src/runtime/task-quality.ts +199 -0
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/child-executor.ts +790 -0
- package/src/runtime/task-runner/context-retrieval.ts +163 -0
- package/src/runtime/task-runner/live-executor.ts +227 -0
- package/src/runtime/task-runner/output-splitter.ts +152 -0
- package/src/runtime/task-runner/post-execution.ts +480 -0
- package/src/runtime/task-runner/pre-execution.ts +365 -0
- package/src/runtime/task-runner/progress.ts +157 -0
- package/src/runtime/task-runner/prompt-builder.ts +278 -0
- package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
- package/src/runtime/task-runner/result-utils.ts +16 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
- package/src/runtime/task-runner/run-projection.ts +125 -0
- package/src/runtime/task-runner/scaffold-executor.ts +25 -0
- package/src/runtime/task-runner/state-helpers.ts +176 -0
- package/src/runtime/task-runner/tail-read.ts +34 -0
- package/src/runtime/task-runner.ts +216 -0
- package/src/runtime/team-runner-artifacts.ts +13 -0
- package/src/runtime/team-runner.ts +2396 -0
- package/src/runtime/tool-output-pruner.ts +333 -0
- package/src/runtime/tool-progress.ts +278 -0
- package/src/runtime/usage-tracker.ts +73 -0
- package/src/runtime/verification-gates.ts +579 -0
- package/src/runtime/verification-integrity.ts +108 -0
- package/src/runtime/verification-worktree.ts +186 -0
- package/src/runtime/worker-heartbeat.ts +25 -0
- package/src/runtime/worker-startup.ts +83 -0
- package/src/runtime/workflow-state.ts +195 -0
- package/src/runtime/workspace-lock.ts +443 -0
- package/src/runtime/workspace-tree.ts +312 -0
- package/src/runtime/yield-handler.ts +224 -0
- package/src/runtime/zombie-scanner.ts +298 -0
- package/src/schema/config-schema.ts +327 -0
- package/src/schema/team-tool-schema.ts +492 -0
- package/src/schema/validation-types.ts +159 -0
- package/src/skills/discover-skills.ts +203 -0
- package/src/skills/skill-templates.ts +456 -0
- package/src/skills/validate.ts +285 -0
- package/src/state/active-run-registry.ts +439 -0
- package/src/state/artifact-store.ts +176 -0
- package/src/state/atomic-write.ts +970 -0
- package/src/state/blob-store.ts +308 -0
- package/src/state/contracts.ts +160 -0
- package/src/state/crew-init.ts +269 -0
- package/src/state/decision-ledger.ts +372 -0
- package/src/state/event-log-rotation.ts +399 -0
- package/src/state/event-log.ts +1465 -0
- package/src/state/event-reconstructor.ts +229 -0
- package/src/state/gitignore-manager.ts +46 -0
- package/src/state/health-store.ts +79 -0
- package/src/state/hook-instinct-bridge.ts +94 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +275 -0
- package/src/state/jsonl-writer.ts +107 -0
- package/src/state/locks.ts +562 -0
- package/src/state/mailbox.ts +916 -0
- package/src/state/observation-store.ts +176 -0
- package/src/state/run-cache.ts +193 -0
- package/src/state/run-graph.ts +183 -0
- package/src/state/run-metrics.ts +165 -0
- package/src/state/schedule.ts +166 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +973 -0
- package/src/state/task-claims.ts +61 -0
- package/src/state/tiered-eval.ts +480 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +447 -0
- package/src/state/usage.ts +138 -0
- package/src/state/worker-atomic-writer.ts +198 -0
- package/src/subagents/async-entry.ts +1 -0
- package/src/subagents/index.ts +3 -0
- package/src/subagents/live/control.ts +1 -0
- package/src/subagents/live/manager.ts +1 -0
- package/src/subagents/live/realtime.ts +1 -0
- package/src/subagents/live/session-runtime.ts +1 -0
- package/src/subagents/manager.ts +1 -0
- package/src/subagents/spawn.ts +1 -0
- package/src/teams/discover-teams.ts +187 -0
- package/src/teams/team-config.ts +27 -0
- package/src/teams/team-serializer.ts +38 -0
- package/src/tools/safe-bash-extension.ts +54 -0
- package/src/tools/safe-bash.ts +505 -0
- package/src/types/diff.d.ts +18 -0
- package/src/types/new-api-types.ts +35 -0
- package/src/ui/agent-management-overlay.ts +160 -0
- package/src/ui/card-colors.ts +139 -0
- package/src/ui/crew-footer.ts +102 -0
- package/src/ui/crew-select-list.ts +112 -0
- package/src/ui/dashboard-panes/agents-pane.ts +164 -0
- package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
- package/src/ui/dashboard-panes/capability-pane.ts +77 -0
- package/src/ui/dashboard-panes/health-pane.ts +31 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
- package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
- package/src/ui/dashboard-panes/progress-pane.ts +38 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
- package/src/ui/deploy-bundled-themes.ts +71 -0
- package/src/ui/dwf-phase-display.ts +151 -0
- package/src/ui/dynamic-border.ts +35 -0
- package/src/ui/format-helpers.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +74 -0
- package/src/ui/key-utils.ts +42 -0
- package/src/ui/keybinding-map.ts +248 -0
- package/src/ui/layout-primitives.ts +106 -0
- package/src/ui/live-conversation-overlay.ts +183 -0
- package/src/ui/live-duration.ts +55 -0
- package/src/ui/live-run-sidebar.ts +293 -0
- package/src/ui/loaders.ts +6 -0
- package/src/ui/mascot.ts +449 -0
- package/src/ui/overlays/agent-picker-overlay.ts +66 -0
- package/src/ui/overlays/confirm-overlay.ts +60 -0
- package/src/ui/overlays/help-overlay.ts +177 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
- package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
- package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
- package/src/ui/pi-ui-compat.ts +68 -0
- package/src/ui/powerbar-publisher.ts +459 -0
- package/src/ui/render-coalescer.ts +101 -0
- package/src/ui/render-diff.ts +160 -0
- package/src/ui/render-scheduler.ts +278 -0
- package/src/ui/run-action-dispatcher.ts +182 -0
- package/src/ui/run-dashboard.ts +910 -0
- package/src/ui/run-event-bus.ts +381 -0
- package/src/ui/run-snapshot-cache.ts +1057 -0
- package/src/ui/settings-overlay.ts +1067 -0
- package/src/ui/shared-overlay-scheduler.ts +96 -0
- package/src/ui/snapshot-types.ts +90 -0
- package/src/ui/spinner.ts +17 -0
- package/src/ui/status-colors.ts +143 -0
- package/src/ui/syntax-highlight.ts +136 -0
- package/src/ui/terminal-status.ts +269 -0
- package/src/ui/theme-adapter.ts +271 -0
- package/src/ui/theme-discovery.ts +199 -0
- package/src/ui/tool-progress-formatter.ts +93 -0
- package/src/ui/tool-renderers/brief-mode.ts +225 -0
- package/src/ui/tool-renderers/index.ts +726 -0
- package/src/ui/transcript-cache.ts +128 -0
- package/src/ui/transcript-entries.ts +256 -0
- package/src/ui/transcript-viewer.ts +424 -0
- package/src/ui/widget/index.ts +418 -0
- package/src/ui/widget/widget-formatters.ts +182 -0
- package/src/ui/widget/widget-model.ts +110 -0
- package/src/ui/widget/widget-renderer.ts +182 -0
- package/src/ui/widget/widget-types.ts +36 -0
- package/src/utils/bm25-search.ts +235 -0
- package/src/utils/completion-dedupe.ts +63 -0
- package/src/utils/conflict-detect.ts +668 -0
- package/src/utils/env-allowlist.ts +30 -0
- package/src/utils/env-filter.ts +155 -0
- package/src/utils/file-coalescer.ts +98 -0
- package/src/utils/fingerprint.ts +180 -0
- package/src/utils/frontmatter.ts +70 -0
- package/src/utils/fs-watch.ts +37 -0
- package/src/utils/gh-protocol.ts +556 -0
- package/src/utils/git.ts +260 -0
- package/src/utils/guards.ts +107 -0
- package/src/utils/ids.ts +24 -0
- package/src/utils/incremental-reader.ts +220 -0
- package/src/utils/internal-error.ts +10 -0
- package/src/utils/names.ts +36 -0
- package/src/utils/ndjson.ts +115 -0
- package/src/utils/paths.ts +227 -0
- package/src/utils/project-detector.ts +160 -0
- package/src/utils/redaction.ts +370 -0
- package/src/utils/resolve-shell.ts +36 -0
- package/src/utils/run-watcher-registry.ts +152 -0
- package/src/utils/safe-paths.ts +393 -0
- package/src/utils/scan-cache.ts +144 -0
- package/src/utils/session-utils.ts +110 -0
- package/src/utils/sleep.ts +54 -0
- package/src/utils/socket-path.ts +164 -0
- package/src/utils/sse-parser.ts +131 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/timings.ts +33 -0
- package/src/utils/token-counter.ts +200 -0
- package/src/utils/visual.ts +207 -0
- package/src/workflows/cost-estimator.ts +34 -0
- package/src/workflows/discover-workflows.ts +308 -0
- package/src/workflows/intermediate-store.ts +166 -0
- package/src/workflows/preflight-validator.ts +168 -0
- package/src/workflows/topology-analyzer.ts +203 -0
- package/src/workflows/validate-workflow.ts +50 -0
- package/src/workflows/workflow-config.ts +75 -0
- package/src/workflows/workflow-serializer.ts +32 -0
- package/src/worktree/branch-freshness.ts +112 -0
- package/src/worktree/cleanup.ts +341 -0
- package/src/worktree/worktree-manager.ts +1204 -0
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { agentOutputPath, agentsPath, readCrewAgents, readCrewAgentsAsync } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
+
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
8
|
+
import { sequencePath } from "../state/event-log.ts";
|
|
9
|
+
import type { MailboxMessageStatus } from "../state/mailbox.ts";
|
|
10
|
+
import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
|
|
11
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
12
|
+
import { extractDwfPhaseState } from "./dwf-phase-display.ts";
|
|
13
|
+
import { runEventBus } from "./run-event-bus.ts";
|
|
14
|
+
import type {
|
|
15
|
+
RunSnapshotCache as RunSnapshotCacheBase,
|
|
16
|
+
RunUiGroupJoin,
|
|
17
|
+
RunUiMailbox,
|
|
18
|
+
RunUiProgress,
|
|
19
|
+
RunUiSnapshot,
|
|
20
|
+
RunUiUsage,
|
|
21
|
+
} from "./snapshot-types.ts";
|
|
22
|
+
|
|
23
|
+
export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
24
|
+
preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
|
|
25
|
+
preloadAllStale(runIds: string[]): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TTL_MS = 1500;
|
|
29
|
+
const DEFAULT_MAX_ENTRIES = 24;
|
|
30
|
+
const DEFAULT_RECENT_EVENTS = 20;
|
|
31
|
+
const DEFAULT_RECENT_OUTPUT_LINES = 20;
|
|
32
|
+
const MAX_TAIL_BYTES = 32 * 1024;
|
|
33
|
+
/** Max JSONL lines to tail when reading growing files (events, mailbox). */
|
|
34
|
+
const MAX_TAIL_LINES = 500;
|
|
35
|
+
|
|
36
|
+
interface FileStamp {
|
|
37
|
+
mtimeMs: number;
|
|
38
|
+
size: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SnapshotStamps {
|
|
42
|
+
manifest: FileStamp;
|
|
43
|
+
tasks: FileStamp;
|
|
44
|
+
agents: FileStamp;
|
|
45
|
+
events: FileStamp;
|
|
46
|
+
mailbox: FileStamp;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CacheEntry {
|
|
50
|
+
snapshot: RunUiSnapshot;
|
|
51
|
+
stamps: SnapshotStamps;
|
|
52
|
+
loadedAtMs: number;
|
|
53
|
+
lastAccessMs: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RunSnapshotCacheOptions {
|
|
57
|
+
ttlMs?: number;
|
|
58
|
+
maxEntries?: number;
|
|
59
|
+
recentEvents?: number;
|
|
60
|
+
recentOutputLines?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function zeroStamp(): FileStamp {
|
|
64
|
+
return { mtimeMs: 0, size: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stampFile(filePath: string | undefined): FileStamp {
|
|
68
|
+
if (!filePath) return zeroStamp();
|
|
69
|
+
try {
|
|
70
|
+
const stat = fs.statSync(filePath);
|
|
71
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
72
|
+
} catch {
|
|
73
|
+
return zeroStamp();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
|
|
78
|
+
if (!filePath) return zeroStamp();
|
|
79
|
+
try {
|
|
80
|
+
const stat = await fs.promises.stat(filePath);
|
|
81
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
82
|
+
} catch {
|
|
83
|
+
return zeroStamp();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sprint-1 / 1.4 — events stamp uses the `.seq` sidecar instead of stat-ing
|
|
89
|
+
* the JSONL itself. The sequence file is a few bytes long and the persisted
|
|
90
|
+
* counter monotonically increases even when the events log gets rotated and
|
|
91
|
+
* shrinks (rotation truncates `size` but keeps `seq` ascending). Encoding the
|
|
92
|
+
* counter into the size field keeps the FileStamp structure unchanged so
|
|
93
|
+
* `sameStamp` continues to work.
|
|
94
|
+
*/
|
|
95
|
+
function eventsStamp(eventsPath: string): FileStamp {
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(sequencePath(eventsPath), "utf-8");
|
|
98
|
+
const seq = Number.parseInt(raw.trim(), 10);
|
|
99
|
+
if (Number.isFinite(seq) && seq >= 0) return { mtimeMs: 0, size: seq + 1 };
|
|
100
|
+
} catch {
|
|
101
|
+
/* fall through to legacy stat */
|
|
102
|
+
}
|
|
103
|
+
return stampFile(eventsPath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function eventsStampAsync(eventsPath: string): Promise<FileStamp> {
|
|
107
|
+
try {
|
|
108
|
+
const raw = await fs.promises.readFile(sequencePath(eventsPath), "utf-8");
|
|
109
|
+
const seq = Number.parseInt(raw.trim(), 10);
|
|
110
|
+
if (Number.isFinite(seq) && seq >= 0) return { mtimeMs: 0, size: seq + 1 };
|
|
111
|
+
} catch {
|
|
112
|
+
/* fall through to legacy stat */
|
|
113
|
+
}
|
|
114
|
+
return stampFileAsync(eventsPath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function combineStamps(stamps: FileStamp[]): FileStamp {
|
|
118
|
+
return stamps.reduce(
|
|
119
|
+
(acc, stamp) => ({
|
|
120
|
+
mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs),
|
|
121
|
+
size: acc.size + stamp.size,
|
|
122
|
+
}),
|
|
123
|
+
zeroStamp(),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function mailboxStamp(manifest: TeamRunManifest): FileStamp {
|
|
128
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
129
|
+
const tasksRoot = path.join(root, "tasks");
|
|
130
|
+
// 1.10 (UI-P1-3): replace the O(tasks) per-task inbox/outbox stat loop
|
|
131
|
+
// with a single stat on the tasks dir. The dir mtime changes whenever a
|
|
132
|
+
// task mailbox subdir is created or removed (the dominant cost when a run
|
|
133
|
+
// has many tasks), and per-file content changes are still caught by the
|
|
134
|
+
// top-level inbox/outbox/delivery stats below. The event-bus
|
|
135
|
+
// (`crew.mailbox.*`) also drives invalidation, so the stamp is the
|
|
136
|
+
// fallback poll, not the only source of truth.
|
|
137
|
+
return combineStamps([
|
|
138
|
+
stampFile(path.join(root, "inbox.jsonl")),
|
|
139
|
+
stampFile(path.join(root, "outbox.jsonl")),
|
|
140
|
+
stampFile(path.join(root, "delivery.json")),
|
|
141
|
+
stampFile(tasksRoot),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
|
|
146
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
147
|
+
const tasksRoot = path.join(root, "tasks");
|
|
148
|
+
// 1.10 (UI-P1-3): see sync `mailboxStamp` above — single tasksRoot stat
|
|
149
|
+
// replaces the O(tasks) per-task loop.
|
|
150
|
+
return combineStamps([
|
|
151
|
+
await stampFileAsync(path.join(root, "inbox.jsonl")),
|
|
152
|
+
await stampFileAsync(path.join(root, "outbox.jsonl")),
|
|
153
|
+
await stampFileAsync(path.join(root, "delivery.json")),
|
|
154
|
+
await stampFileAsync(tasksRoot),
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
|
|
159
|
+
try {
|
|
160
|
+
return agentOutputPath(manifest, agent.taskId);
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): FileStamp {
|
|
167
|
+
return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
|
|
171
|
+
return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sameStamp(a: FileStamp, b: FileStamp): boolean {
|
|
175
|
+
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sameStamps(a: SnapshotStamps, b: SnapshotStamps): boolean {
|
|
179
|
+
return (
|
|
180
|
+
sameStamp(a.manifest, b.manifest) &&
|
|
181
|
+
sameStamp(a.tasks, b.tasks) &&
|
|
182
|
+
sameStamp(a.agents, b.agents) &&
|
|
183
|
+
sameStamp(a.events, b.events) &&
|
|
184
|
+
sameStamp(a.mailbox, b.mailbox)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")) as unknown;
|
|
191
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error(`Failed to parse tasks at ${tasksPath}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
198
|
+
function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
|
|
199
|
+
if (limit <= 0) return [];
|
|
200
|
+
try {
|
|
201
|
+
const stat = fs.statSync(filePath);
|
|
202
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
203
|
+
const fd = fs.openSync(filePath, "r");
|
|
204
|
+
try {
|
|
205
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
206
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
207
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
208
|
+
return lines
|
|
209
|
+
.flatMap((line) => {
|
|
210
|
+
const item = parse(line);
|
|
211
|
+
return item ? [item] : [];
|
|
212
|
+
})
|
|
213
|
+
.slice(-limit);
|
|
214
|
+
} finally {
|
|
215
|
+
fs.closeSync(fd);
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
223
|
+
async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
|
|
224
|
+
if (limit <= 0) return [];
|
|
225
|
+
try {
|
|
226
|
+
const stat = await fs.promises.stat(filePath);
|
|
227
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
228
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
229
|
+
try {
|
|
230
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
231
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
232
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
233
|
+
return lines
|
|
234
|
+
.flatMap((line) => {
|
|
235
|
+
const item = parse(line);
|
|
236
|
+
return item ? [item] : [];
|
|
237
|
+
})
|
|
238
|
+
.slice(-limit);
|
|
239
|
+
} finally {
|
|
240
|
+
await handle.close();
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
248
|
+
return tailJsonlLines(eventsPath, limit, (line) => {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(line) as unknown;
|
|
251
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
252
|
+
} catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
|
|
259
|
+
return tailJsonlLinesAsync(eventsPath, limit, (line) => {
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(line) as unknown;
|
|
262
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
263
|
+
} catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function tailLines(filePath: string, limit: number): string[] {
|
|
270
|
+
if (limit <= 0) return [];
|
|
271
|
+
try {
|
|
272
|
+
const stat = fs.statSync(filePath);
|
|
273
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
274
|
+
const fd = fs.openSync(filePath, "r");
|
|
275
|
+
try {
|
|
276
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
277
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
278
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
279
|
+
} finally {
|
|
280
|
+
fs.closeSync(fd);
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
|
|
288
|
+
if (limit <= 0) return [];
|
|
289
|
+
try {
|
|
290
|
+
const stat = await fs.promises.stat(filePath);
|
|
291
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
292
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
293
|
+
try {
|
|
294
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
295
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
296
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
297
|
+
} finally {
|
|
298
|
+
await handle.close();
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
|
|
306
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
307
|
+
const fromFiles = agents.flatMap((agent) => {
|
|
308
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
309
|
+
return outputPath ? tailLines(outputPath, limit) : [];
|
|
310
|
+
});
|
|
311
|
+
return [...fromProgress, ...fromFiles]
|
|
312
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
313
|
+
.filter(Boolean)
|
|
314
|
+
.slice(-limit);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
|
|
318
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
319
|
+
const fromFilesArrays = await Promise.all(
|
|
320
|
+
agents.map((agent) => {
|
|
321
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
322
|
+
return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
const fromFiles = fromFilesArrays.flat();
|
|
326
|
+
return [...fromProgress, ...fromFiles]
|
|
327
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
.slice(-limit);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
333
|
+
const progress: RunUiProgress = {
|
|
334
|
+
total: tasks.length,
|
|
335
|
+
completed: 0,
|
|
336
|
+
running: 0,
|
|
337
|
+
failed: 0,
|
|
338
|
+
queued: 0,
|
|
339
|
+
waiting: 0,
|
|
340
|
+
cancelled: 0,
|
|
341
|
+
skipped: 0,
|
|
342
|
+
needsAttention: 0,
|
|
343
|
+
};
|
|
344
|
+
for (const task of tasks) {
|
|
345
|
+
if (task.status === "completed") progress.completed += 1;
|
|
346
|
+
else if (task.status === "running") progress.running += 1;
|
|
347
|
+
else if (task.status === "failed") progress.failed += 1;
|
|
348
|
+
else if (task.status === "queued") progress.queued += 1;
|
|
349
|
+
else if (task.status === "waiting") progress.waiting = (progress.waiting ?? 0) + 1;
|
|
350
|
+
else if (task.status === "cancelled") progress.cancelled = (progress.cancelled ?? 0) + 1;
|
|
351
|
+
else if (task.status === "skipped") progress.skipped = (progress.skipped ?? 0) + 1;
|
|
352
|
+
else if (task.status === "needs_attention") progress.needsAttention = (progress.needsAttention ?? 0) + 1;
|
|
353
|
+
}
|
|
354
|
+
return progress;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function usageFrom(tasks: TeamTaskState[], agents: CrewAgentRecord[]): RunUiUsage {
|
|
358
|
+
const taskUsage = tasks.reduce(
|
|
359
|
+
(acc, task) => {
|
|
360
|
+
acc.tokensIn += task.usage?.input ?? 0;
|
|
361
|
+
acc.tokensOut += task.usage?.output ?? 0;
|
|
362
|
+
acc.toolUses += task.agentProgress?.toolCount ?? 0;
|
|
363
|
+
return acc;
|
|
364
|
+
},
|
|
365
|
+
{ tokensIn: 0, tokensOut: 0, toolUses: 0 },
|
|
366
|
+
);
|
|
367
|
+
if (taskUsage.tokensIn || taskUsage.tokensOut || taskUsage.toolUses) return taskUsage;
|
|
368
|
+
return agents.reduce(
|
|
369
|
+
(acc, agent) => {
|
|
370
|
+
acc.tokensIn += agent.usage?.input ?? 0;
|
|
371
|
+
acc.tokensOut += agent.usage?.output ?? agent.progress?.tokens ?? 0;
|
|
372
|
+
acc.toolUses += agent.toolUses ?? agent.progress?.toolCount ?? 0;
|
|
373
|
+
return acc;
|
|
374
|
+
},
|
|
375
|
+
{ tokensIn: 0, tokensOut: 0, toolUses: 0 },
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isMailboxStatus(value: unknown): value is MailboxMessageStatus {
|
|
380
|
+
return value === "queued" || value === "delivered" || value === "acknowledged";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function readDeliveryMessages(filePath: string): Record<string, MailboxMessageStatus> {
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
386
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
387
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
388
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
389
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
390
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
391
|
+
return output;
|
|
392
|
+
} catch {
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
|
|
398
|
+
try {
|
|
399
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
400
|
+
const parsed = JSON.parse(content) as unknown;
|
|
401
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
402
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
403
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
404
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
405
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
406
|
+
return output;
|
|
407
|
+
} catch {
|
|
408
|
+
return {};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
|
|
413
|
+
return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(line) as unknown;
|
|
416
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
417
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
418
|
+
const data =
|
|
419
|
+
message.data && typeof message.data === "object" && !Array.isArray(message.data)
|
|
420
|
+
? (message.data as Record<string, unknown>)
|
|
421
|
+
: undefined;
|
|
422
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
423
|
+
return {
|
|
424
|
+
requestId: data.requestId,
|
|
425
|
+
messageId: message.id,
|
|
426
|
+
partial: data.partial === true,
|
|
427
|
+
ack: delivery[message.id] === "acknowledged" ? ("acknowledged" as const) : ("pending" as const),
|
|
428
|
+
};
|
|
429
|
+
} catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
|
|
436
|
+
return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(line) as unknown;
|
|
439
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
440
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
441
|
+
const data =
|
|
442
|
+
message.data && typeof message.data === "object" && !Array.isArray(message.data)
|
|
443
|
+
? (message.data as Record<string, unknown>)
|
|
444
|
+
: undefined;
|
|
445
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
446
|
+
return {
|
|
447
|
+
requestId: data.requestId,
|
|
448
|
+
messageId: message.id,
|
|
449
|
+
partial: data.partial === true,
|
|
450
|
+
ack: delivery[message.id] === "acknowledged" ? ("acknowledged" as const) : ("pending" as const),
|
|
451
|
+
};
|
|
452
|
+
} catch {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
interface MailboxCount {
|
|
459
|
+
count: number;
|
|
460
|
+
approximate: boolean;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
interface MailboxKindCount extends MailboxCount {
|
|
464
|
+
steer: number;
|
|
465
|
+
followUp: number;
|
|
466
|
+
response: number;
|
|
467
|
+
message: number;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function tailApproximate(filePath: string): boolean {
|
|
471
|
+
try {
|
|
472
|
+
return fs.statSync(filePath).size > MAX_TAIL_BYTES;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function tailApproximateAsync(filePath: string): Promise<boolean> {
|
|
479
|
+
try {
|
|
480
|
+
return (await fs.promises.stat(filePath)).size > MAX_TAIL_BYTES;
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxKindCount {
|
|
487
|
+
const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
|
|
488
|
+
const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(line) as unknown;
|
|
491
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
492
|
+
const msg = parsed as {
|
|
493
|
+
id?: unknown;
|
|
494
|
+
status?: unknown;
|
|
495
|
+
kind?: unknown;
|
|
496
|
+
data?: unknown;
|
|
497
|
+
};
|
|
498
|
+
if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
|
|
499
|
+
if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
|
|
500
|
+
const kind =
|
|
501
|
+
typeof msg.kind === "string"
|
|
502
|
+
? msg.kind
|
|
503
|
+
: typeof (msg.data as Record<string, unknown>)?.kind === "string"
|
|
504
|
+
? ((msg.data as Record<string, unknown>).kind as string)
|
|
505
|
+
: undefined;
|
|
506
|
+
if (kind === "steer") kindCounts.steer++;
|
|
507
|
+
else if (kind === "follow-up") kindCounts.followUp++;
|
|
508
|
+
else if (kind === "response") kindCounts.response++;
|
|
509
|
+
else kindCounts.message++;
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
} catch {
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
}) as number[];
|
|
517
|
+
const count = items.reduce((sum, val) => sum + val, 0);
|
|
518
|
+
return {
|
|
519
|
+
count,
|
|
520
|
+
approximate: tailApproximate(filePath),
|
|
521
|
+
steer: kindCounts.steer,
|
|
522
|
+
followUp: kindCounts.followUp,
|
|
523
|
+
response: kindCounts.response,
|
|
524
|
+
message: kindCounts.message,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxKindCount> {
|
|
529
|
+
const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
|
|
530
|
+
const items = (await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
531
|
+
try {
|
|
532
|
+
const parsed = JSON.parse(line) as unknown;
|
|
533
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
534
|
+
const msg = parsed as {
|
|
535
|
+
id?: unknown;
|
|
536
|
+
status?: unknown;
|
|
537
|
+
kind?: unknown;
|
|
538
|
+
data?: unknown;
|
|
539
|
+
};
|
|
540
|
+
if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
|
|
541
|
+
if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
|
|
542
|
+
const kind =
|
|
543
|
+
typeof msg.kind === "string"
|
|
544
|
+
? msg.kind
|
|
545
|
+
: typeof (msg.data as Record<string, unknown>)?.kind === "string"
|
|
546
|
+
? ((msg.data as Record<string, unknown>).kind as string)
|
|
547
|
+
: undefined;
|
|
548
|
+
if (kind === "steer") kindCounts.steer++;
|
|
549
|
+
else if (kind === "follow-up") kindCounts.followUp++;
|
|
550
|
+
else if (kind === "response") kindCounts.response++;
|
|
551
|
+
else kindCounts.message++;
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
554
|
+
return 0;
|
|
555
|
+
} catch {
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
})) as number[];
|
|
559
|
+
const count = items.reduce((sum, val) => sum + val, 0);
|
|
560
|
+
return {
|
|
561
|
+
count,
|
|
562
|
+
approximate: await tailApproximateAsync(filePath),
|
|
563
|
+
steer: kindCounts.steer,
|
|
564
|
+
followUp: kindCounts.followUp,
|
|
565
|
+
response: kindCounts.response,
|
|
566
|
+
message: kindCounts.message,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
571
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
572
|
+
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
573
|
+
return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
|
|
577
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
578
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
579
|
+
return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function mergeKindCounts(a: MailboxKindCount, b: MailboxKindCount): MailboxKindCount {
|
|
583
|
+
return {
|
|
584
|
+
count: a.count + b.count,
|
|
585
|
+
approximate: a.approximate || b.approximate,
|
|
586
|
+
steer: a.steer + b.steer,
|
|
587
|
+
followUp: a.followUp + b.followUp,
|
|
588
|
+
response: a.response + b.response,
|
|
589
|
+
message: a.message + b.message,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
|
|
594
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
595
|
+
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
596
|
+
let inbox = readMailboxCounts(path.join(root, "inbox.jsonl"), delivery);
|
|
597
|
+
let outbox = readMailboxCounts(path.join(root, "outbox.jsonl"), delivery);
|
|
598
|
+
const tasksRoot = path.join(root, "tasks");
|
|
599
|
+
try {
|
|
600
|
+
for (const entry of fs.readdirSync(tasksRoot, {
|
|
601
|
+
withFileTypes: true,
|
|
602
|
+
})) {
|
|
603
|
+
if (!entry.isDirectory()) continue;
|
|
604
|
+
const taskInbox = readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
605
|
+
const taskOutbox = readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
606
|
+
inbox = mergeKindCounts(inbox, taskInbox);
|
|
607
|
+
outbox = mergeKindCounts(outbox, taskOutbox);
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
// No task mailboxes yet.
|
|
611
|
+
}
|
|
612
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
613
|
+
return {
|
|
614
|
+
inboxUnread: inbox.count,
|
|
615
|
+
outboxPending: outbox.count,
|
|
616
|
+
needsAttention: inbox.count + attentionAgents,
|
|
617
|
+
approximate: inbox.approximate || outbox.approximate,
|
|
618
|
+
steerUnread: inbox.steer + outbox.steer,
|
|
619
|
+
followUpUnread: inbox.followUp + outbox.followUp,
|
|
620
|
+
responseUnread: inbox.response + outbox.response,
|
|
621
|
+
messageUnread: inbox.message + outbox.message,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
|
|
626
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
627
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
628
|
+
let inbox = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
|
|
629
|
+
let outbox = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
|
|
630
|
+
const tasksRoot = path.join(root, "tasks");
|
|
631
|
+
try {
|
|
632
|
+
for (const entry of await fs.promises.readdir(tasksRoot, {
|
|
633
|
+
withFileTypes: true,
|
|
634
|
+
})) {
|
|
635
|
+
if (!entry.isDirectory()) continue;
|
|
636
|
+
const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
637
|
+
const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
638
|
+
inbox = mergeKindCounts(inbox, taskInbox);
|
|
639
|
+
outbox = mergeKindCounts(outbox, taskOutbox);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// No task mailboxes yet.
|
|
643
|
+
}
|
|
644
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
645
|
+
return {
|
|
646
|
+
inboxUnread: inbox.count,
|
|
647
|
+
outboxPending: outbox.count,
|
|
648
|
+
needsAttention: inbox.count + attentionAgents,
|
|
649
|
+
approximate: inbox.approximate || outbox.approximate,
|
|
650
|
+
steerUnread: inbox.steer + outbox.steer,
|
|
651
|
+
followUpUnread: inbox.followUp + outbox.followUp,
|
|
652
|
+
responseUnread: inbox.response + outbox.response,
|
|
653
|
+
messageUnread: inbox.message + outbox.message,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function cancellationReasonFromEvents(events: TeamEvent[]): string | undefined {
|
|
658
|
+
return [...events].reverse().find((event) => event.type === "run.cancelled" && typeof event.data?.reason === "string")?.data?.reason as
|
|
659
|
+
| string
|
|
660
|
+
| undefined;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
type SliceSignatures = NonNullable<RunUiSnapshot["sliceSignatures"]>;
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* FIND-08: compute the per-slice short hashes once per build and reuse the
|
|
667
|
+
* result for both the full-snapshot `signature` and the per-pane
|
|
668
|
+
* `sliceSignatures`. Previously, `signatureFor` and `sliceSignaturesFor` each
|
|
669
|
+
* called `JSON.stringify` + `createHash("sha256")` on the tasks, agents, and
|
|
670
|
+
* recentEvents projections independently — twice the work for the slices the
|
|
671
|
+
* two signatures share. The per-slice hashes are embedded into the full
|
|
672
|
+
* signature so the full-snapshot signature still changes whenever any slice
|
|
673
|
+
* changes (collision risk: 1 in 16^16 per build, well within tolerance for
|
|
674
|
+
* cache-invalidation use).
|
|
675
|
+
*/
|
|
676
|
+
function computeSliceSignatures(input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">): SliceSignatures {
|
|
677
|
+
const hash = (value: unknown): string => {
|
|
678
|
+
try {
|
|
679
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 12);
|
|
680
|
+
} catch {
|
|
681
|
+
return String(Date.now());
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
tasks: hash(input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage])),
|
|
686
|
+
agents: hash(
|
|
687
|
+
input.agents.map((agent) => [
|
|
688
|
+
agent.id,
|
|
689
|
+
agent.status,
|
|
690
|
+
agent.startedAt,
|
|
691
|
+
agent.completedAt,
|
|
692
|
+
agent.toolUses,
|
|
693
|
+
agent.progress,
|
|
694
|
+
agent.usage,
|
|
695
|
+
agent.model,
|
|
696
|
+
]),
|
|
697
|
+
),
|
|
698
|
+
mailbox: hash([input.mailbox, input.groupJoins]),
|
|
699
|
+
progress: hash([input.progress, input.usage, input.cancellationReason]),
|
|
700
|
+
events: hash(
|
|
701
|
+
input.recentEvents.map((event) => [
|
|
702
|
+
event.metadata?.seq,
|
|
703
|
+
event.time,
|
|
704
|
+
event.type,
|
|
705
|
+
event.taskId,
|
|
706
|
+
event.message,
|
|
707
|
+
event.data?.reason,
|
|
708
|
+
]),
|
|
709
|
+
),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function signatureFor(
|
|
714
|
+
input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">,
|
|
715
|
+
stamps: SnapshotStamps,
|
|
716
|
+
sliceSignatures: SliceSignatures,
|
|
717
|
+
): string {
|
|
718
|
+
try {
|
|
719
|
+
const digest = createHash("sha256");
|
|
720
|
+
digest.update(
|
|
721
|
+
JSON.stringify({
|
|
722
|
+
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
723
|
+
tasks: sliceSignatures.tasks,
|
|
724
|
+
agents: sliceSignatures.agents,
|
|
725
|
+
progress: input.progress,
|
|
726
|
+
usage: input.usage,
|
|
727
|
+
mailbox: input.mailbox,
|
|
728
|
+
groupJoins: input.groupJoins,
|
|
729
|
+
events: sliceSignatures.events,
|
|
730
|
+
cancellationReason: input.cancellationReason,
|
|
731
|
+
dwfPhaseState: input.dwfPhaseState,
|
|
732
|
+
output: input.recentOutputLines,
|
|
733
|
+
stamps,
|
|
734
|
+
}),
|
|
735
|
+
);
|
|
736
|
+
return digest.digest("hex").slice(0, 16);
|
|
737
|
+
} catch {
|
|
738
|
+
// Circular reference or non-serializable data — fall back to timestamp.
|
|
739
|
+
return String(Date.now());
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 1.6 / 1.7 — compute one short hash per logical slice of the snapshot so
|
|
745
|
+
* dashboard panes / widget can short-circuit when their slice hasn't moved.
|
|
746
|
+
* The slice contents must mirror what `signatureFor` packs into each branch.
|
|
747
|
+
*/
|
|
748
|
+
function sliceSignaturesFor(sliceSignatures: SliceSignatures): SliceSignatures {
|
|
749
|
+
return sliceSignatures;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): SnapshotStamps {
|
|
753
|
+
// 1.4: use events sequence file instead of stat-ing the events log directly.
|
|
754
|
+
// 1.5: drop per-agent output.log stamping; rely on event-bus invalidation
|
|
755
|
+
// (`crew.subagent.*` and stream events) and on agents.json mtime which
|
|
756
|
+
// updates whenever crew-agent-records.appendCrewAgentOutput touches the
|
|
757
|
+
// aggregate. Saves O(N) statSync per render tick.
|
|
758
|
+
return {
|
|
759
|
+
manifest: stampFile(path.join(manifest.stateRoot, "manifest.json")),
|
|
760
|
+
tasks: stampFile(manifest.tasksPath),
|
|
761
|
+
agents: stampFile(agentsPath(manifest)),
|
|
762
|
+
events: eventsStamp(manifest.eventsPath),
|
|
763
|
+
mailbox: mailboxStamp(manifest),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function stampsForAsync(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
|
|
768
|
+
const [manifestStamp, tasksStamp, agentsStamp, eventsStampValue, mailbox] = await Promise.all([
|
|
769
|
+
stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
|
|
770
|
+
stampFileAsync(manifest.tasksPath),
|
|
771
|
+
stampFileAsync(agentsPath(manifest)),
|
|
772
|
+
eventsStampAsync(manifest.eventsPath),
|
|
773
|
+
mailboxStampAsync(manifest),
|
|
774
|
+
]);
|
|
775
|
+
return {
|
|
776
|
+
manifest: manifestStamp,
|
|
777
|
+
tasks: tasksStamp,
|
|
778
|
+
agents: agentsStamp,
|
|
779
|
+
events: eventsStampValue,
|
|
780
|
+
mailbox,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
|
|
785
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
786
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
787
|
+
const recentEventsLimit = options.recentEvents ?? DEFAULT_RECENT_EVENTS;
|
|
788
|
+
const recentOutputLimit = options.recentOutputLines ?? DEFAULT_RECENT_OUTPUT_LINES;
|
|
789
|
+
const entries = new Map<string, CacheEntry>();
|
|
790
|
+
|
|
791
|
+
function touch(runId: string, entry: CacheEntry): RunUiSnapshot {
|
|
792
|
+
entry.lastAccessMs = Date.now();
|
|
793
|
+
if (entries.has(runId)) {
|
|
794
|
+
entries.delete(runId);
|
|
795
|
+
entries.set(runId, entry);
|
|
796
|
+
}
|
|
797
|
+
return entry.snapshot;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function evictIfNeeded(): void {
|
|
801
|
+
while (entries.size > maxEntries) {
|
|
802
|
+
// Map iteration respects insertion order, so the first inactive entry
|
|
803
|
+
// we see IS the oldest inactive entry — no Array.from(...).sort()
|
|
804
|
+
// allocation, no copy of the entries map.
|
|
805
|
+
let key: string | undefined;
|
|
806
|
+
for (const [k, entry] of entries) {
|
|
807
|
+
if (!isActiveRunStatus(entry.snapshot.manifest.status)) {
|
|
808
|
+
key = k;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (!key) key = entries.keys().next().value;
|
|
813
|
+
if (!key) break;
|
|
814
|
+
entries.delete(key);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function build(runId: string, previous?: CacheEntry): CacheEntry {
|
|
819
|
+
let loaded: ReturnType<typeof loadRunManifestById>;
|
|
820
|
+
try {
|
|
821
|
+
loaded = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
822
|
+
} catch {
|
|
823
|
+
if (previous) return previous;
|
|
824
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
825
|
+
}
|
|
826
|
+
if (!loaded) {
|
|
827
|
+
if (previous) return previous;
|
|
828
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
829
|
+
}
|
|
830
|
+
let tasks: TeamTaskState[];
|
|
831
|
+
let agents: CrewAgentRecord[];
|
|
832
|
+
try {
|
|
833
|
+
tasks = readTasks(loaded.manifest.tasksPath);
|
|
834
|
+
agents = readCrewAgents(loaded.manifest);
|
|
835
|
+
} catch {
|
|
836
|
+
if (previous) return previous;
|
|
837
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
838
|
+
}
|
|
839
|
+
const mailbox = mailboxFrom(loaded.manifest, agents);
|
|
840
|
+
const groupJoins = groupJoinsFrom(loaded.manifest);
|
|
841
|
+
const recentEvents = safeRecentEvents(loaded.manifest.eventsPath, recentEventsLimit);
|
|
842
|
+
const base = {
|
|
843
|
+
runId: loaded.manifest.runId,
|
|
844
|
+
cwd: loaded.manifest.cwd,
|
|
845
|
+
manifest: loaded.manifest,
|
|
846
|
+
tasks,
|
|
847
|
+
agents,
|
|
848
|
+
progress: progressFromTasks(tasks),
|
|
849
|
+
usage: usageFrom(tasks, agents),
|
|
850
|
+
mailbox,
|
|
851
|
+
groupJoins,
|
|
852
|
+
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
853
|
+
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
854
|
+
recentEvents,
|
|
855
|
+
recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
|
|
856
|
+
};
|
|
857
|
+
const stamps = stampsFor(loaded.manifest, agents);
|
|
858
|
+
const sliceSignatures = computeSliceSignatures(base);
|
|
859
|
+
const snapshot: RunUiSnapshot = {
|
|
860
|
+
...base,
|
|
861
|
+
fetchedAt: Date.now(),
|
|
862
|
+
signature: signatureFor(base, stamps, sliceSignatures),
|
|
863
|
+
sliceSignatures,
|
|
864
|
+
};
|
|
865
|
+
return {
|
|
866
|
+
snapshot,
|
|
867
|
+
stamps,
|
|
868
|
+
loadedAtMs: snapshot.fetchedAt,
|
|
869
|
+
lastAccessMs: snapshot.fetchedAt,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
|
|
874
|
+
let loaded: Awaited<ReturnType<typeof loadRunManifestByIdAsync>>;
|
|
875
|
+
try {
|
|
876
|
+
loaded = await loadRunManifestByIdAsync(cwd, runId);
|
|
877
|
+
} catch {
|
|
878
|
+
if (previous) return previous;
|
|
879
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
880
|
+
}
|
|
881
|
+
if (!loaded) {
|
|
882
|
+
if (previous) return previous;
|
|
883
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
884
|
+
}
|
|
885
|
+
let tasks: TeamTaskState[];
|
|
886
|
+
let agents: CrewAgentRecord[];
|
|
887
|
+
try {
|
|
888
|
+
tasks = loaded.tasks;
|
|
889
|
+
agents = await readCrewAgentsAsync(loaded.manifest);
|
|
890
|
+
} catch {
|
|
891
|
+
if (previous) return previous;
|
|
892
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
893
|
+
}
|
|
894
|
+
const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
|
|
895
|
+
mailboxFromAsync(loaded.manifest, agents),
|
|
896
|
+
groupJoinsFromAsync(loaded.manifest),
|
|
897
|
+
safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
|
|
898
|
+
recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
|
|
899
|
+
]);
|
|
900
|
+
const base = {
|
|
901
|
+
runId: loaded.manifest.runId,
|
|
902
|
+
cwd: loaded.manifest.cwd,
|
|
903
|
+
manifest: loaded.manifest,
|
|
904
|
+
tasks,
|
|
905
|
+
agents,
|
|
906
|
+
progress: progressFromTasks(tasks),
|
|
907
|
+
usage: usageFrom(tasks, agents),
|
|
908
|
+
mailbox,
|
|
909
|
+
groupJoins,
|
|
910
|
+
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
911
|
+
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
912
|
+
recentEvents,
|
|
913
|
+
recentOutputLines: recentOutput,
|
|
914
|
+
};
|
|
915
|
+
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
916
|
+
const sliceSignatures = computeSliceSignatures(base);
|
|
917
|
+
const snapshot: RunUiSnapshot = {
|
|
918
|
+
...base,
|
|
919
|
+
fetchedAt: Date.now(),
|
|
920
|
+
signature: signatureFor(base, stamps, sliceSignatures),
|
|
921
|
+
sliceSignatures,
|
|
922
|
+
};
|
|
923
|
+
return {
|
|
924
|
+
snapshot,
|
|
925
|
+
stamps,
|
|
926
|
+
loadedAtMs: snapshot.fetchedAt,
|
|
927
|
+
lastAccessMs: snapshot.fetchedAt,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function currentStamps(previous: CacheEntry): SnapshotStamps {
|
|
932
|
+
const manifest = previous.snapshot.manifest;
|
|
933
|
+
return {
|
|
934
|
+
manifest: stampFile(path.join(manifest.stateRoot, "manifest.json")),
|
|
935
|
+
tasks: stampFile(manifest.tasksPath),
|
|
936
|
+
agents: stampFile(agentsPath(manifest)),
|
|
937
|
+
events: eventsStamp(manifest.eventsPath),
|
|
938
|
+
mailbox: mailboxStamp(manifest),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
|
|
943
|
+
return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
|
|
947
|
+
const previous = entries.get(runId);
|
|
948
|
+
const now = Date.now();
|
|
949
|
+
// Fresh enough? Return immediately
|
|
950
|
+
if (previous && now - previous.loadedAtMs < ttlMs) {
|
|
951
|
+
return touch(runId, previous);
|
|
952
|
+
}
|
|
953
|
+
// Check stamps async
|
|
954
|
+
if (previous) {
|
|
955
|
+
const stamps = await currentStampsAsync(previous);
|
|
956
|
+
if (sameStamps(stamps, previous.stamps)) {
|
|
957
|
+
previous.loadedAtMs = now;
|
|
958
|
+
return touch(runId, previous);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// Full async build
|
|
962
|
+
const entry = await buildAsync(runId, previous);
|
|
963
|
+
entries.set(runId, entry);
|
|
964
|
+
evictIfNeeded();
|
|
965
|
+
return entry.snapshot;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function preloadAllStale(runIds: string[]): Promise<void> {
|
|
969
|
+
const batchSize = 4;
|
|
970
|
+
for (let i = 0; i < runIds.length; i += batchSize) {
|
|
971
|
+
const batch = runIds.slice(i, i + batchSize);
|
|
972
|
+
await Promise.all(batch.map((id) => preloadStale(id)));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Coalesced eager refresh on event-bus signals. Previously every
|
|
977
|
+
// `run:state` / `worker:lifecycle` event deleted the cache entry, leaving
|
|
978
|
+
// a window where `widget-model.ts: snapshotCache.get(runId)` returned
|
|
979
|
+
// `undefined`. The widget then fell back to `agentsFor(run)` (a disk read
|
|
980
|
+
// with no snapshot.tasks) and rendered the "0/1 done" branch of
|
|
981
|
+
// `widget-renderer.ts:39-41` instead of the "Phase 1/1 default: 0% (0/3)"
|
|
982
|
+
// branch — producing the live flicker between those two progressPart
|
|
983
|
+
// values every render tick. Replacing the delete with a coalesced
|
|
984
|
+
// refreshIfStale keeps the cache populated so the widget always sees the
|
|
985
|
+
// same logical snapshot between stamp changes; multiple events for the
|
|
986
|
+
// same runId within INVAL_COALESCE_MS are batched into one refresh.
|
|
987
|
+
function localRefresh(runId: string): RunUiSnapshot {
|
|
988
|
+
const previous = entries.get(runId);
|
|
989
|
+
const entry = build(runId, previous);
|
|
990
|
+
entries.set(runId, entry);
|
|
991
|
+
evictIfNeeded();
|
|
992
|
+
return entry.snapshot;
|
|
993
|
+
}
|
|
994
|
+
function localRefreshIfStale(runId: string): RunUiSnapshot {
|
|
995
|
+
const previous = entries.get(runId);
|
|
996
|
+
if (!previous) return localRefresh(runId);
|
|
997
|
+
const now = Date.now();
|
|
998
|
+
if (now - previous.loadedAtMs < ttlMs) return touch(runId, previous);
|
|
999
|
+
const stamps = currentStamps(previous);
|
|
1000
|
+
if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
|
|
1001
|
+
return localRefresh(runId);
|
|
1002
|
+
}
|
|
1003
|
+
const pendingRefreshes = new Map<string, ReturnType<typeof setTimeout>>();
|
|
1004
|
+
const INVAL_COALESCE_MS = 80;
|
|
1005
|
+
const scheduleRefresh = (runId: string): void => {
|
|
1006
|
+
const existing = pendingRefreshes.get(runId);
|
|
1007
|
+
if (existing) clearTimeout(existing);
|
|
1008
|
+
const timer = setTimeout(() => {
|
|
1009
|
+
pendingRefreshes.delete(runId);
|
|
1010
|
+
try {
|
|
1011
|
+
localRefreshIfStale(runId);
|
|
1012
|
+
} catch {
|
|
1013
|
+
/* best-effort; widget falls back gracefully */
|
|
1014
|
+
}
|
|
1015
|
+
}, INVAL_COALESCE_MS);
|
|
1016
|
+
timer.unref();
|
|
1017
|
+
pendingRefreshes.set(runId, timer);
|
|
1018
|
+
};
|
|
1019
|
+
const unsubState = runEventBus.onChannel("run:state", (event) => {
|
|
1020
|
+
if (entries.has(event.runId)) scheduleRefresh(event.runId);
|
|
1021
|
+
});
|
|
1022
|
+
const unsubLifecycle = runEventBus.onChannel("worker:lifecycle", (event) => {
|
|
1023
|
+
if (entries.has(event.runId)) scheduleRefresh(event.runId);
|
|
1024
|
+
});
|
|
1025
|
+
const unsubscribe = () => {
|
|
1026
|
+
unsubState();
|
|
1027
|
+
unsubLifecycle();
|
|
1028
|
+
for (const timer of pendingRefreshes.values()) clearTimeout(timer);
|
|
1029
|
+
pendingRefreshes.clear();
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
get(runId: string): RunUiSnapshot | undefined {
|
|
1034
|
+
const entry = entries.get(runId);
|
|
1035
|
+
return entry ? touch(runId, entry) : undefined;
|
|
1036
|
+
},
|
|
1037
|
+
refresh(runId: string): RunUiSnapshot {
|
|
1038
|
+
return localRefresh(runId);
|
|
1039
|
+
},
|
|
1040
|
+
refreshIfStale(runId: string): RunUiSnapshot {
|
|
1041
|
+
return localRefreshIfStale(runId);
|
|
1042
|
+
},
|
|
1043
|
+
preloadStale,
|
|
1044
|
+
preloadAllStale,
|
|
1045
|
+
invalidate(runId?: string): void {
|
|
1046
|
+
if (runId) entries.delete(runId);
|
|
1047
|
+
else entries.clear();
|
|
1048
|
+
},
|
|
1049
|
+
snapshotsByKey(): Map<string, RunUiSnapshot> {
|
|
1050
|
+
return new Map([...entries.entries()].map(([key, entry]) => [key, entry.snapshot]));
|
|
1051
|
+
},
|
|
1052
|
+
dispose(): void {
|
|
1053
|
+
unsubscribe();
|
|
1054
|
+
entries.clear();
|
|
1055
|
+
},
|
|
1056
|
+
};
|
|
1057
|
+
}
|