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,923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle handler installer for pi-crew.
|
|
3
|
+
*
|
|
4
|
+
* Owns:
|
|
5
|
+
* • `session_start` — heavy setup (config, scheduler, render loop, watchers,
|
|
6
|
+
* deferred crash recovery). This is the bulk of the orchestrator's
|
|
7
|
+
* per-session work, extracted here so `register.ts` stays thin.
|
|
8
|
+
* • `session_shutdown` — reason-aware cleanup (quit/reload aborts
|
|
9
|
+
* foreground runs; resume/new/fork preserves them).
|
|
10
|
+
* • `session_before_switch` — graceful session switch handoff.
|
|
11
|
+
*
|
|
12
|
+
* Imports here are kept top-level (non-lazy) on purpose: this module IS
|
|
13
|
+
* where the heavy work happens, so there is no cold-start benefit to
|
|
14
|
+
* deferring it. The session_start handler internally uses lazy imports
|
|
15
|
+
* for its own per-call optional work (foreground-watchdog, atomic-write).
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { loadConfig } from "../../config/config.ts";
|
|
21
|
+
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
22
|
+
import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
|
|
23
|
+
import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker-issuer.ts";
|
|
24
|
+
import { reconcileAllStaleRuns } from "../../runtime/crash-recovery.ts";
|
|
25
|
+
import { CrewBroker } from "../../runtime/crew-broker.ts";
|
|
26
|
+
import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
|
|
27
|
+
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
28
|
+
import { cleanupOrphanWorkers } from "../../runtime/orphan-worker-registry.ts";
|
|
29
|
+
import { cleanupLegacyOrphanTempDirs, cleanupOrphanTempDirs, currentCrewDepth } from "../../runtime/pi-args.ts";
|
|
30
|
+
import { CrewScheduler, type ScheduledJob } from "../../runtime/scheduler.ts";
|
|
31
|
+
import { tryRegisterSessionCleanup } from "../../runtime/session-resources.ts";
|
|
32
|
+
import { createSessionSnapshot } from "../../runtime/session-snapshot.ts";
|
|
33
|
+
import { applyCrewSettingsToConfig, loadCrewSettings } from "../../runtime/settings-store.ts";
|
|
34
|
+
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
35
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
36
|
+
import { terminateActiveChildPiProcesses } from "../../subagents/spawn.ts";
|
|
37
|
+
import { summarizeHeartbeats } from "../../ui/heartbeat-aggregator.ts";
|
|
38
|
+
import { requestRender, setExtensionWidget } from "../../ui/pi-ui-compat.ts";
|
|
39
|
+
import {
|
|
40
|
+
registerPiCrewPowerbarSegments,
|
|
41
|
+
requestPowerbarUpdate,
|
|
42
|
+
resetPowerbarDedupState,
|
|
43
|
+
updatePiCrewPowerbar,
|
|
44
|
+
} from "../../ui/powerbar-publisher.ts";
|
|
45
|
+
import { RenderScheduler } from "../../ui/render-scheduler.ts";
|
|
46
|
+
import { runEventBus } from "../../ui/run-event-bus.ts";
|
|
47
|
+
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
48
|
+
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
49
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
50
|
+
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
51
|
+
import { RunWatcherRegistry } from "../../utils/run-watcher-registry.ts";
|
|
52
|
+
import { extractBrokerSessionId } from "../../utils/session-utils.ts";
|
|
53
|
+
import { getBrokerSocketPath } from "../../utils/socket-path.ts";
|
|
54
|
+
import { startAsyncRunNotifier, stopAsyncRunNotifier } from "../async-notifier.ts";
|
|
55
|
+
import { registerCrewAutocomplete } from "../crew-autocomplete.ts";
|
|
56
|
+
import { notifyActiveRuns } from "../session-summary.ts";
|
|
57
|
+
import { persistScheduledJobUpdate } from "../team-tool/handle-schedule.ts";
|
|
58
|
+
import { handleTeamTool } from "../team-tool.ts";
|
|
59
|
+
import { runArtifactCleanup } from "./artifact-cleanup.ts";
|
|
60
|
+
import type { RegistrationContext } from "./registration-types.ts";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register all session-lifecycle handlers on the ExtensionAPI. The caller
|
|
64
|
+
* (`register.ts`) must have already wired the orchestrator-side cleanup
|
|
65
|
+
* functions into `ctx.cleanupRuntime` and `ctx.cleanupSessionResourcesOnly`.
|
|
66
|
+
*/
|
|
67
|
+
export function installSessionLifecycleHandlers(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
68
|
+
installSessionShutdownHandler(pi, ctx);
|
|
69
|
+
installSessionStartHandler(pi, ctx);
|
|
70
|
+
installSessionBeforeSwitchHandler(pi, ctx);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* session_shutdown:
|
|
75
|
+
* • reason="quit" / "reload" → full cleanup (abort foreground runs).
|
|
76
|
+
* • reason="resume" / "new" / "fork" → resource cleanup only (preserve
|
|
77
|
+
* foreground runs; they share the process with the session).
|
|
78
|
+
*/
|
|
79
|
+
function installSessionShutdownHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
80
|
+
pi.on("session_shutdown", (event) => {
|
|
81
|
+
const reason = typeof event === "object" && event !== null && "reason" in event ? (event as { reason: string }).reason : undefined;
|
|
82
|
+
if (reason === "quit" || reason === "reload") {
|
|
83
|
+
// Actual shutdown — abort foreground runs and cleanup everything
|
|
84
|
+
ctx.cleanupRuntime();
|
|
85
|
+
} else {
|
|
86
|
+
// Session switch (resume/new/fork) — cleanup resources but preserve foreground runs
|
|
87
|
+
ctx.cleanupSessionResourcesOnly();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* session_before_switch:
|
|
94
|
+
* Bump generation, deactivate delivery coordinator, stop async notifier,
|
|
95
|
+
* abort session-bound subagents. Foreground team runs are NOT aborted here.
|
|
96
|
+
*/
|
|
97
|
+
function installSessionBeforeSwitchHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
98
|
+
pi.on("session_before_switch", () => {
|
|
99
|
+
ctx.sessionGeneration++;
|
|
100
|
+
const pendingCount = ctx.lifecycleState.deliveryCoordinator?.getPendingCount() ?? 0;
|
|
101
|
+
try {
|
|
102
|
+
const activeRuns = ctx.currentCtx
|
|
103
|
+
? ctx
|
|
104
|
+
.getManifestCache(ctx.currentCtx.cwd)
|
|
105
|
+
.list(50)
|
|
106
|
+
.filter((run) => run.status === "running" || run.status === "queued" || run.status === "blocked")
|
|
107
|
+
: [];
|
|
108
|
+
const snapshot = createSessionSnapshot(activeRuns, pendingCount, ctx.sessionGeneration);
|
|
109
|
+
if (pendingCount > 0 || snapshot.activeRunIds.length > 0)
|
|
110
|
+
logInternalError("register.session-before-switch", undefined, JSON.stringify(snapshot));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logInternalError("register.session-before-switch.snapshot", error);
|
|
113
|
+
}
|
|
114
|
+
if (pendingCount > 0) {
|
|
115
|
+
logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
|
|
116
|
+
}
|
|
117
|
+
ctx.lifecycleState.deliveryCoordinator?.deactivate();
|
|
118
|
+
resetPowerbarDedupState();
|
|
119
|
+
stopAsyncRunNotifier(ctx.notifierState);
|
|
120
|
+
ctx.stopSessionBoundSubagents();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* session_start — the bulk of pi-crew's per-session work.
|
|
126
|
+
*
|
|
127
|
+
* Pipeline:
|
|
128
|
+
* 1. Resolve session metadata + restore brief mode (best-effort).
|
|
129
|
+
* 2. Bump generation, set currentCtx, register autocomplete (once).
|
|
130
|
+
* 3. Schedule deferred crash recovery (orphan cleanup, stale-reconcile,
|
|
131
|
+
* auto-prune). MUST run in setTimeout(0) — these block 100ms-1s on
|
|
132
|
+
* Windows and cannot stall the session_start event.
|
|
133
|
+
* 4. Synchronously: load config + crew settings, start CrewScheduler,
|
|
134
|
+
* configure notifications/observability/delivery-coordinator,
|
|
135
|
+
* register Pi-side powerbar segments, start async notifier,
|
|
136
|
+
* kick off the render scheduler + preload loop + bounded watchers.
|
|
137
|
+
*/
|
|
138
|
+
function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
139
|
+
pi.on("session_start", (_event, extensionCtx) => {
|
|
140
|
+
runArtifactCleanup(extensionCtx.cwd);
|
|
141
|
+
|
|
142
|
+
// Restore brief mode state from session entries (best-effort).
|
|
143
|
+
try {
|
|
144
|
+
const entries = extensionCtx.sessionManager?.getEntries?.();
|
|
145
|
+
if (entries) {
|
|
146
|
+
// LAZY: brief-mode is only used inside the session-start restore path.
|
|
147
|
+
import("../../ui/tool-renderers/brief-mode.ts")
|
|
148
|
+
.then(({ restoreBriefState }) => {
|
|
149
|
+
restoreBriefState(entries);
|
|
150
|
+
})
|
|
151
|
+
.catch(() => {
|
|
152
|
+
/* non-critical */
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
/* non-critical */
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ctx.cleanedUp = false;
|
|
160
|
+
ctx.sessionGeneration++;
|
|
161
|
+
const ownerGeneration = ctx.sessionGeneration;
|
|
162
|
+
ctx.currentCtx = extensionCtx;
|
|
163
|
+
// Round 13 UX: register the crew natural-language autocomplete provider
|
|
164
|
+
// once we have a UI context. Guarded so repeated session_start events
|
|
165
|
+
// don't stack wrappers (each wrapper delegates, but stacking wastes
|
|
166
|
+
// call depth).
|
|
167
|
+
if (!ctx.crewAutocompleteRegistered) {
|
|
168
|
+
ctx.crewAutocompleteRegistered = true;
|
|
169
|
+
registerCrewAutocomplete(extensionCtx);
|
|
170
|
+
}
|
|
171
|
+
notifyActiveRuns(extensionCtx);
|
|
172
|
+
|
|
173
|
+
const currentSessionId = extractBrokerSessionId(extensionCtx);
|
|
174
|
+
// Phase 0 broker: feed the captured session_id to the controller so
|
|
175
|
+
// it can issue tokens for child runs in this session. The controller
|
|
176
|
+
// already gates by flag + root-session; this is a no-op when disabled.
|
|
177
|
+
ctx.brokerController?.setSessionId(currentSessionId);
|
|
178
|
+
|
|
179
|
+
// Defer ALL heavy cleanup to after the session_start handler returns.
|
|
180
|
+
// These operations involve synchronous directory scanning (readdirSync, readFileSync)
|
|
181
|
+
// which can take 100ms–1s+ on Windows. They MUST NOT block the session_start event.
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
void runDeferredSessionCleanup(pi, ctx, ownerGeneration, currentSessionId, extensionCtx);
|
|
184
|
+
}, 0);
|
|
185
|
+
|
|
186
|
+
const loadedConfig = loadConfig(extensionCtx.cwd);
|
|
187
|
+
const crewSettings = loadCrewSettings(extensionCtx.cwd);
|
|
188
|
+
applyCrewSettingsToConfig(loadedConfig.config, crewSettings);
|
|
189
|
+
|
|
190
|
+
// Start scheduler with event-based executor
|
|
191
|
+
const sessionId =
|
|
192
|
+
extensionCtx.sessionManager?.getSessionId?.() ??
|
|
193
|
+
(typeof extensionCtx === "object" && extensionCtx !== null && "sessionId" in extensionCtx
|
|
194
|
+
? (extensionCtx as Record<string, unknown>).sessionId
|
|
195
|
+
: undefined);
|
|
196
|
+
ctx.crewScheduler = setupCrewScheduler(pi, ctx, extensionCtx, sessionId);
|
|
197
|
+
|
|
198
|
+
// Wire scheduler into handle-schedule.ts so handlers can add/list jobs.
|
|
199
|
+
// Uses a global symbol so the module doesn't need a direct circular import.
|
|
200
|
+
(globalThis as Record<symbol | string, unknown>)[Symbol.for("pi-crew:scheduler")] = ctx.crewScheduler;
|
|
201
|
+
// Load scheduled jobs from settings if present
|
|
202
|
+
if (Array.isArray(crewSettings.scheduledJobs)) {
|
|
203
|
+
for (const job of crewSettings.scheduledJobs) {
|
|
204
|
+
try {
|
|
205
|
+
ctx.crewScheduler.add(job as ScheduledJob);
|
|
206
|
+
} catch {
|
|
207
|
+
/* skip invalid */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
ctx.autoRecoveryLast.clear();
|
|
212
|
+
ctx.configureNotifications(extensionCtx);
|
|
213
|
+
ctx.configureObservability(extensionCtx);
|
|
214
|
+
ctx.configureDeliveryCoordinator();
|
|
215
|
+
if (typeof sessionId === "string" && sessionId) ctx.lifecycleState.deliveryCoordinator?.activate(sessionId);
|
|
216
|
+
tryRegisterSessionCleanup(pi, () => {
|
|
217
|
+
terminateActiveChildPiProcesses();
|
|
218
|
+
ctx.cleanupRuntime();
|
|
219
|
+
});
|
|
220
|
+
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
221
|
+
startAsyncRunNotifier(extensionCtx, ctx.notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, {
|
|
222
|
+
generation: ownerGeneration,
|
|
223
|
+
isCurrent: (generation) => generation === ctx.sessionGeneration && ctx.currentCtx === extensionCtx && !ctx.cleanedUp,
|
|
224
|
+
});
|
|
225
|
+
const cache = ctx.getManifestCache(extensionCtx.cwd);
|
|
226
|
+
updateCrewWidget(extensionCtx, ctx.widgetState, loadedConfig.config.ui, cache, ctx.getRunSnapshotCache(extensionCtx.cwd));
|
|
227
|
+
updatePiCrewPowerbar(
|
|
228
|
+
pi.events,
|
|
229
|
+
extensionCtx.cwd,
|
|
230
|
+
loadedConfig.config.ui,
|
|
231
|
+
cache,
|
|
232
|
+
ctx.getRunSnapshotCache(extensionCtx.cwd),
|
|
233
|
+
extensionCtx,
|
|
234
|
+
ctx.widgetState.notificationCount ?? 0,
|
|
235
|
+
);
|
|
236
|
+
setupRenderLoop(pi, ctx, extensionCtx, loadedConfig);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Heavy cleanup that runs after session_start returns.
|
|
242
|
+
*
|
|
243
|
+
* Wrapped in setTimeout(0) so the session_start event is not blocked by
|
|
244
|
+
* the synchronous I/O involved (readdirSync, readFileSync) — observed to
|
|
245
|
+
* take 100ms-1s+ on Windows with many runs on disk.
|
|
246
|
+
*/
|
|
247
|
+
async function runDeferredSessionCleanup(
|
|
248
|
+
pi: ExtensionAPI,
|
|
249
|
+
ctx: RegistrationContext,
|
|
250
|
+
ownerGeneration: number,
|
|
251
|
+
currentSessionId: string | undefined,
|
|
252
|
+
extensionCtx: ExtensionContext,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return; // session switched while we waited
|
|
255
|
+
|
|
256
|
+
// 2.7: load crash-recovery lazily once per session_start cleanup batch.
|
|
257
|
+
let crashRecovery: Awaited<ReturnType<typeof ctx.importCrashRecovery>> | undefined;
|
|
258
|
+
try {
|
|
259
|
+
crashRecovery = await ctx.importCrashRecovery();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logInternalError("register.sessionStart.lazyCrashRecovery", error);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return;
|
|
265
|
+
const { cancelOrphanedRuns: cancelOrphanedRunsFn, purgeStaleActiveRunIndex: purgeStaleActiveRunIndexFn } = crashRecovery;
|
|
266
|
+
|
|
267
|
+
// Auto-cancel orphaned runs
|
|
268
|
+
if (currentSessionId) {
|
|
269
|
+
try {
|
|
270
|
+
const { cancelled } = (
|
|
271
|
+
cancelOrphanedRunsFn as (
|
|
272
|
+
cwd: string,
|
|
273
|
+
cache: ReturnType<typeof createManifestCache>,
|
|
274
|
+
sessionId: string,
|
|
275
|
+
) => { cancelled: string[] }
|
|
276
|
+
)(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd), currentSessionId);
|
|
277
|
+
if (cancelled.length > 0) {
|
|
278
|
+
ctx.notifyOperator({
|
|
279
|
+
id: `orphan_cleanup`,
|
|
280
|
+
severity: "info",
|
|
281
|
+
source: "crash-recovery",
|
|
282
|
+
title: `Cleaned up ${cancelled.length} orphaned run(s)`,
|
|
283
|
+
body: `Runs from previous sessions were auto-cancelled: ${cancelled.join(", ")}`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logInternalError("register.sessionStart.orphanCleanup", error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Startup cleanup (Fix A): run orphan-temp-dir cleanup
|
|
292
|
+
try {
|
|
293
|
+
const orphanTmp = cleanupOrphanTempDirs();
|
|
294
|
+
const legacyTmp = cleanupLegacyOrphanTempDirs();
|
|
295
|
+
if (orphanTmp.cleaned > 0 || legacyTmp.cleaned > 0) {
|
|
296
|
+
ctx.notifyOperator({
|
|
297
|
+
id: `startup_temp_cleanup_${Date.now()}`,
|
|
298
|
+
severity: "info",
|
|
299
|
+
source: "temp-cleanup",
|
|
300
|
+
title: `Startup cleanup: removed ${orphanTmp.cleaned + legacyTmp.cleaned} orphan temp dir(s)`,
|
|
301
|
+
body: `${orphanTmp.cleaned} from ~/.pi/agent/pi-crew/tmp/ + ${legacyTmp.cleaned} legacy /tmp/pi-crew-*`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logInternalError("register.sessionStart.startupTempCleanup", error);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Orphan worker cleanup (Fix B): kill stale background-runner processes
|
|
309
|
+
try {
|
|
310
|
+
const orphanWorkers = cleanupOrphanWorkers(currentSessionId);
|
|
311
|
+
if (orphanWorkers.killed > 0) {
|
|
312
|
+
ctx.notifyOperator({
|
|
313
|
+
id: `orphan_workers_cleanup`,
|
|
314
|
+
severity: "info",
|
|
315
|
+
source: "worker-cleanup",
|
|
316
|
+
title: `Cleaned up ${orphanWorkers.killed} orphan worker(s)`,
|
|
317
|
+
body: `Background workers from previous (SIGKILL'd) sessions were terminated (pruned ${orphanWorkers.pruned} dead, kept ${orphanWorkers.kept}).`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logInternalError("register.sessionStart.orphanWorkers", error);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Global purge of stale active-run-index entries
|
|
325
|
+
try {
|
|
326
|
+
const { purged } = purgeStaleActiveRunIndexFn();
|
|
327
|
+
if (purged.length > 0) {
|
|
328
|
+
ctx.notifyOperator({
|
|
329
|
+
id: `active_index_purge`,
|
|
330
|
+
severity: "info",
|
|
331
|
+
source: "crash-recovery",
|
|
332
|
+
title: `Purged ${purged.length} stale active-run-index entr${purged.length === 1 ? "y" : "ies"}`,
|
|
333
|
+
body: `Cleaned up global active run index`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
logInternalError("register.sessionStart.globalIndexPurge", error);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Reconcile stale runs found on disk
|
|
341
|
+
try {
|
|
342
|
+
const staleResults = reconcileAllStaleRuns(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd)) ?? [];
|
|
343
|
+
if (staleResults.length > 0) {
|
|
344
|
+
ctx.notifyOperator({
|
|
345
|
+
id: "stale_reconcile",
|
|
346
|
+
severity: "info",
|
|
347
|
+
source: "crash-recovery",
|
|
348
|
+
title: `Reconciled ${staleResults.length} stale run(s)`,
|
|
349
|
+
body: `Found and repaired ghost runs from previous sessions: ${staleResults.map((r) => r.runId).join(", ")}`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
logInternalError("register.sessionStart.reconcileStale", error);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Auto-prune finished project-level run directories
|
|
357
|
+
try {
|
|
358
|
+
const { removed } = pruneFinishedRuns(extensionCtx.cwd, 10);
|
|
359
|
+
if (removed.length > 0) {
|
|
360
|
+
ctx.notifyOperator({
|
|
361
|
+
id: `auto_prune_project`,
|
|
362
|
+
severity: "info",
|
|
363
|
+
source: "run-maintenance",
|
|
364
|
+
title: `Auto-pruned ${removed.length} finished project run(s)`,
|
|
365
|
+
body: `Removed old finished runs: ${removed.join(", ")}`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
logInternalError("register.sessionStart.autoPruneProject", error);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Auto-prune finished user-level run directories
|
|
373
|
+
try {
|
|
374
|
+
const { removed } = pruneUserLevelRuns(10);
|
|
375
|
+
if (removed.length > 0) {
|
|
376
|
+
ctx.notifyOperator({
|
|
377
|
+
id: `auto_prune_user`,
|
|
378
|
+
severity: "info",
|
|
379
|
+
source: "run-maintenance",
|
|
380
|
+
title: `Auto-pruned ${removed.length} finished user-level run(s)`,
|
|
381
|
+
body: `Removed old finished runs: ${removed.join(", ")}`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logInternalError("register.sessionStart.autoPruneUser", error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build a CrewScheduler wired to the current session. The scheduler's
|
|
391
|
+
* executor closure invokes handleTeamTool lazily — the heavy team-tool
|
|
392
|
+
* import only fires when a scheduled job actually runs.
|
|
393
|
+
*/
|
|
394
|
+
function setupCrewScheduler(
|
|
395
|
+
pi: ExtensionAPI,
|
|
396
|
+
ctx: RegistrationContext,
|
|
397
|
+
extensionCtx: ExtensionContext,
|
|
398
|
+
sessionId: string | undefined,
|
|
399
|
+
): CrewScheduler {
|
|
400
|
+
const crewScheduler = new CrewScheduler();
|
|
401
|
+
crewScheduler.start({
|
|
402
|
+
emit: (event) => {
|
|
403
|
+
if (ctx.cleanedUp) return;
|
|
404
|
+
pi.events?.emit?.("crew-scheduler", event);
|
|
405
|
+
},
|
|
406
|
+
executor: (job) => {
|
|
407
|
+
let runParams: { action: string; team: string; goal: string };
|
|
408
|
+
try {
|
|
409
|
+
runParams = JSON.parse(job.prompt);
|
|
410
|
+
} catch {
|
|
411
|
+
runParams = {
|
|
412
|
+
action: "run",
|
|
413
|
+
team: "default",
|
|
414
|
+
goal: job.prompt,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (runParams.action !== "run") return `scheduled-${job.id}-${Date.now()}`;
|
|
418
|
+
const agentId = `scheduled-${job.id}-${Date.now()}`;
|
|
419
|
+
setImmediate(async () => {
|
|
420
|
+
try {
|
|
421
|
+
const runResult = await handleTeamTool(
|
|
422
|
+
{
|
|
423
|
+
action: "run",
|
|
424
|
+
team: runParams.team,
|
|
425
|
+
goal: runParams.goal,
|
|
426
|
+
async: true,
|
|
427
|
+
},
|
|
428
|
+
{ cwd: extensionCtx.cwd, sessionId },
|
|
429
|
+
);
|
|
430
|
+
const runId = runResult?.details?.runId;
|
|
431
|
+
if (runId && typeof runId === "string") {
|
|
432
|
+
crewScheduler?.recordSpawnedRun(job.id, runId);
|
|
433
|
+
// Update run manifest with scheduler provenance for traceability
|
|
434
|
+
try {
|
|
435
|
+
const cwd = extensionCtx.cwd ?? process.cwd();
|
|
436
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
437
|
+
if (loaded) {
|
|
438
|
+
// LAZY: defer dynamic import of atomic-write.ts to its call site.
|
|
439
|
+
const { atomicWriteJson } = await import("../../state/atomic-write.ts");
|
|
440
|
+
atomicWriteJson(loaded.manifest.stateRoot + "/manifest.json", {
|
|
441
|
+
...loaded.manifest,
|
|
442
|
+
schedulerJobId: job.id,
|
|
443
|
+
schedulerName: job.name,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
/* best-effort provenance tracking */
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const updatedJob = crewScheduler?.list().find((j) => j.id === job.id);
|
|
452
|
+
if (updatedJob) persistScheduledJobUpdate(extensionCtx.cwd, updatedJob);
|
|
453
|
+
} catch {
|
|
454
|
+
/* best-effort */
|
|
455
|
+
}
|
|
456
|
+
crewScheduler?.update(job.id, {
|
|
457
|
+
runCount: job.runCount + 1,
|
|
458
|
+
lastRun: new Date().toISOString(),
|
|
459
|
+
lastStatus: "success",
|
|
460
|
+
});
|
|
461
|
+
} catch (err) {
|
|
462
|
+
logInternalError("scheduler.execute", err);
|
|
463
|
+
crewScheduler?.update(job.id, { lastStatus: "error" });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return agentId;
|
|
467
|
+
},
|
|
468
|
+
finalizer: () => {},
|
|
469
|
+
runCancelFn: (runId: string) => {
|
|
470
|
+
try {
|
|
471
|
+
handleTeamTool({ action: "cancel", runId, confirm: true }, { cwd: extensionCtx.cwd, sessionId }).catch((err) =>
|
|
472
|
+
logInternalError("scheduler.runCancelFn", err, `runId=${runId}`),
|
|
473
|
+
);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
logInternalError("scheduler.runCancelFn.sync", err, `runId=${runId}`);
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
return crewScheduler;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Build the render scheduler + preload loop + bounded run watchers.
|
|
484
|
+
*
|
|
485
|
+
* Render path:
|
|
486
|
+
* - RenderScheduler fires renderTick() every `effectiveRefreshMs()`.
|
|
487
|
+
* - 160ms when live agents OR background runs are active (spinner-friendly),
|
|
488
|
+
* else the configured `dashboardLiveRefreshMs` (default DEFAULT_UI.refreshMs).
|
|
489
|
+
* - renderTick reads from a pre-computed frame (`lastPreloadedManifests`) —
|
|
490
|
+
* zero fs I/O on the hot path.
|
|
491
|
+
*
|
|
492
|
+
* Watchers:
|
|
493
|
+
* - pts/2 hang fix (2026-06-16): a SINGLE non-recursive watcher on the
|
|
494
|
+
* `runs/` root (new-run detection) plus per-active-run watchers
|
|
495
|
+
* reconciled each preload tick. Total inotify cost: O(active runs).
|
|
496
|
+
*/
|
|
497
|
+
function setupRenderLoop(
|
|
498
|
+
pi: ExtensionAPI,
|
|
499
|
+
ctx: RegistrationContext,
|
|
500
|
+
extensionCtx: ExtensionContext,
|
|
501
|
+
loadedConfig: ReturnType<typeof loadConfig>,
|
|
502
|
+
): void {
|
|
503
|
+
ctx.disposeRenderSchedulerSubscriptions();
|
|
504
|
+
ctx.renderScheduler?.dispose();
|
|
505
|
+
ctx.terminalStatus?.dispose();
|
|
506
|
+
ctx.terminalStatus = undefined;
|
|
507
|
+
ctx.terminalStatusActive = false;
|
|
508
|
+
|
|
509
|
+
// Phase 12: Async preloading — renderTick reads only a pre-computed frame.
|
|
510
|
+
let preloading = false;
|
|
511
|
+
let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
|
|
512
|
+
let lastPreloadedManifests: TeamRunManifest[] = [];
|
|
513
|
+
let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
|
|
514
|
+
let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
|
|
515
|
+
|
|
516
|
+
const ownerGeneration = ctx.sessionGeneration;
|
|
517
|
+
|
|
518
|
+
const buildFrame = async (): Promise<boolean> => {
|
|
519
|
+
if (!ctx.currentCtx) return false;
|
|
520
|
+
lastPreloadedConfig = loadConfig(ctx.currentCtx.cwd);
|
|
521
|
+
lastFrameManifestCache = ctx.getManifestCache(ctx.currentCtx.cwd);
|
|
522
|
+
lastFrameSnapshotCache = ctx.getRunSnapshotCache(ctx.currentCtx.cwd);
|
|
523
|
+
const manifests = lastFrameManifestCache.list(20);
|
|
524
|
+
lastPreloadedManifests = manifests;
|
|
525
|
+
// pts/2 hang fix: reconcile per-run watchers against the ACTIVE set only.
|
|
526
|
+
{
|
|
527
|
+
const onRunChange = (runId: string): void => {
|
|
528
|
+
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return;
|
|
529
|
+
// FLICKER FIX: rebuild-in-place instead of deleting the entry. The
|
|
530
|
+
// file just changed on disk, so force a fresh snapshot while keeping
|
|
531
|
+
// the entry populated — deleting it left a window where the widget's
|
|
532
|
+
// `get()` returned undefined and dropped the run to "(loading…)".
|
|
533
|
+
try {
|
|
534
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
logInternalError("register.runWatcher.refresh", error, runId);
|
|
537
|
+
}
|
|
538
|
+
ctx.renderScheduler?.schedule({ runId });
|
|
539
|
+
};
|
|
540
|
+
const onWatchErr = (error: unknown): void => {
|
|
541
|
+
logInternalError("register.runWatcher.change", error);
|
|
542
|
+
};
|
|
543
|
+
const active = manifests
|
|
544
|
+
.filter((r) => r.status === "running" || r.status === "queued" || r.status === "planning")
|
|
545
|
+
.map((r) => ({ runId: r.runId, runDir: r.stateRoot }));
|
|
546
|
+
ctx.crewRunWatchers?.reconcile(active, onRunChange, onWatchErr);
|
|
547
|
+
ctx.userCrewWatchers?.reconcile(active, onRunChange, onWatchErr);
|
|
548
|
+
}
|
|
549
|
+
const runIds = manifests.map((r) => r.runId);
|
|
550
|
+
await lastFrameSnapshotCache.preloadAllStale(runIds);
|
|
551
|
+
return true;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const backgroundPreload = (): void => {
|
|
555
|
+
if (!ctx.currentCtx || preloading) return;
|
|
556
|
+
preloading = true;
|
|
557
|
+
buildFrame()
|
|
558
|
+
.then((ok) => {
|
|
559
|
+
preloading = false;
|
|
560
|
+
if (ok) ctx.renderScheduler?.schedule();
|
|
561
|
+
})
|
|
562
|
+
.catch((error: unknown) => {
|
|
563
|
+
preloading = false;
|
|
564
|
+
logInternalError("register.backgroundPreload", error);
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const startPreloadLoop = (intervalMs: number, dynamicMs?: () => number): void => {
|
|
569
|
+
if (ctx.preloadTimer) clearTimeout(ctx.preloadTimer);
|
|
570
|
+
const tick = (): void => {
|
|
571
|
+
backgroundPreload();
|
|
572
|
+
const nextMs = dynamicMs?.() ?? intervalMs;
|
|
573
|
+
ctx.preloadTimer = setTimeout(tick, nextMs);
|
|
574
|
+
ctx.preloadTimer.unref();
|
|
575
|
+
};
|
|
576
|
+
ctx.preloadTimer = setTimeout(tick, intervalMs);
|
|
577
|
+
ctx.preloadTimer.unref();
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const renderTick = (): void => {
|
|
581
|
+
if (!ctx.currentCtx) return;
|
|
582
|
+
const config = lastPreloadedConfig?.config.ui;
|
|
583
|
+
const activeCache = lastFrameManifestCache ?? ctx.getManifestCache(ctx.currentCtx.cwd);
|
|
584
|
+
const snapshotCache = lastFrameSnapshotCache ?? ctx.getRunSnapshotCache(ctx.currentCtx.cwd);
|
|
585
|
+
const manifests = lastPreloadedManifests;
|
|
586
|
+
if (!lastPreloadedConfig) backgroundPreload();
|
|
587
|
+
if (ctx.uiState.liveSidebarRunId || ctx.uiState.dashboardOpen) {
|
|
588
|
+
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
589
|
+
if (ctx.widgetState.lastVisibility !== "hidden" || ctx.widgetState.lastPlacement !== placement) {
|
|
590
|
+
setExtensionWidget(ctx.currentCtx, "pi-crew", undefined, { placement });
|
|
591
|
+
setExtensionWidget(ctx.currentCtx, "pi-crew-active", undefined, { placement });
|
|
592
|
+
ctx.widgetState.lastVisibility = "hidden";
|
|
593
|
+
ctx.widgetState.lastPlacement = placement;
|
|
594
|
+
ctx.widgetState.lastKey = "pi-crew-active";
|
|
595
|
+
ctx.widgetState.model = undefined;
|
|
596
|
+
}
|
|
597
|
+
requestRender(ctx.currentCtx);
|
|
598
|
+
} else {
|
|
599
|
+
updateCrewWidget(ctx.currentCtx, ctx.widgetState, config, activeCache, snapshotCache, manifests);
|
|
600
|
+
}
|
|
601
|
+
requestPowerbarUpdate(
|
|
602
|
+
pi.events,
|
|
603
|
+
ctx.currentCtx.cwd,
|
|
604
|
+
config,
|
|
605
|
+
activeCache,
|
|
606
|
+
snapshotCache,
|
|
607
|
+
ctx.currentCtx,
|
|
608
|
+
ctx.widgetState.notificationCount ?? 0,
|
|
609
|
+
manifests,
|
|
610
|
+
);
|
|
611
|
+
// Health notifications: only warn about genuinely running runs.
|
|
612
|
+
const currentSessionGen = ctx.sessionGeneration;
|
|
613
|
+
const currentSessionId = ctx.currentCtx ? (ctx.currentCtx as { sessionId?: string }).sessionId : undefined;
|
|
614
|
+
const sessionManifests = manifests.filter(
|
|
615
|
+
(run) =>
|
|
616
|
+
!run.ownerSessionId ||
|
|
617
|
+
run.ownerSessionId === currentSessionId ||
|
|
618
|
+
(run as unknown as Record<string, unknown>).ownerSessionGeneration === currentSessionGen,
|
|
619
|
+
);
|
|
620
|
+
const now = Date.now();
|
|
621
|
+
for (const run of sessionManifests) {
|
|
622
|
+
if (run.status !== "running") continue;
|
|
623
|
+
try {
|
|
624
|
+
const snapshot = snapshotCache.get(run.runId);
|
|
625
|
+
if (!snapshot) continue;
|
|
626
|
+
if (snapshot.manifest.status !== "running") continue;
|
|
627
|
+
const summary = summarizeHeartbeats(snapshot, { now });
|
|
628
|
+
const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
|
|
629
|
+
if (count <= 0) return;
|
|
630
|
+
const key = `${kind}_${run.runId}`;
|
|
631
|
+
const previous = ctx.autoRecoveryLast.get(key);
|
|
632
|
+
if (previous !== undefined && now - previous.lastAccessAt < 5 * 60_000) return;
|
|
633
|
+
// Defensive cap: evict entry with oldest lastAccessAt before inserting/updating.
|
|
634
|
+
while (ctx.autoRecoveryLast.size >= ctx.AUTO_RECOVERY_LAST_MAX_ENTRIES) {
|
|
635
|
+
let oldestKey: string | undefined;
|
|
636
|
+
let oldestAccess = Infinity;
|
|
637
|
+
for (const [k, v] of ctx.autoRecoveryLast) {
|
|
638
|
+
if (v.lastAccessAt < oldestAccess) {
|
|
639
|
+
oldestAccess = v.lastAccessAt;
|
|
640
|
+
oldestKey = k;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (oldestKey === undefined) break;
|
|
644
|
+
ctx.autoRecoveryLast.delete(oldestKey);
|
|
645
|
+
}
|
|
646
|
+
ctx.autoRecoveryLast.set(key, {
|
|
647
|
+
insertedAt: now,
|
|
648
|
+
lastAccessAt: now,
|
|
649
|
+
});
|
|
650
|
+
ctx.notifyOperator({
|
|
651
|
+
id: key,
|
|
652
|
+
severity: "warning",
|
|
653
|
+
source: "health",
|
|
654
|
+
runId: run.runId,
|
|
655
|
+
title,
|
|
656
|
+
body,
|
|
657
|
+
});
|
|
658
|
+
};
|
|
659
|
+
maybeNotifyHealth(
|
|
660
|
+
"recovery_dead_workers",
|
|
661
|
+
summary.dead,
|
|
662
|
+
`Run ${run.runId} has ${summary.dead} dead worker(s).`,
|
|
663
|
+
"Open /team-dashboard → 5 health → R recovery / K kill stale / D diagnostic.",
|
|
664
|
+
);
|
|
665
|
+
maybeNotifyHealth(
|
|
666
|
+
"recovery_missing_heartbeat",
|
|
667
|
+
summary.missing,
|
|
668
|
+
`Run ${run.runId} has ${summary.missing} worker(s) missing heartbeat.`,
|
|
669
|
+
"Open /team-dashboard → 5 health → inspect health actions.",
|
|
670
|
+
);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
logInternalError("register.health-notification", error, run.runId);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? DEFAULT_UI.refreshMs;
|
|
678
|
+
const liveRefreshMs = 160;
|
|
679
|
+
const hasActiveWork = (): boolean => {
|
|
680
|
+
if (listLiveAgents().some((a) => a.status === "running")) return true;
|
|
681
|
+
return lastPreloadedManifests.some((r) => r.status === "running" || r.status === "queued" || r.status === "planning");
|
|
682
|
+
};
|
|
683
|
+
const effectiveRefreshMs = () => (hasActiveWork() ? liveRefreshMs : fallbackMs);
|
|
684
|
+
ctx.renderScheduler = new RenderScheduler(pi.events, renderTick, {
|
|
685
|
+
fallbackMs: effectiveRefreshMs,
|
|
686
|
+
onInvalidate: (payload: unknown) => {
|
|
687
|
+
const runId =
|
|
688
|
+
typeof payload === "object" &&
|
|
689
|
+
payload !== null &&
|
|
690
|
+
"runId" in payload &&
|
|
691
|
+
typeof (payload as { runId: unknown }).runId === "string"
|
|
692
|
+
? (payload as { runId: string }).runId
|
|
693
|
+
: undefined;
|
|
694
|
+
// FLICKER FIX: never hard-delete snapshot entries from a render-scheduler
|
|
695
|
+
// invalidate. A no-runId payload — emitted by EVERY fallback tick
|
|
696
|
+
// (~every 160ms while a run is active) — previously ran
|
|
697
|
+
// `invalidate(undefined)` → `entries.clear()`, wiping ALL snapshots.
|
|
698
|
+
// The next `renderTick` then saw `get() === undefined` for every run,
|
|
699
|
+
// so `activeWidgetRuns` dropped them to "(loading…)" until the async
|
|
700
|
+
// preload rebuilt the cache — an endless visible flicker. For a
|
|
701
|
+
// specific runId we now refresh-if-stale (stale-while-revalidate) so
|
|
702
|
+
// the widget always sees a populated snapshot; a no-runId tick does
|
|
703
|
+
// nothing (renderTick itself repaints; the cache's own
|
|
704
|
+
// run:state/worker:lifecycle subscription refreshes affected runs).
|
|
705
|
+
if (!runId) return;
|
|
706
|
+
try {
|
|
707
|
+
ctx.getRunSnapshotCache(extensionCtx.cwd).refreshIfStale(runId);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logInternalError("register.renderScheduler.refresh", error, runId);
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
// Fix D: bridge internal runEventBus events to renderScheduler so the UI
|
|
714
|
+
// re-renders within debounceMs of any agent lifecycle event.
|
|
715
|
+
const sched = ctx.renderScheduler;
|
|
716
|
+
const unsubscribeRunEvents = runEventBus.onAny((event) => {
|
|
717
|
+
sched.schedule({
|
|
718
|
+
runId: event.runId,
|
|
719
|
+
source: "runEventBus",
|
|
720
|
+
type: event.type,
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
ctx.renderSchedulerUnsubscribers.push(unsubscribeRunEvents);
|
|
724
|
+
startPreloadLoop(fallbackMs, effectiveRefreshMs);
|
|
725
|
+
|
|
726
|
+
// Bounded run watcher setup (pts/2 hang fix 2026-06-16).
|
|
727
|
+
const crewRunWatcherOnChange = (runId: string): void => {
|
|
728
|
+
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return;
|
|
729
|
+
// FLICKER FIX: rebuild-in-place instead of deleting the entry (see
|
|
730
|
+
// onRunChange above). A hard delete left `get()` returning undefined for
|
|
731
|
+
// a frame, dropping the run to "(loading…)" and causing visible flicker.
|
|
732
|
+
try {
|
|
733
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
logInternalError("register.crewRunWatcher.refresh", error, runId);
|
|
736
|
+
}
|
|
737
|
+
ctx.renderScheduler?.schedule({ runId });
|
|
738
|
+
};
|
|
739
|
+
const crewRunWatcherOnError = (error: unknown): void => {
|
|
740
|
+
logInternalError("register.crewRunWatchers.error", error);
|
|
741
|
+
};
|
|
742
|
+
try {
|
|
743
|
+
ctx.crewRunWatchers?.closeAll();
|
|
744
|
+
ctx.crewRunWatchers = undefined;
|
|
745
|
+
const crewRunsDir = path.join(projectCrewRoot(extensionCtx.cwd), "state", "runs");
|
|
746
|
+
if (fs.existsSync(crewRunsDir)) {
|
|
747
|
+
ctx.crewRunWatchers = new RunWatcherRegistry();
|
|
748
|
+
ctx.crewRunWatchers.setRootWatcher(crewRunsDir, crewRunWatcherOnChange, crewRunWatcherOnError);
|
|
749
|
+
}
|
|
750
|
+
} catch (error) {
|
|
751
|
+
logInternalError("register.crewRunWatchers.start", error);
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
ctx.userCrewWatchers?.closeAll();
|
|
755
|
+
ctx.userCrewWatchers = undefined;
|
|
756
|
+
const userRunsDir = path.join(userCrewRoot(), "state", "runs");
|
|
757
|
+
if (fs.existsSync(userRunsDir)) {
|
|
758
|
+
ctx.userCrewWatchers = new RunWatcherRegistry();
|
|
759
|
+
ctx.userCrewWatchers.setRootWatcher(userRunsDir, crewRunWatcherOnChange, crewRunWatcherOnError);
|
|
760
|
+
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
logInternalError("register.userCrewWatchers.start", error);
|
|
763
|
+
}
|
|
764
|
+
// Kick an immediate preload so the first buildFrame reconciles per-run
|
|
765
|
+
// watchers for any runs that are already active on session start.
|
|
766
|
+
backgroundPreload();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// =============================================================================
|
|
770
|
+
// Phase 0 inter-pi broker lifecycle controller (sub-task 0.5)
|
|
771
|
+
// =============================================================================
|
|
772
|
+
//
|
|
773
|
+
// `installCrewBrokerLifecycleController` wires the per-session broker into
|
|
774
|
+
// the existing extension lifecycle. The controller:
|
|
775
|
+
//
|
|
776
|
+
// - is a no-op unless broker.enabled is true AND the current process is the
|
|
777
|
+
// root pi session (PI_CREW_KIND !== "subagent" AND currentCrewDepth === 0).
|
|
778
|
+
// Children NEVER install a broker.
|
|
779
|
+
// - lazily constructs a single CrewBroker instance per session_id. listen()
|
|
780
|
+
// is deferred until the first child run actually requests broker credentials.
|
|
781
|
+
// - issues a heap-only token per child run via `issueForChild`. The token
|
|
782
|
+
// NEVER leaves the parent's heap and the child's env (PI_CREW_BROKER_TOKEN).
|
|
783
|
+
// It is never written to disk.
|
|
784
|
+
// - retains the broker across session switches when the session_id is
|
|
785
|
+
// unchanged; on a session_id change, stops the old broker and binds a new
|
|
786
|
+
// one on the next acquire.
|
|
787
|
+
// - stops the broker during session_shutdown BEFORE the runtime cleanup path.
|
|
788
|
+
//
|
|
789
|
+
// The gate is re-evaluated on every `issueForChild` call (cheap env+depth
|
|
790
|
+
// check) so the kill switch (PI_CREW_BROKER=0) takes effect immediately.
|
|
791
|
+
|
|
792
|
+
export interface CrewBrokerLifecycleController {
|
|
793
|
+
/** Issue credentials for a child run. Returns undefined when the broker
|
|
794
|
+
* is disabled, this process is a subagent, or no session_id is known. */
|
|
795
|
+
issueForChild(runId: string, taskId?: string): Promise<BrokerSpawnCredentials | undefined>;
|
|
796
|
+
/** Stop the broker (idempotent). Called on session_shutdown. */
|
|
797
|
+
stop(): Promise<void>;
|
|
798
|
+
/** Test/lifecycle seam: remember the most recent session_id for token issuance. */
|
|
799
|
+
setSessionId(sessionId: string | undefined): void;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function isRootSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
803
|
+
if (env.PI_CREW_KIND === "subagent") return false;
|
|
804
|
+
try {
|
|
805
|
+
return currentCrewDepth(env) === 0;
|
|
806
|
+
} catch {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: RegistrationContext): CrewBrokerLifecycleController {
|
|
812
|
+
let broker: CrewBroker | null = null;
|
|
813
|
+
let brokerSessionId: string | undefined;
|
|
814
|
+
let starting: Promise<CrewBroker> | null = null;
|
|
815
|
+
let cachedSessionId: string | undefined;
|
|
816
|
+
|
|
817
|
+
function effectiveEnabled(): boolean {
|
|
818
|
+
// Env wins over config. PI_CREW_BROKER=1 forces on, =0 forces off.
|
|
819
|
+
const envOverride = process.env.PI_CREW_BROKER;
|
|
820
|
+
if (envOverride === "0") return false;
|
|
821
|
+
// Config block: read fresh so a runtime config update takes effect.
|
|
822
|
+
try {
|
|
823
|
+
const cfg = loadConfig().config.broker;
|
|
824
|
+
if (envOverride === "1") return cfg !== undefined ? cfg.enabled !== false : true;
|
|
825
|
+
// Phase 4 (v0.9.47) default-on: enabled unless explicitly disabled.
|
|
826
|
+
return cfg?.enabled !== false;
|
|
827
|
+
} catch {
|
|
828
|
+
// Fail-safe: config load failed → keep broker disabled.
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function getOrStartBroker(sessionId: string): Promise<CrewBroker> {
|
|
834
|
+
if (broker && brokerSessionId === sessionId) return broker;
|
|
835
|
+
if (broker && brokerSessionId !== sessionId) {
|
|
836
|
+
try {
|
|
837
|
+
await broker.stop();
|
|
838
|
+
} catch {
|
|
839
|
+
/* ignore */
|
|
840
|
+
}
|
|
841
|
+
broker = null;
|
|
842
|
+
brokerSessionId = undefined;
|
|
843
|
+
}
|
|
844
|
+
if (!broker && !starting) {
|
|
845
|
+
starting = (async () => {
|
|
846
|
+
const cfg = (() => {
|
|
847
|
+
try {
|
|
848
|
+
return loadConfig().config.broker;
|
|
849
|
+
} catch {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
})();
|
|
853
|
+
const b = new CrewBroker({
|
|
854
|
+
sessionId,
|
|
855
|
+
socketPath: getBrokerSocketPath(sessionId),
|
|
856
|
+
maxFrameBytes: cfg?.maxFrameBytes ?? 262144,
|
|
857
|
+
outboundQueueCap: cfg?.outboundQueueCap ?? 256,
|
|
858
|
+
enabled: true,
|
|
859
|
+
cwd: process.cwd(),
|
|
860
|
+
});
|
|
861
|
+
try {
|
|
862
|
+
await b.start();
|
|
863
|
+
broker = b;
|
|
864
|
+
brokerSessionId = sessionId;
|
|
865
|
+
return b;
|
|
866
|
+
} finally {
|
|
867
|
+
starting = null;
|
|
868
|
+
}
|
|
869
|
+
})();
|
|
870
|
+
}
|
|
871
|
+
return starting!;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const issueForChild = async (runId: string, taskId?: string): Promise<BrokerSpawnCredentials | undefined> => {
|
|
875
|
+
if (!runId || typeof runId !== "string") return undefined;
|
|
876
|
+
if (!isRootSession(process.env)) return undefined;
|
|
877
|
+
if (!effectiveEnabled()) return undefined;
|
|
878
|
+
const sessionId = cachedSessionId;
|
|
879
|
+
if (!sessionId) return undefined;
|
|
880
|
+
try {
|
|
881
|
+
const b = await getOrStartBroker(sessionId);
|
|
882
|
+
const token = b.issueRunToken(runId, taskId);
|
|
883
|
+
return { socketPath: b.socketPath, token };
|
|
884
|
+
} catch {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// Publish this issuer as the process-local active issuer so runChildPi can
|
|
890
|
+
// default `brokerIssuer` without the registration context being threaded
|
|
891
|
+
// through every runner call site. The issuer self-gates (root + flag), so
|
|
892
|
+
// publishing it unconditionally is safe even when the broker is disabled.
|
|
893
|
+
setActiveBrokerIssuer(issueForChild);
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
issueForChild,
|
|
897
|
+
stop: async () => {
|
|
898
|
+
setActiveBrokerIssuer(undefined);
|
|
899
|
+
if (broker) {
|
|
900
|
+
try {
|
|
901
|
+
await broker.stop();
|
|
902
|
+
} catch {
|
|
903
|
+
/* ignore */
|
|
904
|
+
}
|
|
905
|
+
broker = null;
|
|
906
|
+
brokerSessionId = undefined;
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
/** Test seam: remember the most recent session_id for token issuance. */
|
|
910
|
+
setSessionId: (sessionId: string | undefined) => {
|
|
911
|
+
// Runtime validation: cap the length and charset to defend against
|
|
912
|
+
// a hostile extension supplying a huge or pathological id. The
|
|
913
|
+
// socket path is already hash-derived (4..32 hex), so a longer
|
|
914
|
+
// sessionId is harmless on disk but wastes heap.
|
|
915
|
+
if (typeof sessionId !== "string") return;
|
|
916
|
+
if (sessionId.length === 0 || sessionId.length > 256) return;
|
|
917
|
+
cachedSessionId = sessionId;
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Marker used by tests to confirm the controller object identity. */
|
|
923
|
+
export const __test__brokerControllerMarker = true;
|