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,1105 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
|
+
import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
|
|
7
|
+
import { registerChildProcess, unregisterChildProcess } from "../extension/crew-cleanup.ts";
|
|
8
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
9
|
+
import type { WorkerExitStatus } from "../state/types.ts";
|
|
10
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
11
|
+
import { redactSecretString } from "../utils/redaction.ts";
|
|
12
|
+
import { getActiveBrokerIssuer } from "./broker-issuer.ts";
|
|
13
|
+
import { FINAL_DRAIN_MS, HARD_KILL_MS, POST_EXIT_STDIO_GUARD_MS, RESPONSE_TIMEOUT_MS } from "./child-pi-constants.ts";
|
|
14
|
+
import { appendBoundedTail, clearHardKillTimer, killProcessTree, registerActiveChild, unregisterActiveChild } from "./child-pi-kill.ts";
|
|
15
|
+
import { buildFinalChildPiSpawnOptions, prepareSpawnContext } from "./child-pi-spawn.ts";
|
|
16
|
+
import { ChildPiSteeringController } from "./child-pi-steering.ts";
|
|
17
|
+
// Internal helpers for active-child bookkeeping (extracted to child-pi-kill.ts).
|
|
18
|
+
import { ChildPiLineObserver } from "./child-pi-streams.ts";
|
|
19
|
+
|
|
20
|
+
// ── Re-exports from child-pi-kill.ts (H-7 decomposition step 2) ──
|
|
21
|
+
// killProcessTree is internal (not previously exported) — keep that invariant.
|
|
22
|
+
export {
|
|
23
|
+
killProcessPid,
|
|
24
|
+
terminateActiveChildPiProcesses,
|
|
25
|
+
} from "./child-pi-kill.ts";
|
|
26
|
+
// ── Re-export from child-pi-spawn.ts (H-7 decomposition step 6) ──
|
|
27
|
+
// buildChildPiSpawnOptions was previously exported from child-pi.ts. Keep the
|
|
28
|
+
// public API surface stable by re-exporting from the new module.
|
|
29
|
+
// buildFinalChildPiSpawnOptions (BLOCKER 2 / S5) — composed spawn helper that
|
|
30
|
+
// owns the canary + filter + spread sequence; lives in child-pi-spawn.ts.
|
|
31
|
+
export { buildChildPiSpawnOptions, buildFinalChildPiSpawnOptions } from "./child-pi-spawn.ts";
|
|
32
|
+
// ── Re-export from child-pi-streams.ts (H-7 decomposition step 4) ──
|
|
33
|
+
export { ChildPiLineObserver } from "./child-pi-streams.ts";
|
|
34
|
+
|
|
35
|
+
import { classifyProcessCrash } from "./crash-classification.ts";
|
|
36
|
+
import { checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
|
|
37
|
+
|
|
38
|
+
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
39
|
+
|
|
40
|
+
/** Maximum size (bytes) for the ChildPiLineObserver's line accumulation buffer.
|
|
41
|
+
* When exceeded, the buffer is force-flushed to prevent unbounded memory growth
|
|
42
|
+
* from chatty child processes that produce output without newlines.
|
|
43
|
+
* (Constant moved to child-pi-constants.ts.) */
|
|
44
|
+
|
|
45
|
+
// Periodic cleanup of dead child process entries to prevent memory leaks.
|
|
46
|
+
/**
|
|
47
|
+
* SEC-1: Extract a redacted stderr/stdout excerpt for embedding in lifecycle
|
|
48
|
+
* events and error messages. The in-memory stdout/stderr accumulators receive
|
|
49
|
+
* RAW worker output (only structurally compacted via compactChildPiEvent —
|
|
50
|
+
* NOT secret-redacted), so any slice embedded into a persisted event must be
|
|
51
|
+
* redacted here. Otherwise worker-emitted secrets (API keys, tokens returned
|
|
52
|
+
* from a tool call) leak through diagnostic logs that bypass artifact-store
|
|
53
|
+
* redaction.
|
|
54
|
+
*
|
|
55
|
+
* Extracted as a single helper (8 call sites were duplicating this) so the
|
|
56
|
+
* redaction boundary is unit-testable directly. The real spawn error/timeout
|
|
57
|
+
* paths are integration-level and NOT reachable via PI_TEAMS_MOCK_CHILD_PI
|
|
58
|
+
* (the mock returns before the lifecycle-event handlers run), so a behavior
|
|
59
|
+
* test must target this helper rather than the full runChildPi path.
|
|
60
|
+
*/
|
|
61
|
+
export function redactStderrExcerpt(stderr: string, maxChars: number): string {
|
|
62
|
+
return redactSecretString(stderr.slice(-maxChars));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* B6: spawn taskkill and attach an 'error' listener. spawn() emits ENOENT/EACCES
|
|
67
|
+
* asynchronously via the 'error' event (not as a throw), so an unlistened spawn
|
|
68
|
+
* can crash the parent as an uncaught exception. taskkill is a standard Windows
|
|
69
|
+
* binary so this is defensive, but the listener keeps failures bounded.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/** Structured lifecycle event emitted by child-pi for critical transitions. */
|
|
73
|
+
export interface ChildPiLifecycleEvent {
|
|
74
|
+
/** Event discriminator. */
|
|
75
|
+
type: "spawned" | "spawn_error" | "response_timeout" | "final_drain" | "hard_kill" | "exit" | "close";
|
|
76
|
+
/** Process ID when available. */
|
|
77
|
+
pid?: number;
|
|
78
|
+
/** Exit code for exit/close events. */
|
|
79
|
+
exitCode?: number | null;
|
|
80
|
+
/** Error message for error events. */
|
|
81
|
+
error?: string;
|
|
82
|
+
/** Stderr captured at timeout moment (for response_timeout events). */
|
|
83
|
+
stderr?: string;
|
|
84
|
+
/** Last N chars of stderr for error context (exit/error events). */
|
|
85
|
+
stderrExcerpt?: string;
|
|
86
|
+
/** Timestamp (ISO). */
|
|
87
|
+
ts: string;
|
|
88
|
+
/** F12: optional cause for `final_drain` events. `"stdout-quiet"` indicates
|
|
89
|
+
* the drain was triggered by the quiet-window early-exit rather than the
|
|
90
|
+
* default 5 s ceiling. Other drain reasons (default) leave this undefined. */
|
|
91
|
+
reason?: "stdout-quiet";
|
|
92
|
+
/** Phase-0 diagnostic (HB-003a): the signal that killed the child (when
|
|
93
|
+
* available). Was previously discarded after building the error string. */
|
|
94
|
+
signal?: string;
|
|
95
|
+
/** Phase-0 diagnostic (HB-003a): final-drain race timing, present only on
|
|
96
|
+
* exit events where a drain timer was armed. Surfaces the exit-null race. */
|
|
97
|
+
diagnostic?: {
|
|
98
|
+
finalDrainArmed: boolean;
|
|
99
|
+
forcedFinalDrain: boolean;
|
|
100
|
+
finalDrainFiredMonotonicMs?: number;
|
|
101
|
+
finalAssistantEventMonotonicMs?: number;
|
|
102
|
+
exitMonotonicMs: number;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ChildPiRunInput {
|
|
107
|
+
cwd: string;
|
|
108
|
+
task: string;
|
|
109
|
+
agent: AgentConfig;
|
|
110
|
+
model?: string;
|
|
111
|
+
skillPaths?: string[];
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
transcriptPath?: string;
|
|
114
|
+
onStdoutLine?: (line: string) => void;
|
|
115
|
+
onJsonEvent?: (event: unknown) => void;
|
|
116
|
+
onSpawn?: (pid: number) => void;
|
|
117
|
+
/** Structured lifecycle events for durable logging (spawn, crash, timeout, kill, exit). */
|
|
118
|
+
onLifecycleEvent?: (event: ChildPiLifecycleEvent) => void;
|
|
119
|
+
maxDepth?: number;
|
|
120
|
+
finalDrainMs?: number;
|
|
121
|
+
/** F12: early-exit the drain when stdout has been silent for this many ms
|
|
122
|
+
* after the final assistant event. Set to ≥ finalDrainMs to disable. */
|
|
123
|
+
finalDrainQuietMs?: number;
|
|
124
|
+
hardKillMs?: number;
|
|
125
|
+
responseTimeoutMs?: number;
|
|
126
|
+
/** Soft limit on assistant turns — inject steer at this count. */
|
|
127
|
+
maxTurns?: number;
|
|
128
|
+
/** Extra turns after soft limit before hard abort. Default: 5. */
|
|
129
|
+
graceTurns?: number;
|
|
130
|
+
/** Parent conversation context to inherit when inheritContext is true. */
|
|
131
|
+
parentContext?: string;
|
|
132
|
+
/** When true, prepend parentContext to the task prompt. */
|
|
133
|
+
inheritContext?: boolean;
|
|
134
|
+
/** Pass to pi to mark certain commands as context-excluded. Default: false */
|
|
135
|
+
excludeContextBash?: boolean;
|
|
136
|
+
/** pi session ID for session naming (aligns with pi-crew run ID) */
|
|
137
|
+
sessionId?: string;
|
|
138
|
+
/** Path to steering JSONL file for real-time steer injection. */
|
|
139
|
+
steeringFile?: string;
|
|
140
|
+
/** Run ID for cleanup tracking */
|
|
141
|
+
runId?: string;
|
|
142
|
+
/** Agent ID for cleanup tracking */
|
|
143
|
+
agentId?: string;
|
|
144
|
+
/** Role for tool restrictions (from role-tools.ts) */
|
|
145
|
+
role?: string;
|
|
146
|
+
/** Root directory for artifacts (used to validate transcriptPath). */
|
|
147
|
+
artifactsRoot?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Optional broker spawn context (Phase 0 inter-pi broker). When present,
|
|
150
|
+
* `prepareSpawnContext` injects `PI_CREW_BROKER_SOCKET` and
|
|
151
|
+
* `PI_CREW_BROKER_TOKEN` into the child env (control-namespace keys,
|
|
152
|
+
* accepted by `assertOnlyControlEnvKeys`). The token is a 128-bit random
|
|
153
|
+
* value issued by the parent's CrewBroker at spawn time; it lives only
|
|
154
|
+
* in the parent's in-memory `Map<runId, token>` and in the child's env.
|
|
155
|
+
* NEVER persisted to manifest, events, mailbox, or any run-dir file.
|
|
156
|
+
*/
|
|
157
|
+
brokerSpawn?: { socketPath: string; token: string };
|
|
158
|
+
/**
|
|
159
|
+
* Optional Phase 0 broker credentials issuer. Called only when
|
|
160
|
+
* `brokerSpawn` is not already set on the input. The default no-op
|
|
161
|
+
* issuer returns undefined, leaving the child to run without broker
|
|
162
|
+
* credentials (today's behavior — file paths remain authoritative).
|
|
163
|
+
* The production wiring passes a closure that delegates to the
|
|
164
|
+
* session's `CrewBrokerLifecycleController.issueForChild`.
|
|
165
|
+
*/
|
|
166
|
+
brokerIssuer?: (runId: string, taskId?: string) => Promise<{ socketPath: string; token: string } | undefined>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ChildPiRunResult {
|
|
170
|
+
exitCode: number | null;
|
|
171
|
+
stdout: string;
|
|
172
|
+
stderr: string;
|
|
173
|
+
error?: string;
|
|
174
|
+
/** RAW (uncapped) final assistant text, captured at stream-parse time BEFORE
|
|
175
|
+
* the 16K transcript compaction. This is the AUTHORITATIVE worker output —
|
|
176
|
+
* it becomes results/<id>.txt so downstream dependencies are not bounded by
|
|
177
|
+
* the transcript's telemetry cap. Undefined when no assistant text was seen
|
|
178
|
+
* (mock paths, error paths) — callers MUST fall back to transcript-derived
|
|
179
|
+
* finalText. See research-findings/output-handling-deep-dive.md §A. */
|
|
180
|
+
rawFinalText?: string;
|
|
181
|
+
exitStatus?: WorkerExitStatus;
|
|
182
|
+
/** True if the agent was hard-aborted (max_turns + grace exceeded). */
|
|
183
|
+
aborted?: boolean;
|
|
184
|
+
/** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
|
|
185
|
+
steered?: boolean;
|
|
186
|
+
/** #7 hardening: bounded digest of intermediate findings (last N tool results or
|
|
187
|
+
* assistant text lines) from the run. Populated by ChildPiLineObserver so that
|
|
188
|
+
* workers that exhaust their budget on tool calls (never emit final assistant
|
|
189
|
+
* text) still produce a non-empty result. Consumers should prefer rawFinalText
|
|
190
|
+
* first — this is a last-resort fallback. */
|
|
191
|
+
intermediateFindings?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Base allowlist of non-provider env vars always passed to child workers.
|
|
195
|
+
// ── Transcript batching + compaction (H-7 decomposition step 1) ────────
|
|
196
|
+
// Extracted to ./child-pi-transcript.ts. Re-exported here to preserve the
|
|
197
|
+
// existing public API surface.
|
|
198
|
+
export {
|
|
199
|
+
appendTranscript,
|
|
200
|
+
compactString,
|
|
201
|
+
compactValue,
|
|
202
|
+
flushPendingTranscriptWrites,
|
|
203
|
+
resetTranscriptBatchState,
|
|
204
|
+
} from "./child-pi-transcript.ts";
|
|
205
|
+
|
|
206
|
+
/** Mock-only path — real code path reuses a single observer.
|
|
207
|
+
* OPT-06 follow-up: returns a Promise so callers can await the transcript
|
|
208
|
+
* drain before resolving runChildPi. Without this, mock-mode callers that
|
|
209
|
+
* read the transcript file post-run see ENOENT (the async file handle had
|
|
210
|
+
* not yet been opened). */
|
|
211
|
+
async function observeStdoutChunk(input: ChildPiRunInput, text: string): Promise<void> {
|
|
212
|
+
const observer = new ChildPiLineObserver(input);
|
|
213
|
+
observer.observe(text);
|
|
214
|
+
await observer.flush();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
218
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isFinalAssistantEvent(event: unknown): boolean {
|
|
222
|
+
const obj = asRecord(event);
|
|
223
|
+
if (obj?.type !== "message_end") return false;
|
|
224
|
+
const message = asRecord(obj.message);
|
|
225
|
+
const role = message?.role;
|
|
226
|
+
if (role !== undefined && role !== "assistant") return false;
|
|
227
|
+
const stopReason =
|
|
228
|
+
typeof message?.stopReason === "string" ? message.stopReason : typeof obj.stopReason === "string" ? obj.stopReason : undefined;
|
|
229
|
+
if (stopReason !== undefined && stopReason !== "stop") return false;
|
|
230
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
231
|
+
return !content.some((part) => asRecord(part)?.type === "toolCall");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
235
|
+
// Phase 1 (live-session parity): prepend parent context when inheritContext is true.
|
|
236
|
+
// This mirrors the effectivePrompt logic in live-session-runtime.ts so that
|
|
237
|
+
// child-process workers receive the same inherited-context treatment.
|
|
238
|
+
const effectiveTask =
|
|
239
|
+
input.inheritContext === true && input.parentContext
|
|
240
|
+
? `${input.parentContext}\n\n---\n# Child Worker Task\n${input.task}`
|
|
241
|
+
: input.task;
|
|
242
|
+
const depth = checkCrewDepth(input.maxDepth);
|
|
243
|
+
if (depth.blocked)
|
|
244
|
+
return {
|
|
245
|
+
exitCode: 1,
|
|
246
|
+
stdout: "",
|
|
247
|
+
stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}`,
|
|
248
|
+
};
|
|
249
|
+
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
250
|
+
if (mock) {
|
|
251
|
+
// SECURITY (Issue #2): Mock mode security model is intentionally asymmetric.
|
|
252
|
+
// PI_TEAMS_MOCK_CHILD_PI is in the allowlist (passed to children) but
|
|
253
|
+
// PI_CREW_ALLOW_MOCK is NOT in the allowlist — it is only checked in the
|
|
254
|
+
// parent process scope. This means:
|
|
255
|
+
// (1) If an attacker sets PI_CREW_ALLOW_MOCK in the parent's environment,
|
|
256
|
+
// it will NOT be passed to child processes (safe).
|
|
257
|
+
// (2) Mock mode activation in the child always fails the PI_CREW_ALLOW_MOCK
|
|
258
|
+
// check, so mock mode can only be triggered from the parent process.
|
|
259
|
+
// This asymmetry is intentional: PI_CREW_ALLOW_MOCK must be set in the Pi root
|
|
260
|
+
// process (the entry point that spawns children), not inherited from a parent.
|
|
261
|
+
// Setup hooks cannot inject PI_CREW_ALLOW_MOCK into the parent's env.
|
|
262
|
+
const allowMock = process.env.PI_CREW_ALLOW_MOCK === "1" || process.env.PI_CREW_ALLOW_MOCK === "true";
|
|
263
|
+
if (!allowMock) {
|
|
264
|
+
return {
|
|
265
|
+
exitCode: 1,
|
|
266
|
+
stdout: "",
|
|
267
|
+
stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// SECURITY: Log mock mode activation prominently for audit trail
|
|
271
|
+
logInternalError("child-pi.mock", new Error(`Mock mode active: ${mock}`), "NOT running real agents");
|
|
272
|
+
if (mock === "success") {
|
|
273
|
+
const stdout = `[MOCK] Success for ${input.agent.name}\n`;
|
|
274
|
+
await observeStdoutChunk(input, stdout);
|
|
275
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
276
|
+
}
|
|
277
|
+
if (mock === "json-success" || mock === "adaptive-plan") {
|
|
278
|
+
const text =
|
|
279
|
+
mock === "adaptive-plan" && effectiveTask.includes("ADAPTIVE_PLAN_JSON_START")
|
|
280
|
+
? `[MOCK] Adaptive plan\nADAPTIVE_PLAN_JSON_START\n${JSON.stringify({
|
|
281
|
+
phases: [
|
|
282
|
+
{
|
|
283
|
+
name: "research",
|
|
284
|
+
tasks: [
|
|
285
|
+
{
|
|
286
|
+
role: "explorer",
|
|
287
|
+
task: "Explore adaptive target",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
role: "analyst",
|
|
291
|
+
task: "Analyze adaptive target",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
role: "planner",
|
|
295
|
+
task: "Plan adaptive target",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "build",
|
|
301
|
+
tasks: [
|
|
302
|
+
{
|
|
303
|
+
role: "executor",
|
|
304
|
+
task: "Implement adaptive target",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "check",
|
|
310
|
+
tasks: [
|
|
311
|
+
{
|
|
312
|
+
role: "reviewer",
|
|
313
|
+
task: "Review adaptive target",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
role: "test-engineer",
|
|
317
|
+
task: "Test adaptive target",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
role: "writer",
|
|
321
|
+
task: "Summarize adaptive target",
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
})}\nADAPTIVE_PLAN_JSON_END`
|
|
327
|
+
: `[MOCK] JSON success for ${input.agent.name}`;
|
|
328
|
+
const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
|
|
329
|
+
await observeStdoutChunk(input, stdout);
|
|
330
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
331
|
+
}
|
|
332
|
+
if (mock === "retryable-failure")
|
|
333
|
+
return {
|
|
334
|
+
exitCode: 1,
|
|
335
|
+
stdout: "",
|
|
336
|
+
stderr: "[MOCK] rate limit: mock failure",
|
|
337
|
+
};
|
|
338
|
+
// E2E fallback-chain fixture: invocation #1 returns a SILENT retryable
|
|
339
|
+
// failure (exit code 0, no real assistant text, message_end carries a
|
|
340
|
+
// retryable-pattern errorMessage). Invocation #2+ delegates to the
|
|
341
|
+
// standard json-success shape. Counter lives in os.tmpdir() keyed by
|
|
342
|
+
// process.pid + mock name so concurrent test processes don't collide.
|
|
343
|
+
// The test cleans up the file in its finally block.
|
|
344
|
+
if (mock === "retryable-failure-then-success") {
|
|
345
|
+
const counterFile = path.join(os.tmpdir(), `pi-crew-mock-counter-${process.pid}-retryable-failure-then-success`);
|
|
346
|
+
let count = 0;
|
|
347
|
+
try {
|
|
348
|
+
const raw = fs.readFileSync(counterFile, "utf-8");
|
|
349
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
350
|
+
if (Number.isFinite(parsed) && parsed >= 0) count = parsed;
|
|
351
|
+
} catch {
|
|
352
|
+
// file missing or unreadable — first invocation in this process
|
|
353
|
+
}
|
|
354
|
+
count += 1;
|
|
355
|
+
try {
|
|
356
|
+
atomicWriteFile(counterFile, String(count));
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logInternalError("child-pi.mock-counter-write", error as Error, `file=${counterFile}`);
|
|
359
|
+
}
|
|
360
|
+
if (count === 1) {
|
|
361
|
+
// Silent retryable failure: exit 0, no real text, message_end
|
|
362
|
+
// carries errorMessage matching `/provider[_ ]?error/i` so that
|
|
363
|
+
// `detectRetryableModelFailureFromOutput` surfaces it as an error
|
|
364
|
+
// and `isRetryableModelFailure` routes the next attempt to the
|
|
365
|
+
// next candidate model. `stopReason:"error"` (NOT "stop") so
|
|
366
|
+
// `isFinalAssistantEvent` does NOT prematurely terminate the run.
|
|
367
|
+
const failureEvent = {
|
|
368
|
+
type: "message_end",
|
|
369
|
+
message: {
|
|
370
|
+
role: "assistant",
|
|
371
|
+
content: [],
|
|
372
|
+
errorMessage: "Provider error: api_error",
|
|
373
|
+
stopReason: "error",
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
const stdout = `${JSON.stringify(failureEvent)}\n`;
|
|
377
|
+
await observeStdoutChunk(input, stdout);
|
|
378
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
379
|
+
}
|
|
380
|
+
// Subsequent invocations: delegate to json-success shape so the
|
|
381
|
+
// fallback chain's second attempt succeeds and the run completes.
|
|
382
|
+
const text = `[MOCK] JSON success for ${input.agent.name}`;
|
|
383
|
+
const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
|
|
384
|
+
await observeStdoutChunk(input, stdout);
|
|
385
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
386
|
+
}
|
|
387
|
+
return { exitCode: 1, stdout: "", stderr: `[MOCK] failure: ${mock}` };
|
|
388
|
+
}
|
|
389
|
+
// H-7 step 6: spawn/env/args preparation extracted to child-pi-spawn.ts.
|
|
390
|
+
// prepareSpawnContext builds the worker args, attaches the steering file env,
|
|
391
|
+
// and handles the pre-spawn abort check (returns an immediate-abort result
|
|
392
|
+
// if the parent signal has already fired).
|
|
393
|
+
//
|
|
394
|
+
// Phase 0 broker: if the caller did not pre-fill `brokerSpawn`, ask the
|
|
395
|
+
// optional issuer for one. The issuer is gated by the lifecycle controller
|
|
396
|
+
// (root-session + flag); a no-op issuer yields undefined (no credentials).
|
|
397
|
+
let brokerSpawn = input.brokerSpawn;
|
|
398
|
+
const brokerIssuer = input.brokerIssuer ?? getActiveBrokerIssuer();
|
|
399
|
+
if (!brokerSpawn && brokerIssuer && input.runId) {
|
|
400
|
+
try {
|
|
401
|
+
brokerSpawn = await brokerIssuer(input.runId, input.agentId);
|
|
402
|
+
} catch {
|
|
403
|
+
brokerSpawn = undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask);
|
|
407
|
+
if (spawnPrep.kind === "aborted") return spawnPrep.result;
|
|
408
|
+
const { spawnSpec, mergedEnv, tempDir, builtEnv } = spawnPrep.ctx;
|
|
409
|
+
try {
|
|
410
|
+
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
411
|
+
// Compose the final SpawnOptions: canary + filter + spread are now
|
|
412
|
+
// owned by buildFinalChildPiSpawnOptions (see child-pi-spawn.ts, BLOCKER 2 / S5).
|
|
413
|
+
const spawnOptions = buildFinalChildPiSpawnOptions(input.cwd, mergedEnv, builtEnv, input.model);
|
|
414
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, spawnOptions);
|
|
415
|
+
if (child.pid) {
|
|
416
|
+
registerActiveChild(child.pid, child);
|
|
417
|
+
input.onSpawn?.(child.pid);
|
|
418
|
+
input.onLifecycleEvent?.({
|
|
419
|
+
type: "spawned",
|
|
420
|
+
pid: child.pid,
|
|
421
|
+
ts: new Date().toISOString(),
|
|
422
|
+
});
|
|
423
|
+
// Register with cleanup handler for graceful shutdown
|
|
424
|
+
if (input.runId && input.agentId) {
|
|
425
|
+
registerChildProcess(child.pid, input.runId, input.agentId);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
input.onLifecycleEvent?.({
|
|
429
|
+
type: "spawn_error",
|
|
430
|
+
error: "spawn returned no pid",
|
|
431
|
+
ts: new Date().toISOString(),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
let stdout = "";
|
|
435
|
+
let stderr = "";
|
|
436
|
+
let settled = false;
|
|
437
|
+
let childExited = false;
|
|
438
|
+
let postExitGuardCleanup: (() => void) | undefined;
|
|
439
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
440
|
+
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
441
|
+
let noResponseTimer: NodeJS.Timeout | undefined;
|
|
442
|
+
const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
|
|
443
|
+
const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
|
|
444
|
+
// Phase-0 diagnostic (HB-003a): track the final-drain race that produces
|
|
445
|
+
// `exit null` for ctx.agent({disableTools:true}). These vars are READ-ONLY
|
|
446
|
+
// instrumentation — no behavior change. finalDrainArmed lets the close
|
|
447
|
+
// handler know a drain timer existed even after clearFinalDrainTimers() ran;
|
|
448
|
+
// spawnMonotonicMs gives us relative timing to distinguish a race from a crash.
|
|
449
|
+
let finalDrainArmed = false;
|
|
450
|
+
// F12: monotonic timestamp of the last stdout JSON event (any event —
|
|
451
|
+
// we want to know when stdout *stopped*, not when the final assistant
|
|
452
|
+
// event arrived). Updated on every onJsonEvent dispatch.
|
|
453
|
+
let lastStdoutActivityMonotonicMs = performance.now();
|
|
454
|
+
let finalDrainFiredMonotonicMs: number | undefined;
|
|
455
|
+
const spawnMonotonicMs = performance.now();
|
|
456
|
+
let finalAssistantEventMonotonicMs: number | undefined;
|
|
457
|
+
// FIX (Round 14): Bound the env-controlled response timeout to
|
|
458
|
+
// [1_000ms, 3_600_000ms] (1s–1h) so a hostile or accidental value
|
|
459
|
+
// (e.g. 1, or 999_999_999) cannot disable the timeout or cause
|
|
460
|
+
// instant kills. Out-of-range values fall back to the input or
|
|
461
|
+
// built-in default.
|
|
462
|
+
const RESPONSE_TIMEOUT_MIN_MS = 1_000;
|
|
463
|
+
const RESPONSE_TIMEOUT_MAX_MS = 3_600_000;
|
|
464
|
+
const responseTimeoutEnv = Number.parseInt(process.env.PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS ?? "", 10);
|
|
465
|
+
const envInRange =
|
|
466
|
+
Number.isFinite(responseTimeoutEnv) &&
|
|
467
|
+
responseTimeoutEnv >= RESPONSE_TIMEOUT_MIN_MS &&
|
|
468
|
+
responseTimeoutEnv <= RESPONSE_TIMEOUT_MAX_MS;
|
|
469
|
+
const responseTimeoutMs = envInRange ? responseTimeoutEnv : (input.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS);
|
|
470
|
+
let responseTimeoutHit = false;
|
|
471
|
+
let forcedFinalDrain = false;
|
|
472
|
+
let abortRequested = input.signal?.aborted === true;
|
|
473
|
+
let hardKilled = false;
|
|
474
|
+
const cleanupErrors: string[] = [];
|
|
475
|
+
const steeringController = new ChildPiSteeringController(input.maxTurns, input.graceTurns);
|
|
476
|
+
// Track in-flight operations for proper rejection on unexpected exit
|
|
477
|
+
interface PendingOperation {
|
|
478
|
+
id: string;
|
|
479
|
+
type: "prompt" | "steer" | "json_event";
|
|
480
|
+
startedAt: number;
|
|
481
|
+
}
|
|
482
|
+
const pendingOperations = new Map<string, PendingOperation>();
|
|
483
|
+
let operationIdCounter = 0;
|
|
484
|
+
|
|
485
|
+
const startOperation = (type: PendingOperation["type"]): string => {
|
|
486
|
+
const id = `op-${++operationIdCounter}`;
|
|
487
|
+
pendingOperations.set(id, { id, type, startedAt: Date.now() });
|
|
488
|
+
return id;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const completeOperation = (id: string): void => {
|
|
492
|
+
pendingOperations.delete(id);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const rejectPendingOperations = (error: Error): void => {
|
|
496
|
+
pendingOperations.forEach((op, id) => {
|
|
497
|
+
logInternalError(
|
|
498
|
+
"child-pi.pending-operation-rejected",
|
|
499
|
+
error,
|
|
500
|
+
`opId=${id} type=${op.type} elapsed=${Date.now() - op.startedAt}ms`,
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
pendingOperations.clear();
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const steerInjectionFailed = false;
|
|
507
|
+
let abortDueToParentSignal = false;
|
|
508
|
+
// CP-1: track whether the turn-limit hard-abort has been initiated. Once
|
|
509
|
+
// true, we must NOT restart the no-response timer — the child is already
|
|
510
|
+
// being killed via killProcessTree (SIGTERM → SIGKILL after 3s), and
|
|
511
|
+
// restarting the timer would delay detection of a SIGTERM-ignoring child.
|
|
512
|
+
// Round 27 (BUG 4): extract to a named handler so settle() can remove it.
|
|
513
|
+
// The previous anonymous listener was never removed → on runs with >10
|
|
514
|
+
// tasks sharing one AbortSignal (background-runner), Node emitted
|
|
515
|
+
// MaxListenersExceededWarning and each leaked listener pinned the task's
|
|
516
|
+
// stack frame (abortDueToParentSignal closure) in memory. { once: true }
|
|
517
|
+
// only auto-removes AFTER the signal fires; on normal completion it leaks.
|
|
518
|
+
const onParentAbort = (): void => {
|
|
519
|
+
abortDueToParentSignal = true;
|
|
520
|
+
};
|
|
521
|
+
input.signal?.addEventListener("abort", onParentAbort, {
|
|
522
|
+
once: true,
|
|
523
|
+
});
|
|
524
|
+
const restartNoResponseTimer = (): void => {
|
|
525
|
+
if (responseTimeoutMs <= 0) return;
|
|
526
|
+
if (noResponseTimer) clearTimeout(noResponseTimer);
|
|
527
|
+
noResponseTimer = setTimeout(() => {
|
|
528
|
+
responseTimeoutHit = true;
|
|
529
|
+
// Capture stderr at timeout moment for debugging
|
|
530
|
+
// SEC-1: redact secrets before embedding in lifecycle event so
|
|
531
|
+
// worker-emitted secrets (API keys etc.) don't bypass redaction.
|
|
532
|
+
const timeoutStderr = redactStderrExcerpt(stderr, 1024); // Last 1KB of stderr (redacted, SEC-1)
|
|
533
|
+
input.onLifecycleEvent?.({
|
|
534
|
+
type: "response_timeout",
|
|
535
|
+
pid: child.pid,
|
|
536
|
+
error: `No output for ${responseTimeoutMs}ms`,
|
|
537
|
+
ts: new Date().toISOString(),
|
|
538
|
+
stderr: timeoutStderr || undefined,
|
|
539
|
+
});
|
|
540
|
+
killProcessTree(child.pid, child);
|
|
541
|
+
try {
|
|
542
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
543
|
+
} catch (error) {
|
|
544
|
+
logInternalError("child-pi.response-timeout-term", error, `pid=${child.pid}`);
|
|
545
|
+
}
|
|
546
|
+
// #3 hardening: if the child never exits (zombie) and neither the
|
|
547
|
+
// 'exit' nor 'close' event ever fires, the promise would hang forever.
|
|
548
|
+
// SIGKILL fires ~3s after SIGTERM via hardKillTimer in killProcessPid,
|
|
549
|
+
// but on platforms where SIGKILL also fails (e.g. permission issues),
|
|
550
|
+
// add a bounded safety settle so the promise always resolves. Using
|
|
551
|
+
// hardKillMs + 2s as the safety window: enough for SIGKILL to work
|
|
552
|
+
// normally, but forces settle if the process is truly immortal.
|
|
553
|
+
// NOTE: we do NOT clear hardKillTimer here (that would defeat its purpose);
|
|
554
|
+
// we intentionally add a parallel safety path.
|
|
555
|
+
const SAFETY_SETTLE_MS = HARD_KILL_MS + 2000;
|
|
556
|
+
const safetyTimer = setTimeout(() => {
|
|
557
|
+
if (settled || childExited) return;
|
|
558
|
+
logInternalError(
|
|
559
|
+
"child-pi.settle-safety-fired",
|
|
560
|
+
new Error(`Child did not exit within ${SAFETY_SETTLE_MS}ms of kill; forcing settle`),
|
|
561
|
+
`pid=${child.pid}, responseTimeoutMs=${responseTimeoutMs}`,
|
|
562
|
+
);
|
|
563
|
+
// Verify the child is still alive before forcing settle.
|
|
564
|
+
// If it somehow exited between childExited=false and here, the
|
|
565
|
+
// settled/childExited guard prevents double-settle (harmless but noisy).
|
|
566
|
+
try {
|
|
567
|
+
process.kill(child.pid!, 0);
|
|
568
|
+
// Child still alive — force settle with timeout error.
|
|
569
|
+
const timeoutErr = `Child Pi produced no new output for ${responseTimeoutMs}ms; killed but did not exit within ${SAFETY_SETTLE_MS}ms (possible zombie).`;
|
|
570
|
+
void settle({
|
|
571
|
+
exitCode: null,
|
|
572
|
+
stdout,
|
|
573
|
+
stderr,
|
|
574
|
+
error: timeoutErr,
|
|
575
|
+
exitStatus: {
|
|
576
|
+
exitCode: null,
|
|
577
|
+
cancelled: abortRequested,
|
|
578
|
+
timedOut: true,
|
|
579
|
+
killed: hardKilled,
|
|
580
|
+
cleanupErrors,
|
|
581
|
+
finalDrainMs,
|
|
582
|
+
crashClass: "timeout",
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
} catch {
|
|
586
|
+
// ESRCH / EPERM — child is already gone. The 'exit'/'close' handler
|
|
587
|
+
// will fire shortly (or already fired in a race). Let it settle normally.
|
|
588
|
+
}
|
|
589
|
+
}, SAFETY_SETTLE_MS);
|
|
590
|
+
safetyTimer.unref();
|
|
591
|
+
}, responseTimeoutMs);
|
|
592
|
+
noResponseTimer.unref();
|
|
593
|
+
};
|
|
594
|
+
const clearNoResponseTimer = (): void => {
|
|
595
|
+
if (noResponseTimer) clearTimeout(noResponseTimer);
|
|
596
|
+
noResponseTimer = undefined;
|
|
597
|
+
};
|
|
598
|
+
restartNoResponseTimer();
|
|
599
|
+
const lineObserver = new ChildPiLineObserver({
|
|
600
|
+
...input,
|
|
601
|
+
onStdoutLine: (line) => {
|
|
602
|
+
if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
|
|
603
|
+
stdout = appendBoundedTail(stdout, `${line}\n`);
|
|
604
|
+
input.onStdoutLine?.(line);
|
|
605
|
+
},
|
|
606
|
+
onJsonEvent: (event) => {
|
|
607
|
+
if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
|
|
608
|
+
const eventOpId = startOperation("json_event");
|
|
609
|
+
try {
|
|
610
|
+
// Turn-count-based steering: soft limit steer + hard abort after graceTurns
|
|
611
|
+
if (event && typeof event === "object" && !Array.isArray(event)) {
|
|
612
|
+
const obj = event as Record<string, unknown>;
|
|
613
|
+
if (obj.type === "turn_end") {
|
|
614
|
+
// H-7 step 5: steering state machine extracted to ChildPiSteeringController.
|
|
615
|
+
const action = steeringController.onTurnEnd(child.pid, child, input.steeringFile);
|
|
616
|
+
if (action.kind === "hardAbort") killProcessTree(action.pid, action.child);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
completeOperation(eventOpId);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
completeOperation(eventOpId);
|
|
622
|
+
throw err;
|
|
623
|
+
}
|
|
624
|
+
// F12: capture monotonic timestamp BEFORE dispatching — any stdout
|
|
625
|
+
// JSON event counts as activity. This lets the quiet-window
|
|
626
|
+
// detection measure "time since last byte of stdout" accurately
|
|
627
|
+
// regardless of what onJsonEvent does.
|
|
628
|
+
lastStdoutActivityMonotonicMs = performance.now();
|
|
629
|
+
input.onJsonEvent?.(event);
|
|
630
|
+
if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
|
|
631
|
+
finalAssistantEventMonotonicMs = performance.now();
|
|
632
|
+
finalDrainArmed = true; // Phase-0 diagnostic: track that a drain timer was created.
|
|
633
|
+
// F12: alongside the 5 s ceiling timer, start a polling watcher
|
|
634
|
+
// that fires the drain early if stdout goes quiet for `quietMs`
|
|
635
|
+
// after the final assistant event. Heavy children that emit a
|
|
636
|
+
// stopReason=stop message_end and then sit idle will exit in
|
|
637
|
+
// ~quietMs (default 800 ms) instead of up to up to 5 s. unref() so
|
|
638
|
+
// the poller never holds the event loop on shutdown.
|
|
639
|
+
// NOTE: The polling watcher is NOT explicitly cleared on process exit.
|
|
640
|
+
// This is safe because: (1) it's unref()'d, so it won't prevent exit;
|
|
641
|
+
// (2) the `settled || childExited` guard at the top prevents firing
|
|
642
|
+
// after the child has exited; (3) sending SIGTERM to an already-
|
|
643
|
+
// exiting process is harmless. The `finalDrainQuietMs` config allows
|
|
644
|
+
// disabling this behavior (set >= finalDrainMs, e.g., 10000).
|
|
645
|
+
const quietMs = input.finalDrainQuietMs ?? DEFAULT_CHILD_PI.finalDrainQuietMs;
|
|
646
|
+
if (quietMs < (input.finalDrainMs ?? DEFAULT_CHILD_PI.finalDrainMs)) {
|
|
647
|
+
const pollHandle = setInterval(() => {
|
|
648
|
+
if (settled || childExited) {
|
|
649
|
+
clearInterval(pollHandle);
|
|
650
|
+
pollHandle.unref();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const sinceLast = performance.now() - lastStdoutActivityMonotonicMs;
|
|
654
|
+
if (sinceLast >= quietMs) {
|
|
655
|
+
clearInterval(pollHandle);
|
|
656
|
+
pollHandle.unref();
|
|
657
|
+
// Trigger the same drain path as the 5 s timer:
|
|
658
|
+
// mark forced, fire final_drain lifecycle, SIGTERM.
|
|
659
|
+
forcedFinalDrain = true;
|
|
660
|
+
finalDrainFiredMonotonicMs = performance.now();
|
|
661
|
+
input.onLifecycleEvent?.({
|
|
662
|
+
type: "final_drain",
|
|
663
|
+
pid: child.pid,
|
|
664
|
+
ts: new Date().toISOString(),
|
|
665
|
+
reason: "stdout-quiet",
|
|
666
|
+
});
|
|
667
|
+
try {
|
|
668
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
669
|
+
} catch (error) {
|
|
670
|
+
logInternalError("child-pi.quiet-drain-term", error, `pid=${child.pid}`);
|
|
671
|
+
}
|
|
672
|
+
// Mark for hard kill fallback so the existing timer is
|
|
673
|
+
// still reaped if it ever fires later.
|
|
674
|
+
hardKillTimer = setTimeout(() => {
|
|
675
|
+
if (settled || childExited) return;
|
|
676
|
+
try {
|
|
677
|
+
hardKilled = true;
|
|
678
|
+
input.onLifecycleEvent?.({
|
|
679
|
+
type: "hard_kill",
|
|
680
|
+
pid: child.pid,
|
|
681
|
+
ts: new Date().toISOString(),
|
|
682
|
+
});
|
|
683
|
+
child.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
684
|
+
} catch (error) {
|
|
685
|
+
logInternalError("child-pi.quiet-drain-hard-kill", error, `pid=${child.pid}`);
|
|
686
|
+
}
|
|
687
|
+
}, hardKillMs);
|
|
688
|
+
hardKillTimer.unref();
|
|
689
|
+
// Cancel the 5 s ceiling so we don't double-fire.
|
|
690
|
+
if (finalDrainTimer) {
|
|
691
|
+
clearTimeout(finalDrainTimer);
|
|
692
|
+
finalDrainTimer = undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}, 200);
|
|
696
|
+
pollHandle.unref();
|
|
697
|
+
}
|
|
698
|
+
finalDrainTimer = setTimeout(() => {
|
|
699
|
+
if (settled || childExited) return;
|
|
700
|
+
forcedFinalDrain = true;
|
|
701
|
+
finalDrainFiredMonotonicMs = performance.now(); // Phase-0 diagnostic: race timing.
|
|
702
|
+
input.onLifecycleEvent?.({
|
|
703
|
+
type: "final_drain",
|
|
704
|
+
pid: child.pid,
|
|
705
|
+
ts: new Date().toISOString(),
|
|
706
|
+
});
|
|
707
|
+
try {
|
|
708
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
709
|
+
} catch (error) {
|
|
710
|
+
logInternalError("child-pi.final-drain-term", error, `pid=${child.pid}`);
|
|
711
|
+
}
|
|
712
|
+
hardKillTimer = setTimeout(() => {
|
|
713
|
+
if (settled || childExited) return;
|
|
714
|
+
try {
|
|
715
|
+
hardKilled = true;
|
|
716
|
+
input.onLifecycleEvent?.({
|
|
717
|
+
type: "hard_kill",
|
|
718
|
+
pid: child.pid,
|
|
719
|
+
ts: new Date().toISOString(),
|
|
720
|
+
});
|
|
721
|
+
child.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
722
|
+
} catch (error) {
|
|
723
|
+
logInternalError("child-pi.final-drain-kill", error, `pid=${child.pid}`);
|
|
724
|
+
}
|
|
725
|
+
}, hardKillMs);
|
|
726
|
+
hardKillTimer.unref();
|
|
727
|
+
}, finalDrainMs);
|
|
728
|
+
finalDrainTimer.unref();
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const clearFinalDrainTimers = (): void => {
|
|
733
|
+
if (finalDrainTimer) clearTimeout(finalDrainTimer);
|
|
734
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
735
|
+
finalDrainTimer = undefined;
|
|
736
|
+
hardKillTimer = undefined;
|
|
737
|
+
};
|
|
738
|
+
const clearPostExitGuard = (): void => {
|
|
739
|
+
if (postExitGuardCleanup) {
|
|
740
|
+
postExitGuardCleanup();
|
|
741
|
+
postExitGuardCleanup = undefined;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const clearChildPiTimeouts = (): void => {
|
|
745
|
+
clearNoResponseTimer();
|
|
746
|
+
clearFinalDrainTimers();
|
|
747
|
+
clearPostExitGuard();
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const settle = (result: ChildPiRunResult): Promise<void> => {
|
|
751
|
+
if (settled) return Promise.resolve();
|
|
752
|
+
settled = true;
|
|
753
|
+
clearChildPiTimeouts();
|
|
754
|
+
// OPT-06 follow-up: lineObserver.flush() is now async (returns
|
|
755
|
+
// Promise<void>) and drains the module-scoped transcript batch buffer
|
|
756
|
+
// before resolving. We must await it before calling `resolve()`
|
|
757
|
+
// below so callers that read the transcript file post-`runChildPi`
|
|
758
|
+
// see all written lines. Caller invocations of `settle` from
|
|
759
|
+
// sync event handlers (`child.on('close'|'exit'|'error')`,
|
|
760
|
+
// safety timer) use `void settle(...)` — errors are caught and
|
|
761
|
+
// logged inside, and `resolve()` only fires after the drain
|
|
762
|
+
// completes, so runChildPi's outer Promise resolves with a
|
|
763
|
+
// durable transcript on disk.
|
|
764
|
+
return lineObserver
|
|
765
|
+
.flush()
|
|
766
|
+
.then(() => {
|
|
767
|
+
input.signal?.removeEventListener("abort", abort);
|
|
768
|
+
input.signal?.removeEventListener("abort", onParentAbort);
|
|
769
|
+
try {
|
|
770
|
+
cleanupTempDir(tempDir);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
773
|
+
}
|
|
774
|
+
// Catch all errors from settle to prevent unhandled rejection from propagating
|
|
775
|
+
try {
|
|
776
|
+
resolve({
|
|
777
|
+
...result,
|
|
778
|
+
rawFinalText: lineObserver.getRawFinalText(),
|
|
779
|
+
intermediateFindings: lineObserver.getIntermediateFindings(),
|
|
780
|
+
exitStatus: result.exitStatus ?? {
|
|
781
|
+
exitCode: result.exitCode,
|
|
782
|
+
cancelled: abortRequested,
|
|
783
|
+
timedOut: responseTimeoutHit,
|
|
784
|
+
killed: hardKilled,
|
|
785
|
+
// Phase-0 diagnostic (HB-003a): surface the final-drain race state.
|
|
786
|
+
// finalDrainArmed lets Phase 1 decide whether a signal-death (exitCode=null)
|
|
787
|
+
// should be treated as a forced final drain. READ-ONLY for now.
|
|
788
|
+
...(finalDrainArmed || forcedFinalDrain
|
|
789
|
+
? {
|
|
790
|
+
finalDrainArmed,
|
|
791
|
+
forcedFinalDrain,
|
|
792
|
+
finalDrainFiredMonotonicMs,
|
|
793
|
+
}
|
|
794
|
+
: {}),
|
|
795
|
+
cleanupErrors,
|
|
796
|
+
finalDrainMs,
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
} catch (resolveError) {
|
|
800
|
+
logInternalError(
|
|
801
|
+
"child-pi.settle-resolve",
|
|
802
|
+
resolveError,
|
|
803
|
+
`result=${JSON.stringify({ exitCode: result.exitCode })}`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
.catch((flushError) => {
|
|
808
|
+
// Drain failed — log and still resolve so runChildPi doesn't hang.
|
|
809
|
+
logInternalError(
|
|
810
|
+
"child-pi.settle-flush-failed",
|
|
811
|
+
flushError,
|
|
812
|
+
`result=${JSON.stringify({ exitCode: result.exitCode })}`,
|
|
813
|
+
);
|
|
814
|
+
input.signal?.removeEventListener("abort", abort);
|
|
815
|
+
input.signal?.removeEventListener("abort", onParentAbort);
|
|
816
|
+
try {
|
|
817
|
+
cleanupTempDir(tempDir);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
resolve({
|
|
823
|
+
...result,
|
|
824
|
+
rawFinalText: lineObserver.getRawFinalText(),
|
|
825
|
+
intermediateFindings: lineObserver.getIntermediateFindings(),
|
|
826
|
+
exitStatus: result.exitStatus ?? {
|
|
827
|
+
exitCode: result.exitCode,
|
|
828
|
+
cancelled: abortRequested,
|
|
829
|
+
timedOut: responseTimeoutHit,
|
|
830
|
+
killed: hardKilled,
|
|
831
|
+
...(finalDrainArmed || forcedFinalDrain
|
|
832
|
+
? {
|
|
833
|
+
finalDrainArmed,
|
|
834
|
+
forcedFinalDrain,
|
|
835
|
+
finalDrainFiredMonotonicMs,
|
|
836
|
+
}
|
|
837
|
+
: {}),
|
|
838
|
+
cleanupErrors,
|
|
839
|
+
finalDrainMs,
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
} catch (resolveError) {
|
|
843
|
+
logInternalError(
|
|
844
|
+
"child-pi.settle-resolve",
|
|
845
|
+
resolveError,
|
|
846
|
+
`result=${JSON.stringify({ exitCode: result.exitCode })}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const abort = (): void => {
|
|
853
|
+
abortRequested = true;
|
|
854
|
+
clearNoResponseTimer();
|
|
855
|
+
killProcessTree(child.pid, child);
|
|
856
|
+
if (process.platform !== "win32") {
|
|
857
|
+
trySignalChild(child, "SIGTERM");
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
861
|
+
} catch {
|
|
862
|
+
// Ignore kill races.
|
|
863
|
+
}
|
|
864
|
+
// 3.5 — fast-escalate to SIGKILL within 200ms on explicit cancel
|
|
865
|
+
// so /team-cancel completes round-trip well under the operator
|
|
866
|
+
// expectation. The standard finalDrainMs / HARD_KILL_MS paths
|
|
867
|
+
// are for graceful drain, not user-initiated cancel.
|
|
868
|
+
const cancelHardKill = setTimeout(() => {
|
|
869
|
+
if (settled || childExited) return;
|
|
870
|
+
try {
|
|
871
|
+
hardKilled = true;
|
|
872
|
+
child.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logInternalError("child-pi.cancel-fast-kill", error, `pid=${child.pid}`);
|
|
875
|
+
}
|
|
876
|
+
}, 200);
|
|
877
|
+
cancelHardKill.unref();
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
881
|
+
// 3.1 — soft watermark backpressure. When inbound stdout exceeds
|
|
882
|
+
// 256KB before the next macrotask, pause for 50ms so the line
|
|
883
|
+
// observer + ancillary handlers get to drain. Prevents the runaway
|
|
884
|
+
// case where a chatty child saturates the parent event loop.
|
|
885
|
+
const BACKPRESSURE_HIGH = 256 * 1024;
|
|
886
|
+
let backpressureBytes = 0;
|
|
887
|
+
const releaseBackpressure = (): void => {
|
|
888
|
+
backpressureBytes = 0;
|
|
889
|
+
try {
|
|
890
|
+
child.stdout?.resume();
|
|
891
|
+
} catch {
|
|
892
|
+
/* ignore */
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
896
|
+
if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
|
|
897
|
+
const text = chunk.toString("utf-8");
|
|
898
|
+
backpressureBytes += text.length;
|
|
899
|
+
try {
|
|
900
|
+
lineObserver.observe(text);
|
|
901
|
+
} catch (err) {
|
|
902
|
+
logInternalError("child-pi.line-observer-observe", err, `text=${text.slice(0, 100)}`);
|
|
903
|
+
}
|
|
904
|
+
if (backpressureBytes > BACKPRESSURE_HIGH && child.stdout && !child.stdout.isPaused()) {
|
|
905
|
+
try {
|
|
906
|
+
child.stdout.pause();
|
|
907
|
+
} catch {
|
|
908
|
+
/* ignore */
|
|
909
|
+
}
|
|
910
|
+
const timer = setTimeout(releaseBackpressure, 50);
|
|
911
|
+
timer.unref();
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
915
|
+
if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
|
|
916
|
+
stderr = appendBoundedTail(stderr, chunk.toString("utf-8"));
|
|
917
|
+
});
|
|
918
|
+
child.on("error", (error) => {
|
|
919
|
+
// Reject pending operations with process error context
|
|
920
|
+
// SEC-1: redact stderr secrets embedded in the error message + excerpt.
|
|
921
|
+
const processError = new Error(
|
|
922
|
+
`Child Pi process error: ${error.message}. Stderr: ${redactStderrExcerpt(stderr, 500) || "(none)"}`,
|
|
923
|
+
);
|
|
924
|
+
rejectPendingOperations(processError);
|
|
925
|
+
try {
|
|
926
|
+
input.onLifecycleEvent?.({
|
|
927
|
+
type: "spawn_error",
|
|
928
|
+
pid: child.pid,
|
|
929
|
+
error: processError.message,
|
|
930
|
+
ts: new Date().toISOString(),
|
|
931
|
+
stderrExcerpt: redactStderrExcerpt(stderr, 500) || undefined,
|
|
932
|
+
});
|
|
933
|
+
} catch (err) {
|
|
934
|
+
logInternalError("child-pi.on-lifecycle-event", err, `event=error, pid=${child.pid}`);
|
|
935
|
+
}
|
|
936
|
+
void settle({
|
|
937
|
+
exitCode: null,
|
|
938
|
+
stdout,
|
|
939
|
+
stderr,
|
|
940
|
+
error: processError.message,
|
|
941
|
+
exitStatus: {
|
|
942
|
+
exitCode: null,
|
|
943
|
+
cancelled: abortRequested,
|
|
944
|
+
timedOut: responseTimeoutHit,
|
|
945
|
+
killed: false,
|
|
946
|
+
cleanupErrors,
|
|
947
|
+
finalDrainMs,
|
|
948
|
+
crashClass: classifyProcessCrash({
|
|
949
|
+
exitCode: null,
|
|
950
|
+
cancelled: abortRequested,
|
|
951
|
+
timedOut: responseTimeoutHit,
|
|
952
|
+
spawnError: error,
|
|
953
|
+
stderrSnippet: stderr ? redactStderrExcerpt(stderr, 1000) : undefined,
|
|
954
|
+
}).crashClass,
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
child.on("exit", (code, signal) => {
|
|
959
|
+
if (child.pid) {
|
|
960
|
+
unregisterActiveChild(child.pid);
|
|
961
|
+
clearHardKillTimer(child.pid);
|
|
962
|
+
// Unregister from cleanup handler
|
|
963
|
+
unregisterChildProcess(child.pid);
|
|
964
|
+
}
|
|
965
|
+
// Build comprehensive exit error for unexpected exits
|
|
966
|
+
// Round-10 test fix: also require non-zero exit code OR a known abnormal condition.
|
|
967
|
+
// Previously fired "exited unexpectedly" on every clean exit (code=0) because the
|
|
968
|
+
// OS-level 'exit' event fires BEFORE pi's 'agent_end' JSON event reaches the line
|
|
969
|
+
// observer (race). Worker actually succeeded but onLifecycleEvent reported an error.
|
|
970
|
+
const abnormalExit = code !== 0 && code !== null;
|
|
971
|
+
const isUnexpectedExit = !childExited && !settled && !responseTimeoutHit && !abortRequested && abnormalExit;
|
|
972
|
+
const exitError = isUnexpectedExit
|
|
973
|
+
? new Error(
|
|
974
|
+
`Child Pi process exited unexpectedly (code=${code ?? "null"} signal=${signal ?? "null"}). ` +
|
|
975
|
+
`Stderr: ${redactStderrExcerpt(stderr, 1000) || "(none)"}`,
|
|
976
|
+
)
|
|
977
|
+
: null;
|
|
978
|
+
if (exitError) {
|
|
979
|
+
rejectPendingOperations(exitError);
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
// Phase-0 diagnostic (HB-003a): capture signal + drain timing in the
|
|
983
|
+
// exit lifecycle event so the exit-null race is diagnosable instead of
|
|
984
|
+
// opaque. `signal` was previously discarded after building the error msg.
|
|
985
|
+
input.onLifecycleEvent?.({
|
|
986
|
+
type: "exit",
|
|
987
|
+
pid: child.pid,
|
|
988
|
+
exitCode: code,
|
|
989
|
+
ts: new Date().toISOString(),
|
|
990
|
+
error: exitError?.message,
|
|
991
|
+
stderrExcerpt: isUnexpectedExit ? redactStderrExcerpt(stderr, 1000) || undefined : undefined,
|
|
992
|
+
// Phase-0 diagnostic fields (kept optional — no type change required).
|
|
993
|
+
...(signal ? { signal } : {}),
|
|
994
|
+
...(finalDrainArmed || forcedFinalDrain
|
|
995
|
+
? {
|
|
996
|
+
diagnostic: {
|
|
997
|
+
finalDrainArmed,
|
|
998
|
+
forcedFinalDrain,
|
|
999
|
+
finalDrainFiredMonotonicMs,
|
|
1000
|
+
finalAssistantEventMonotonicMs,
|
|
1001
|
+
exitMonotonicMs: performance.now() - spawnMonotonicMs,
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
: {}),
|
|
1005
|
+
});
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
logInternalError("child-pi.on-lifecycle-event", err, `event=exit, pid=${child.pid}`);
|
|
1008
|
+
}
|
|
1009
|
+
childExited = true;
|
|
1010
|
+
clearNoResponseTimer();
|
|
1011
|
+
clearFinalDrainTimers();
|
|
1012
|
+
if (!postExitGuardCleanup) {
|
|
1013
|
+
postExitGuardCleanup = attachPostExitStdioGuard(child, {
|
|
1014
|
+
idleMs: POST_EXIT_STDIO_GUARD_MS,
|
|
1015
|
+
hardMs: HARD_KILL_MS,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
child.on("close", (exitCode) => {
|
|
1020
|
+
if (child.pid) {
|
|
1021
|
+
unregisterActiveChild(child.pid);
|
|
1022
|
+
clearHardKillTimer(child.pid);
|
|
1023
|
+
// Unregister from cleanup handler
|
|
1024
|
+
unregisterChildProcess(child.pid);
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
input.onLifecycleEvent?.({
|
|
1028
|
+
type: "close",
|
|
1029
|
+
pid: child.pid,
|
|
1030
|
+
exitCode,
|
|
1031
|
+
ts: new Date().toISOString(),
|
|
1032
|
+
});
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
logInternalError("child-pi.on-lifecycle-event", err, `event=close, pid=${child.pid}`);
|
|
1035
|
+
}
|
|
1036
|
+
const timeoutError =
|
|
1037
|
+
responseTimeoutHit && !stderr.trim()
|
|
1038
|
+
? {
|
|
1039
|
+
error: `Child Pi produced no new output for ${responseTimeoutMs}ms; process was terminated as unresponsive.`,
|
|
1040
|
+
}
|
|
1041
|
+
: responseTimeoutHit && stderr.trim()
|
|
1042
|
+
? {
|
|
1043
|
+
error: `Child Pi timed out after ${responseTimeoutMs}ms with stderr: ${redactStderrExcerpt(stderr, 500)}`,
|
|
1044
|
+
}
|
|
1045
|
+
: undefined;
|
|
1046
|
+
// M6 fix: log when forced final drain converts non-zero exit to 0.
|
|
1047
|
+
// This is expected in normal operation (child finished cleanly but linger was killed),
|
|
1048
|
+
// but the telemetry helps detect regressions where crashes are hidden.
|
|
1049
|
+
if (forcedFinalDrain && !timeoutError && exitCode !== 0) {
|
|
1050
|
+
logInternalError(
|
|
1051
|
+
"child-pi.final-drain-zero-exit",
|
|
1052
|
+
new Error(`Child exit code overridden to 0 after forced final drain (original=${exitCode})`),
|
|
1053
|
+
`pid=${child.pid}, finalDrainMs=${finalDrainMs}`,
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const finalExitCode = forcedFinalDrain && !timeoutError ? 0 : exitCode;
|
|
1057
|
+
const wasGraceAborted =
|
|
1058
|
+
steeringController.isSoftLimitReached() &&
|
|
1059
|
+
steeringController.getTurnCount() >=
|
|
1060
|
+
(steeringController.getMaxTurns() ?? 0) + (steeringController.getGraceTurns() ?? 5);
|
|
1061
|
+
const wasParentAborted = abortDueToParentSignal && !wasGraceAborted;
|
|
1062
|
+
// steerInjectionFailed is now always false (Phase-1 fix: steer backpressure
|
|
1063
|
+
// is logged, not fatal). The steerError branch is retained for safety in
|
|
1064
|
+
// case a future change reintroduces a fatal steer path.
|
|
1065
|
+
const steerError = steerInjectionFailed ? "Steer injection failed due to stdin backpressure; process killed" : undefined;
|
|
1066
|
+
// P0 crash taxonomy: classify the exit so callers/dashboards can bucket
|
|
1067
|
+
// failure modes (timeout vs cancel vs native panic vs signal …).
|
|
1068
|
+
// The classifier is a pure function; this is the single integration point.
|
|
1069
|
+
const crashClassification = classifyProcessCrash({
|
|
1070
|
+
exitCode: finalExitCode,
|
|
1071
|
+
signal: child.signalCode ?? undefined,
|
|
1072
|
+
cancelled: abortRequested,
|
|
1073
|
+
timedOut: responseTimeoutHit,
|
|
1074
|
+
killed: hardKilled,
|
|
1075
|
+
spawnError: undefined,
|
|
1076
|
+
stderrSnippet: stderr ? redactStderrExcerpt(stderr, 1000) : undefined,
|
|
1077
|
+
});
|
|
1078
|
+
void settle({
|
|
1079
|
+
exitCode: finalExitCode,
|
|
1080
|
+
stdout,
|
|
1081
|
+
stderr,
|
|
1082
|
+
...(timeoutError ? { error: timeoutError.error } : {}),
|
|
1083
|
+
...(steerError ? { error: steerError } : {}),
|
|
1084
|
+
aborted: wasGraceAborted || wasParentAborted,
|
|
1085
|
+
steered: steeringController.isSoftLimitReached() && !wasGraceAborted,
|
|
1086
|
+
exitStatus: {
|
|
1087
|
+
exitCode: finalExitCode,
|
|
1088
|
+
cancelled: abortRequested,
|
|
1089
|
+
timedOut: responseTimeoutHit,
|
|
1090
|
+
killed: hardKilled,
|
|
1091
|
+
cleanupErrors,
|
|
1092
|
+
finalDrainMs,
|
|
1093
|
+
crashClass: crashClassification.crashClass,
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
} finally {
|
|
1099
|
+
// cleanupTempDir is already called inside settle(), but guard against
|
|
1100
|
+
// the case where settle() was never reached (spawn throws synchronously).
|
|
1101
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
1102
|
+
cleanupTempDir(tempDir);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|