pi-crew 0.9.50 → 0.9.52
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 +150 -0
- package/README.md +216 -20
- package/dist/build-meta.json +2222 -1761
- package/dist/index.mjs +18636 -17603
- package/docs/actions-reference.md +431 -1
- package/package.json +19 -9
- package/schema.json +41 -0
- package/scripts/check-bundle-size.mjs +38 -0
- package/scripts/test-runner.mjs +11 -1
- package/skills/distill-software/SKILL.md +24 -1
- package/dist/index.mjs.map +0 -7
- package/docs/FEATURE_INTAKE.md +0 -126
- package/docs/HARNESS.md +0 -87
- package/docs/HARNESS_BACKLOG.md +0 -89
- package/docs/PROJECT_REVIEW.md +0 -270
- package/docs/PROJECT_REVIEW_FIXES.md +0 -342
- package/docs/PROJECT_REVIEW_ROUND4.md +0 -156
- package/docs/PROJECT_REVIEW_ROUND5.md +0 -86
- package/docs/REVIEW-FINDINGS-2026-06 +0 -357
- package/docs/REVIEW-FINDINGS-2026-07-CORE.md +0 -139
- package/docs/REVIEW-FINDINGS-2026-07.md +0 -125
- package/docs/TEST_MATRIX.md +0 -51
- package/docs/archive/README.md +0 -91
- package/docs/archive/followup-review-round3-2026-05-12.md +0 -342
- package/docs/archive/followup-review-round4-2026-05-13.md +0 -107
- package/docs/archive/pi-crew-bugs.md +0 -961
- package/docs/archive/pi-crew-test-final.md +0 -120
- package/docs/archive/pi-crew-test-results.md +0 -260
- package/docs/archive/pi-crew-test-round2.md +0 -136
- package/docs/archive/pi-crew-test-round4.md +0 -100
- package/docs/archive/pi-crew-test-round5.md +0 -70
- package/docs/archive/pi-crew-test-round6.md +0 -110
- package/docs/archive/pi-crew-v0.5.10-audit-fix-plan.md +0 -60
- package/docs/archive/pi-crew-v0.5.11-audit-fix-plan.md +0 -92
- package/docs/archive/pi-crew-v0.5.12-audit-fix-plan.md +0 -76
- package/docs/archive/pi-crew-v0.5.13-audit-fix-plan.md +0 -75
- package/docs/archive/pi-crew-v0.5.14-audit-fix-plan.md +0 -75
- package/docs/archive/pi-crew-v0.5.16-audit-fix-plan.md +0 -35
- package/docs/archive/pi-crew-v0.5.17-audit-fix-plan.md +0 -80
- package/docs/archive/pi-crew-v0.5.5-audit-fix-plan.md +0 -133
- package/docs/archive/pi-crew-v0.5.9-audit-fix-plan.md +0 -88
- package/docs/archive/pi-mono-opportunities.md +0 -969
- package/docs/archive/pi-mono-review.md +0 -291
- package/docs/archive/pi-subagent4-comparison.md +0 -261
- package/docs/archive/pi-subagents3-deep-analysis.md +0 -508
- package/docs/bugs/bug-021-notification-badge-counter-misleading.md +0 -293
- package/docs/bugs/bug-023-chain-windows-path-resolution.md +0 -106
- package/docs/bugs/bug-quota-display-truncation.md +0 -223
- package/docs/bugs/cross-session-notification-leakage.md +0 -82
- package/docs/code-review-2026-05-11.md +0 -592
- package/docs/coding-agent-optimization.md +0 -268
- package/docs/comparison-pi-subagents-vs-pi-crew.md +0 -303
- package/docs/decisions/0001-durable-state.md +0 -41
- package/docs/decisions/0002-child-process-for-async.md +0 -42
- package/docs/decisions/0003-depth-guard.md +0 -36
- package/docs/decisions/0004-execfile-over-exec.md +0 -34
- package/docs/decisions/0005-no-parameter-properties.md +0 -49
- package/docs/decisions/0006-publish-bundled-esm.md +0 -63
- package/docs/decisions/0007-active-run-binary-index.md +0 -54
- package/docs/decisions/0008-child-pi-warm-pool.md +0 -61
- package/docs/decisions/2026-07-21-broker-phase4-default-on.md +0 -77
- package/docs/decisions/2026-07-21-broker-windows-perms.md +0 -91
- package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +0 -99
- package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +0 -112
- package/docs/decisions/README.md +0 -26
- package/docs/deep-review-report.md +0 -384
- package/docs/distillation/cybersecurity-patterns.md +0 -294
- package/docs/feature-analysis-subagent4.md +0 -305
- package/docs/fix-plan-disabletools-exit-null.md +0 -219
- package/docs/fixes/BATCH_A_H1_H2.md +0 -86
- package/docs/fixes/bug-006-foreground-cancel-concurrent.md +0 -78
- package/docs/fixes/bug-007-async-notifier-stale-ctx.md +0 -112
- package/docs/fixes/bug-008-child-process-silent-timeout.md +0 -100
- package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +0 -75
- package/docs/fixes/bug-010-child-process-api-key-filtered.md +0 -109
- package/docs/fixes/bug-011-spawn-pi-enoent.md +0 -92
- package/docs/fixes/bug-012-essential-env-stripped.md +0 -89
- package/docs/fixes/bug-013-background-runner-death.md +0 -84
- package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +0 -82
- package/docs/fixes/bug-015-background-runner-sigterm.md +0 -65
- package/docs/fixes/bug-017-background-runner-session-shutdown.md +0 -66
- package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +0 -28
- package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +0 -61
- package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +0 -52
- package/docs/fixes/bug-020-infinite-retry-loop-needs-attention.md +0 -47
- package/docs/fixes/v0.9.10/locks-fix-verify.md +0 -3
- package/docs/fixes/v0.9.10/smoke-test.md +0 -12
- package/docs/followup-plan-2026-05-12.md +0 -463
- package/docs/followup-review-2026-05-12.md +0 -297
- package/docs/goals.md +0 -59
- package/docs/implementation-plan-top3.md +0 -333
- package/docs/issue-29-analysis.md +0 -189
- package/docs/live-mailbox-runtime.md +0 -36
- package/docs/migration/atomic-write-v2-migration.md +0 -299
- package/docs/next-upgrade-roadmap.md +0 -808
- package/docs/oh-my-pi-research.md +0 -509
- package/docs/optimization-plan-2026-07.md +0 -366
- package/docs/optimization-plan.md +0 -644
- package/docs/patterns/command-agent-skill.md +0 -71
- package/docs/perf/baseline-2026-05.md +0 -113
- package/docs/perf/final-report-2026-05.md +0 -206
- package/docs/perf/optimization-plan-2026-07-verified.md +0 -396
- package/docs/perf/performance-audit-report-2026-07.md +0 -948
- package/docs/perf/performance-review-2026-07.md +0 -287
- package/docs/perf/sprint-1-report.md +0 -71
- package/docs/perf/sprint-2-report.md +0 -81
- package/docs/perf/sprint-2.5-report.md +0 -53
- package/docs/perf/sprint-3-report.md +0 -36
- package/docs/perf/sprint-4-report.md +0 -47
- package/docs/perf/sprint-5-report.md +0 -51
- package/docs/perf/sprint-6-report.md +0 -94
- package/docs/perf/sprint-7-report.md +0 -74
- package/docs/perf/upgrade-plan-2026-05.md +0 -147
- package/docs/performance-optimization-execution-plan.md +0 -520
- package/docs/phase4-triage.md +0 -58
- package/docs/pi-crew-investigation-report.md +0 -411
- package/docs/product/README.md +0 -31
- package/docs/product/platform.md +0 -27
- package/docs/product/runtime-safety.md +0 -37
- package/docs/product/team-run.md +0 -39
- package/docs/product/team-tool.md +0 -37
- package/docs/runtime-analysis-child-vs-live.md +0 -171
- package/docs/runtime-migration-in-process-analysis.md +0 -250
- package/docs/skills/REFERENCE.md +0 -138
- package/docs/stories/README.md +0 -32
- package/docs/stories/US-DEPS-major-upgrade.md +0 -62
- package/docs/stories/backlog.md +0 -36
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +0 -1270
- package/docs/templates/decision.md +0 -27
- package/docs/templates/story.md +0 -44
- package/docs/templates/validation-report.md +0 -32
- package/docs/ui-optimization-plan.md +0 -447
- package/src/adapters/claude-adapter.ts +0 -23
- package/src/adapters/codex-adapter.ts +0 -21
- package/src/adapters/cursor-adapter.ts +0 -17
- package/src/adapters/export-util.ts +0 -143
- package/src/adapters/index.ts +0 -15
- package/src/adapters/registry.ts +0 -18
- package/src/adapters/types.ts +0 -23
- package/src/agents/agent-config.ts +0 -194
- package/src/agents/agent-search.ts +0 -98
- package/src/agents/agent-serializer.ts +0 -38
- package/src/agents/discover-agents.ts +0 -623
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/config.ts +0 -1269
- package/src/config/defaults.ts +0 -193
- package/src/config/drift-detector.ts +0 -290
- package/src/config/markers.ts +0 -330
- package/src/config/resilient-parser.ts +0 -117
- package/src/config/role-tools.ts +0 -118
- package/src/config/suggestions.ts +0 -75
- package/src/config/types.ts +0 -278
- package/src/errors.ts +0 -194
- package/src/extension/action-suggestions.ts +0 -117
- package/src/extension/async-notifier.ts +0 -179
- package/src/extension/autonomous-policy.ts +0 -209
- package/src/extension/command-completions.ts +0 -120
- package/src/extension/context-status-injection.ts +0 -185
- package/src/extension/crew-autocomplete.ts +0 -131
- package/src/extension/crew-cleanup.ts +0 -168
- package/src/extension/crew-input-router.ts +0 -93
- package/src/extension/crew-shortcuts.ts +0 -87
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/crew-vibes/config.ts +0 -195
- package/src/extension/crew-vibes/figures.ts +0 -101
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/extension/crew-vibes/footer.ts +0 -292
- package/src/extension/crew-vibes/index.ts +0 -432
- package/src/extension/crew-vibes/provider-usage.ts +0 -396
- package/src/extension/crew-vibes/render.ts +0 -206
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/extension/cross-extension-rpc.ts +0 -350
- package/src/extension/help.ts +0 -62
- package/src/extension/import-index.ts +0 -74
- package/src/extension/knowledge-injection.ts +0 -402
- package/src/extension/management.ts +0 -571
- package/src/extension/message-renderers.ts +0 -108
- package/src/extension/notification-router.ts +0 -152
- package/src/extension/notification-sink.ts +0 -54
- package/src/extension/pi-api.ts +0 -56
- package/src/extension/plan-orchestrate.ts +0 -302
- package/src/extension/project-init.ts +0 -170
- package/src/extension/register.ts +0 -113
- package/src/extension/registration/artifact-cleanup.ts +0 -20
- package/src/extension/registration/command-registration.ts +0 -58
- package/src/extension/registration/command-utils.ts +0 -58
- package/src/extension/registration/commands.ts +0 -1228
- package/src/extension/registration/compaction-guard.ts +0 -333
- package/src/extension/registration/context-builder.ts +0 -135
- package/src/extension/registration/crash-recovery-cache.ts +0 -54
- package/src/extension/registration/foreground-run-controller.ts +0 -238
- package/src/extension/registration/hook-registration.ts +0 -119
- package/src/extension/registration/lazy-configurers.ts +0 -124
- package/src/extension/registration/lifecycle-handlers.ts +0 -923
- package/src/extension/registration/lifecycle.ts +0 -259
- package/src/extension/registration/observability.ts +0 -322
- package/src/extension/registration/registration-types.ts +0 -158
- package/src/extension/registration/runtime-cleanup.ts +0 -230
- package/src/extension/registration/subagent-helpers.ts +0 -125
- package/src/extension/registration/subagent-manager-setup.ts +0 -330
- package/src/extension/registration/subagent-tools.ts +0 -456
- package/src/extension/registration/team-tool.ts +0 -220
- package/src/extension/registration/tool-registration.ts +0 -50
- package/src/extension/registration/ui.ts +0 -172
- package/src/extension/registration/viewers.ts +0 -121
- package/src/extension/registration/wire-cross-extension.ts +0 -37
- package/src/extension/result-watcher.ts +0 -139
- package/src/extension/rpc-hmac.ts +0 -256
- package/src/extension/run-bundle-schema.ts +0 -104
- package/src/extension/run-export.ts +0 -97
- package/src/extension/run-import.ts +0 -168
- package/src/extension/run-index.ts +0 -131
- package/src/extension/run-maintenance.ts +0 -190
- package/src/extension/session-summary.ts +0 -18
- package/src/extension/team-manager-command.ts +0 -153
- package/src/extension/team-onboard.ts +0 -177
- package/src/extension/team-recommendation.ts +0 -288
- package/src/extension/team-tool/anchor.ts +0 -172
- package/src/extension/team-tool/api.ts +0 -1244
- package/src/extension/team-tool/auto-summarize.ts +0 -142
- package/src/extension/team-tool/cache-control.ts +0 -19
- package/src/extension/team-tool/cancel.ts +0 -337
- package/src/extension/team-tool/chain-dispatch.ts +0 -95
- package/src/extension/team-tool/chain-executor.ts +0 -379
- package/src/extension/team-tool/config-patch.ts +0 -50
- package/src/extension/team-tool/context.ts +0 -149
- package/src/extension/team-tool/destructive-gate.ts +0 -52
- package/src/extension/team-tool/doctor.ts +0 -431
- package/src/extension/team-tool/explain.ts +0 -282
- package/src/extension/team-tool/failure-patterns.ts +0 -118
- package/src/extension/team-tool/goal-wrap.ts +0 -330
- package/src/extension/team-tool/goal.ts +0 -556
- package/src/extension/team-tool/handle-schedule.ts +0 -332
- package/src/extension/team-tool/handle-settings.ts +0 -520
- package/src/extension/team-tool/health-monitor.ts +0 -481
- package/src/extension/team-tool/inspect.ts +0 -88
- package/src/extension/team-tool/intent-policy.ts +0 -45
- package/src/extension/team-tool/lifecycle-actions.ts +0 -615
- package/src/extension/team-tool/orchestrate.ts +0 -98
- package/src/extension/team-tool/parallel-dispatch.ts +0 -183
- package/src/extension/team-tool/plan.ts +0 -50
- package/src/extension/team-tool/respond.ts +0 -150
- package/src/extension/team-tool/run-not-found.ts +0 -49
- package/src/extension/team-tool/run.ts +0 -1035
- package/src/extension/team-tool/status.ts +0 -260
- package/src/extension/team-tool/workflow-manage.ts +0 -261
- package/src/extension/team-tool-types.ts +0 -26
- package/src/extension/team-tool.ts +0 -1116
- package/src/extension/tool-result.ts +0 -16
- package/src/extension/validate-resources.ts +0 -108
- package/src/hooks/registry.ts +0 -200
- package/src/hooks/types.ts +0 -69
- package/src/i18n.ts +0 -212
- package/src/observability/correlation.ts +0 -50
- package/src/observability/event-bus.ts +0 -86
- package/src/observability/event-to-metric.ts +0 -170
- package/src/observability/exporters/adapter.ts +0 -27
- package/src/observability/exporters/otlp-exporter.ts +0 -356
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-registry.ts +0 -100
- package/src/observability/metric-retention.ts +0 -64
- package/src/observability/metric-sink.ts +0 -99
- package/src/observability/metrics-primitives.ts +0 -219
- package/src/plugins/plugin-define.ts +0 -6
- package/src/plugins/plugin-registry.ts +0 -32
- package/src/plugins/plugins/index.ts +0 -3
- package/src/plugins/plugins/nextjs.ts +0 -19
- package/src/plugins/plugins/vite.ts +0 -10
- package/src/plugins/plugins/vitest.ts +0 -9
- package/src/prompt/prompt-runtime.ts +0 -380
- package/src/runtime/adaptive-plan.ts +0 -563
- package/src/runtime/agent-control.ts +0 -215
- package/src/runtime/agent-memory.ts +0 -81
- package/src/runtime/agent-observability.ts +0 -122
- package/src/runtime/anchor-manager.ts +0 -475
- package/src/runtime/async-marker.ts +0 -32
- package/src/runtime/async-runner.ts +0 -381
- package/src/runtime/attention-events.ts +0 -28
- package/src/runtime/auto-summarize.ts +0 -344
- package/src/runtime/background-runner.ts +0 -837
- package/src/runtime/batch-barrier.ts +0 -147
- package/src/runtime/broker-issuer.ts +0 -37
- package/src/runtime/cancellation-token.ts +0 -99
- package/src/runtime/cancellation.ts +0 -99
- package/src/runtime/capability-inventory.ts +0 -137
- package/src/runtime/chain-parser.ts +0 -237
- package/src/runtime/chain-runner.ts +0 -606
- package/src/runtime/checkpoint.ts +0 -291
- package/src/runtime/child-pi-constants.ts +0 -42
- package/src/runtime/child-pi-kill.ts +0 -180
- package/src/runtime/child-pi-pool.ts +0 -68
- package/src/runtime/child-pi-spawn.ts +0 -287
- package/src/runtime/child-pi-steering.ts +0 -128
- package/src/runtime/child-pi-streams.ts +0 -296
- package/src/runtime/child-pi-transcript.ts +0 -169
- package/src/runtime/child-pi.ts +0 -1105
- package/src/runtime/coalesce-tasks.ts +0 -268
- package/src/runtime/code-summary.ts +0 -292
- package/src/runtime/command-trace.ts +0 -105
- package/src/runtime/compact-pipeline.ts +0 -56
- package/src/runtime/compact-stages/ansi-strip-stage.ts +0 -25
- package/src/runtime/compact-stages/blank-collapse-stage.ts +0 -31
- package/src/runtime/compact-stages/deduplicate-stage.ts +0 -34
- package/src/runtime/compact-stages/head-snap-stage.ts +0 -57
- package/src/runtime/compact-stages/index.ts +0 -23
- package/src/runtime/compact-stages/tail-capture-stage.ts +0 -77
- package/src/runtime/compact-stages/truncation-stage.ts +0 -74
- package/src/runtime/compaction-summary.ts +0 -278
- package/src/runtime/completion-guard.ts +0 -207
- package/src/runtime/concurrency.ts +0 -58
- package/src/runtime/crash-classification.ts +0 -229
- package/src/runtime/crash-recovery.ts +0 -577
- package/src/runtime/crew-agent-records.ts +0 -593
- package/src/runtime/crew-agent-runtime.ts +0 -62
- package/src/runtime/crew-broker-child.ts +0 -88
- package/src/runtime/crew-broker-client.ts +0 -673
- package/src/runtime/crew-broker-tokens.ts +0 -84
- package/src/runtime/crew-broker.ts +0 -1276
- package/src/runtime/crew-hooks.ts +0 -227
- package/src/runtime/cross-extension-rpc.ts +0 -143
- package/src/runtime/custom-tools/irc-tool.ts +0 -282
- package/src/runtime/custom-tools/submit-result-tool.ts +0 -102
- package/src/runtime/deadletter.ts +0 -47
- package/src/runtime/delivery-coordinator.ts +0 -211
- package/src/runtime/delta-conflict.ts +0 -348
- package/src/runtime/deterministic-ast.ts +0 -161
- package/src/runtime/diagnostic-export.ts +0 -171
- package/src/runtime/direct-run.ts +0 -45
- package/src/runtime/dwf-state-store.ts +0 -102
- package/src/runtime/dynamic-workflow-context.ts +0 -1016
- package/src/runtime/dynamic-workflow-runner.ts +0 -310
- package/src/runtime/effectiveness.ts +0 -90
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/event-stream-bridge.ts +0 -98
- package/src/runtime/foreground-control.ts +0 -193
- package/src/runtime/foreground-watchdog.ts +0 -132
- package/src/runtime/global-worker-cap.ts +0 -96
- package/src/runtime/goal-achievement.ts +0 -148
- package/src/runtime/goal-evaluator.ts +0 -361
- package/src/runtime/goal-loop-runner.ts +0 -828
- package/src/runtime/goal-state-store.ts +0 -219
- package/src/runtime/green-contract.ts +0 -55
- package/src/runtime/group-join.ts +0 -267
- package/src/runtime/handoff-manager.ts +0 -598
- package/src/runtime/heartbeat-gradient.ts +0 -36
- package/src/runtime/heartbeat-watcher.ts +0 -239
- package/src/runtime/hidden-handoff.ts +0 -407
- package/src/runtime/important-line-classifier.ts +0 -140
- package/src/runtime/intercom-bridge.ts +0 -187
- package/src/runtime/iteration-hooks.ts +0 -305
- package/src/runtime/live-agent-control.ts +0 -136
- package/src/runtime/live-agent-manager.ts +0 -704
- package/src/runtime/live-control-realtime.ts +0 -56
- package/src/runtime/live-extension-bridge.ts +0 -148
- package/src/runtime/live-irc.ts +0 -97
- package/src/runtime/live-session-health.ts +0 -108
- package/src/runtime/live-session-runtime.ts +0 -1139
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/manifest-cache.ts +0 -375
- package/src/runtime/mcp-proxy.ts +0 -105
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/model-fallback.ts +0 -409
- package/src/runtime/model-resolver.ts +0 -126
- package/src/runtime/model-scope.ts +0 -158
- package/src/runtime/orphan-worker-registry.ts +0 -465
- package/src/runtime/output-validator.ts +0 -202
- package/src/runtime/overflow-recovery.ts +0 -206
- package/src/runtime/parallel-research.ts +0 -68
- package/src/runtime/parallel-utils.ts +0 -160
- package/src/runtime/parent-guard.ts +0 -134
- package/src/runtime/path-overlap.ts +0 -150
- package/src/runtime/peer-dep.ts +0 -292
- package/src/runtime/per-write-validator.ts +0 -181
- package/src/runtime/phase-progress.ts +0 -217
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pi-args.ts +0 -655
- package/src/runtime/pi-json-output.ts +0 -170
- package/src/runtime/pi-spawn.ts +0 -273
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/plan-templates.ts +0 -201
- package/src/runtime/policy-engine.ts +0 -115
- package/src/runtime/post-checks.ts +0 -142
- package/src/runtime/post-exit-stdio-guard.ts +0 -104
- package/src/runtime/process-lifecycle.ts +0 -491
- package/src/runtime/process-status.ts +0 -154
- package/src/runtime/progress-event-coalescer.ts +0 -44
- package/src/runtime/progress-tracker.ts +0 -124
- package/src/runtime/prose-compressor.ts +0 -162
- package/src/runtime/recovery-recipes.ts +0 -203
- package/src/runtime/replace.ts +0 -570
- package/src/runtime/resilient-edit.ts +0 -153
- package/src/runtime/result-extractor.ts +0 -213
- package/src/runtime/retry-executor.ts +0 -100
- package/src/runtime/retry-runner.ts +0 -336
- package/src/runtime/role-permission.ts +0 -59
- package/src/runtime/run-coalesced-task-group.ts +0 -308
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/run-tracker.ts +0 -116
- package/src/runtime/runtime-policy.ts +0 -29
- package/src/runtime/runtime-resolver.ts +0 -172
- package/src/runtime/runtime-warmup.ts +0 -160
- package/src/runtime/scheduler.ts +0 -353
- package/src/runtime/semaphore.ts +0 -141
- package/src/runtime/sensitive-paths.ts +0 -93
- package/src/runtime/session-resources.ts +0 -25
- package/src/runtime/session-snapshot.ts +0 -59
- package/src/runtime/session-usage.ts +0 -79
- package/src/runtime/settings-store.ts +0 -155
- package/src/runtime/sidechain-output.ts +0 -35
- package/src/runtime/single-agent-compose.ts +0 -84
- package/src/runtime/skill-effectiveness.ts +0 -524
- package/src/runtime/skill-instructions.ts +0 -391
- package/src/runtime/stale-reconciler.ts +0 -680
- package/src/runtime/stream-preview.ts +0 -184
- package/src/runtime/streaming-output.ts +0 -50
- package/src/runtime/subagent-manager.ts +0 -561
- package/src/runtime/subprocess-tool-registry.ts +0 -70
- package/src/runtime/supervisor-contact.ts +0 -64
- package/src/runtime/task-display.ts +0 -52
- package/src/runtime/task-graph-scheduler.ts +0 -227
- package/src/runtime/task-graph.ts +0 -290
- package/src/runtime/task-health.ts +0 -83
- package/src/runtime/task-id.ts +0 -161
- package/src/runtime/task-output-context.ts +0 -602
- package/src/runtime/task-packet.ts +0 -268
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/capabilities.ts +0 -78
- package/src/runtime/task-runner/context-retrieval.ts +0 -163
- package/src/runtime/task-runner/live-executor.ts +0 -227
- package/src/runtime/task-runner/output-splitter.ts +0 -152
- package/src/runtime/task-runner/progress.ts +0 -157
- package/src/runtime/task-runner/prompt-builder.ts +0 -278
- package/src/runtime/task-runner/prompt-pipeline.ts +0 -88
- package/src/runtime/task-runner/result-utils.ts +0 -16
- package/src/runtime/task-runner/retrieval-orchestrator.ts +0 -310
- package/src/runtime/task-runner/run-projection.ts +0 -125
- package/src/runtime/task-runner/state-helpers.ts +0 -176
- package/src/runtime/task-runner/tail-read.ts +0 -34
- package/src/runtime/task-runner.ts +0 -1420
- package/src/runtime/team-runner-artifacts.ts +0 -13
- package/src/runtime/team-runner.ts +0 -1993
- package/src/runtime/tool-output-pruner.ts +0 -333
- package/src/runtime/tool-progress.ts +0 -278
- package/src/runtime/usage-tracker.ts +0 -73
- package/src/runtime/verification-gates.ts +0 -571
- package/src/runtime/verification-integrity.ts +0 -105
- package/src/runtime/verification-worktree.ts +0 -186
- package/src/runtime/worker-heartbeat.ts +0 -25
- package/src/runtime/worker-startup.ts +0 -83
- package/src/runtime/workflow-state.ts +0 -195
- package/src/runtime/workspace-lock.ts +0 -443
- package/src/runtime/workspace-tree.ts +0 -312
- package/src/runtime/yield-handler.ts +0 -224
- package/src/runtime/zombie-scanner.ts +0 -298
- package/src/schema/config-schema.ts +0 -327
- package/src/schema/team-tool-schema.ts +0 -415
- package/src/schema/validation-types.ts +0 -159
- package/src/skills/discover-skills.ts +0 -197
- package/src/skills/skill-templates.ts +0 -456
- package/src/skills/validate.ts +0 -285
- package/src/state/active-run-registry.ts +0 -439
- package/src/state/artifact-store.ts +0 -176
- package/src/state/atomic-write.ts +0 -903
- package/src/state/blob-store.ts +0 -308
- package/src/state/contracts.ts +0 -158
- package/src/state/crew-init.ts +0 -269
- package/src/state/decision-ledger.ts +0 -345
- package/src/state/event-log-rotation.ts +0 -342
- package/src/state/event-log.ts +0 -1311
- package/src/state/event-reconstructor.ts +0 -229
- package/src/state/gitignore-manager.ts +0 -46
- package/src/state/health-store.ts +0 -79
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/instinct-store.ts +0 -275
- package/src/state/jsonl-writer.ts +0 -107
- package/src/state/locks.ts +0 -562
- package/src/state/mailbox.ts +0 -916
- package/src/state/observation-store.ts +0 -176
- package/src/state/run-cache.ts +0 -193
- package/src/state/run-graph.ts +0 -183
- package/src/state/run-metrics.ts +0 -165
- package/src/state/schedule.ts +0 -166
- package/src/state/session-state-map.ts +0 -51
- package/src/state/state-store.ts +0 -958
- package/src/state/task-claims.ts +0 -61
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/state/types.ts +0 -440
- package/src/state/usage.ts +0 -138
- package/src/state/worker-atomic-writer.ts +0 -194
- package/src/subagents/async-entry.ts +0 -1
- package/src/subagents/index.ts +0 -3
- package/src/subagents/live/control.ts +0 -1
- package/src/subagents/live/manager.ts +0 -1
- package/src/subagents/live/realtime.ts +0 -1
- package/src/subagents/live/session-runtime.ts +0 -1
- package/src/subagents/manager.ts +0 -1
- package/src/subagents/spawn.ts +0 -1
- package/src/teams/discover-teams.ts +0 -187
- package/src/teams/team-config.ts +0 -27
- package/src/teams/team-serializer.ts +0 -38
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/types/diff.d.ts +0 -18
- package/src/types/new-api-types.ts +0 -35
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/card-colors.ts +0 -139
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -111
- package/src/ui/dashboard-panes/agents-pane.ts +0 -164
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -66
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/dashboard-panes/health-pane.ts +0 -31
- package/src/ui/dashboard-panes/mailbox-pane.ts +0 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +0 -37
- package/src/ui/dashboard-panes/progress-pane.ts +0 -38
- package/src/ui/dashboard-panes/transcript-pane.ts +0 -10
- package/src/ui/deploy-bundled-themes.ts +0 -71
- package/src/ui/dwf-phase-display.ts +0 -151
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/heartbeat-aggregator.ts +0 -74
- package/src/ui/key-utils.ts +0 -42
- package/src/ui/keybinding-map.ts +0 -248
- package/src/ui/layout-primitives.ts +0 -106
- package/src/ui/live-conversation-overlay.ts +0 -183
- package/src/ui/live-duration.ts +0 -55
- package/src/ui/live-run-sidebar.ts +0 -301
- package/src/ui/loaders.ts +0 -176
- package/src/ui/mascot.ts +0 -436
- package/src/ui/overlays/agent-picker-overlay.ts +0 -65
- package/src/ui/overlays/confirm-overlay.ts +0 -60
- package/src/ui/overlays/help-overlay.ts +0 -177
- package/src/ui/overlays/mailbox-compose-overlay.ts +0 -181
- package/src/ui/overlays/mailbox-compose-preview.ts +0 -78
- package/src/ui/overlays/mailbox-detail-overlay.ts +0 -154
- package/src/ui/pi-ui-compat.ts +0 -68
- package/src/ui/powerbar-publisher.ts +0 -459
- package/src/ui/render-coalescer.ts +0 -101
- package/src/ui/render-diff.ts +0 -160
- package/src/ui/render-scheduler.ts +0 -224
- package/src/ui/run-action-dispatcher.ts +0 -182
- package/src/ui/run-dashboard.ts +0 -913
- package/src/ui/run-event-bus.ts +0 -381
- package/src/ui/run-snapshot-cache.ts +0 -1057
- package/src/ui/settings-overlay.ts +0 -1067
- package/src/ui/snapshot-types.ts +0 -90
- package/src/ui/spinner.ts +0 -17
- package/src/ui/status-colors.ts +0 -143
- package/src/ui/syntax-highlight.ts +0 -136
- package/src/ui/terminal-status.ts +0 -269
- package/src/ui/theme-adapter.ts +0 -271
- package/src/ui/theme-discovery.ts +0 -199
- package/src/ui/tool-progress-formatter.ts +0 -93
- package/src/ui/tool-render.ts +0 -453
- package/src/ui/tool-renderers/brief-mode.ts +0 -225
- package/src/ui/tool-renderers/index.ts +0 -726
- package/src/ui/transcript-cache.ts +0 -128
- package/src/ui/transcript-entries.ts +0 -256
- package/src/ui/transcript-viewer.ts +0 -424
- package/src/ui/widget/index.ts +0 -357
- package/src/ui/widget/widget-formatters.ts +0 -182
- package/src/ui/widget/widget-model.ts +0 -110
- package/src/ui/widget/widget-renderer.ts +0 -182
- package/src/ui/widget/widget-types.ts +0 -36
- package/src/utils/bm25-search.ts +0 -235
- package/src/utils/completion-dedupe.ts +0 -63
- package/src/utils/conflict-detect.ts +0 -668
- package/src/utils/env-allowlist.ts +0 -30
- package/src/utils/env-filter.ts +0 -155
- package/src/utils/file-coalescer.ts +0 -98
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/frontmatter.ts +0 -70
- package/src/utils/fs-watch.ts +0 -37
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/git.ts +0 -260
- package/src/utils/guards.ts +0 -107
- package/src/utils/ids.ts +0 -24
- package/src/utils/incremental-reader.ts +0 -220
- package/src/utils/internal-error.ts +0 -10
- package/src/utils/names.ts +0 -36
- package/src/utils/ndjson.ts +0 -115
- package/src/utils/paths.ts +0 -227
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/redaction.ts +0 -347
- package/src/utils/resolve-shell.ts +0 -36
- package/src/utils/run-watcher-registry.ts +0 -152
- package/src/utils/safe-paths.ts +0 -383
- package/src/utils/scan-cache.ts +0 -144
- package/src/utils/session-utils.ts +0 -110
- package/src/utils/sleep.ts +0 -54
- package/src/utils/socket-path.ts +0 -127
- package/src/utils/sse-parser.ts +0 -129
- package/src/utils/task-name-generator.ts +0 -337
- package/src/utils/timings.ts +0 -33
- package/src/utils/token-counter.ts +0 -200
- package/src/utils/visual.ts +0 -207
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/discover-workflows.ts +0 -290
- package/src/workflows/intermediate-store.ts +0 -166
- package/src/workflows/preflight-validator.ts +0 -168
- package/src/workflows/topology-analyzer.ts +0 -203
- package/src/workflows/validate-workflow.ts +0 -50
- package/src/workflows/workflow-config.ts +0 -70
- package/src/workflows/workflow-serializer.ts +0 -32
- package/src/worktree/branch-freshness.ts +0 -112
- package/src/worktree/cleanup.ts +0 -341
- package/src/worktree/worktree-manager.ts +0 -1200
package/src/state/state-store.ts
DELETED
|
@@ -1,958 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as fsp from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
|
-
import { errors } from "../errors.ts";
|
|
6
|
-
import type { TeamConfig } from "../teams/team-config.ts";
|
|
7
|
-
import { createRunId, createTaskId } from "../utils/ids.ts";
|
|
8
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
|
-
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
10
|
-
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
11
|
-
import { toPiSessionId } from "../utils/session-utils.ts";
|
|
12
|
-
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
13
|
-
import { unregisterActiveRun } from "./active-run-registry.ts";
|
|
14
|
-
import { atomicWriteJson, atomicWriteJsonAsync, atomicWriteJsonCoalesced, flushPendingAtomicWrites, readJsonFile } from "./atomic-write.ts";
|
|
15
|
-
import { canTransitionRunStatus } from "./contracts.ts";
|
|
16
|
-
import { appendEvent } from "./event-log.ts";
|
|
17
|
-
import { withRunLock, withRunLockSync } from "./locks.ts";
|
|
18
|
-
import type { TeamRunManifest, TeamTaskState } from "./types.ts";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* stat() the manifest with a brief retry on Windows for the AV-scan window.
|
|
22
|
-
*
|
|
23
|
-
* On the GitHub Actions windows-latest runner, Windows Defender real-time
|
|
24
|
-
* scanning can make a freshly-written manifest.json briefly invisible to
|
|
25
|
-
* statSync (ENOENT) even though the write succeeded and the file is on disk.
|
|
26
|
-
* loadRunManifestById is called right after createRunManifest in tests and in
|
|
27
|
-
* production (e.g. refreshPersistedSubagentRecord), so without a retry the
|
|
28
|
-
* caller sees a phantom "missing" run.
|
|
29
|
-
*
|
|
30
|
-
* On non-Windows, ENOENT means the file genuinely doesn't exist — passthrough
|
|
31
|
-
* (throw immediately) with no retry. On Windows, ENOENT/EPERM/EBUSY/EAGAIN get
|
|
32
|
-
* a handful of short retries (~30ms worst case) before giving up and throwing
|
|
33
|
-
* so the caller's catch returns undefined as before.
|
|
34
|
-
*/
|
|
35
|
-
function statManifestWithWindowsRetry(manifestPath: string): fs.Stats {
|
|
36
|
-
if (process.platform !== "win32") return fs.statSync(manifestPath);
|
|
37
|
-
const retryable = new Set(["ENOENT", "EPERM", "EBUSY", "EAGAIN"]);
|
|
38
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
39
|
-
try {
|
|
40
|
-
return fs.statSync(manifestPath);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
43
|
-
if (!retryable.has(code ?? "")) throw error;
|
|
44
|
-
const end = Date.now() + Math.min(8, 1 * 2 ** attempt);
|
|
45
|
-
while (Date.now() < end) {
|
|
46
|
-
/* brief spin to ride out the AV scan window */
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return fs.statSync(manifestPath); // last attempt — let caller's catch handle ENOENT
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface RunPaths {
|
|
54
|
-
runId: string;
|
|
55
|
-
stateRoot: string;
|
|
56
|
-
artifactsRoot: string;
|
|
57
|
-
manifestPath: string;
|
|
58
|
-
tasksPath: string;
|
|
59
|
-
eventsPath: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ManifestCacheEntry {
|
|
63
|
-
manifest: TeamRunManifest;
|
|
64
|
-
tasks: TeamTaskState[];
|
|
65
|
-
manifestMtimeMs: number;
|
|
66
|
-
manifestSize: number;
|
|
67
|
-
tasksMtimeMs: number;
|
|
68
|
-
tasksSize: number;
|
|
69
|
-
cachedAt?: number;
|
|
70
|
-
generation?: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// F1: per-stateRoot generation counter. The previous global counter was
|
|
74
|
-
// incremented on every `invalidateRunCache` call regardless of which state's
|
|
75
|
-
// cache was invalidated, so a write to run A caused a hot-path miss in run B's
|
|
76
|
-
// cache even though B's on-disk files were untouched. With per-stateRoot
|
|
77
|
-
// generations, run B only misses its own cache when its own writes happen.
|
|
78
|
-
// NOTE: This generation counter is process-local. Cross-process consistency
|
|
79
|
-
// (e.g., parent + child processes writing to the same run) relies on the
|
|
80
|
-
// mtime/size checks in loadRunManifestById, not on this counter.
|
|
81
|
-
const manifestCacheGeneration = new Map<string, number>();
|
|
82
|
-
function genOf(stateRoot: string): number {
|
|
83
|
-
return manifestCacheGeneration.get(stateRoot) ?? 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const MANIFEST_CACHE_TTL_MS = 60 * 1000; // 60 seconds (FIX: increased from 15s for read-heavy workloads; render tick 160ms caused frequent misses)
|
|
87
|
-
const LOAD_MANIFEST_RETRY_LIMIT = 5; // Configurable retry limit for mtime/size stability checks under contention
|
|
88
|
-
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
89
|
-
|
|
90
|
-
/** @internal — exported for TTL-eviction unit testing (Round 19). */
|
|
91
|
-
export function __test__setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
|
|
92
|
-
setManifestCache(stateRoot, entry);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** @internal — exported for TTL-eviction unit testing (Round 19). */
|
|
96
|
-
export function __test__getManifestCacheEntry(stateRoot: string): ManifestCacheEntry | undefined {
|
|
97
|
-
return manifestCache.get(stateRoot);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** @internal — the TTL in ms used for manifest cache eviction. */
|
|
101
|
-
export const MANIFEST_CACHE_TTL_MS_VALUE = MANIFEST_CACHE_TTL_MS;
|
|
102
|
-
|
|
103
|
-
function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
|
|
104
|
-
if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot);
|
|
105
|
-
entry.cachedAt = Date.now();
|
|
106
|
-
// Stamp with the *current* per-root generation so a concurrent writer's
|
|
107
|
-
// increment between read and write invalidates this cache hit.
|
|
108
|
-
entry.generation = genOf(stateRoot);
|
|
109
|
-
// FIX: Evict all stale entries by TTL before adding new entry.
|
|
110
|
-
// This ensures entries that are never accessed still get evicted
|
|
111
|
-
// based on TTL, not just entries that are hit.
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
for (const [key, val] of manifestCache.entries()) {
|
|
114
|
-
if (val.cachedAt && now - val.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
115
|
-
manifestCache.delete(key);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
manifestCache.set(stateRoot, entry);
|
|
119
|
-
while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
|
|
120
|
-
// FIX: Evict oldest entry by cachedAt (LRU), not insertion order.
|
|
121
|
-
// cachedAt is set on both initial insertion and cache hits, so
|
|
122
|
-
// frequently accessed entries bubble to the end and survive longer.
|
|
123
|
-
let oldestKey: string | undefined;
|
|
124
|
-
let oldestTime = Infinity;
|
|
125
|
-
for (const [key, val] of manifestCache.entries()) {
|
|
126
|
-
const t = val.cachedAt ?? 0;
|
|
127
|
-
if (t < oldestTime) {
|
|
128
|
-
oldestTime = t;
|
|
129
|
-
oldestKey = key;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (!oldestKey) break;
|
|
133
|
-
manifestCache.delete(oldestKey);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function useProjectState(cwd: string): boolean {
|
|
138
|
-
return findRepoRoot(cwd) !== undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function invalidateRunCache(stateRoot: string): void {
|
|
142
|
-
manifestCache.delete(stateRoot);
|
|
143
|
-
// F1: only bump the generation of THIS stateRoot — sibling runs keep their
|
|
144
|
-
// generation intact, so concurrent writes to run A no longer evict run B's
|
|
145
|
-
// hot cache.
|
|
146
|
-
manifestCacheGeneration.set(stateRoot, genOf(stateRoot) + 1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function scopeBaseRoot(cwd: string): string {
|
|
150
|
-
return useProjectState(cwd) ? projectCrewRoot(cwd) : userCrewRoot();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
|
|
154
|
-
assertSafePathId("runId", runId);
|
|
155
|
-
const runsRoot = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
|
|
156
|
-
const scopedPath = resolveContainedRelativePath(runsRoot, runId, "runId");
|
|
157
|
-
try {
|
|
158
|
-
// Single atomic validation: resolves through symlinks via realpath,
|
|
159
|
-
// verifies containment within runsRoot, and throws ENOENT if missing.
|
|
160
|
-
// Eliminates the TOCTOU window from the previous existsSync + lstatSync
|
|
161
|
-
// + resolveRealContainedPath sequence.
|
|
162
|
-
resolveRealContainedPath(runsRoot, runId);
|
|
163
|
-
} catch {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
return scopedPath;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunManifest, stateRoot: string, tasksPath: string): boolean {
|
|
170
|
-
// Issue 2 fix: Reject manifests missing status field to prevent undefined
|
|
171
|
-
// behavior in callers like canTransitionRunStatus(manifest.status, newStatus).
|
|
172
|
-
if (!manifest.status || typeof manifest.status !== "string") return false;
|
|
173
|
-
if (
|
|
174
|
-
manifest.runId !== runId ||
|
|
175
|
-
manifest.stateRoot !== stateRoot ||
|
|
176
|
-
manifest.tasksPath !== tasksPath ||
|
|
177
|
-
manifest.eventsPath !== path.join(stateRoot, "events.jsonl")
|
|
178
|
-
)
|
|
179
|
-
return false;
|
|
180
|
-
const artifactsParent = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir);
|
|
181
|
-
const expectedArtifactsRoot = resolveContainedRelativePath(artifactsParent, runId, "runId");
|
|
182
|
-
if (manifest.artifactsRoot !== expectedArtifactsRoot) return false;
|
|
183
|
-
// Always validate artifactsRoot is not a symlink, even when manifest has
|
|
184
|
-
// no artifacts entries. A symlinked artifactsRoot pointing outside the
|
|
185
|
-
// artifacts parent is a security violation (could write to attacker-
|
|
186
|
-
// controlled locations). This check catches the case where the run was
|
|
187
|
-
// created legitimately but the artifactsRoot was later replaced with a
|
|
188
|
-
// symlink between manifest creation and load.
|
|
189
|
-
if (fs.existsSync(expectedArtifactsRoot)) {
|
|
190
|
-
try {
|
|
191
|
-
if (fs.lstatSync(expectedArtifactsRoot).isSymbolicLink()) return false;
|
|
192
|
-
resolveRealContainedPath(artifactsParent, runId);
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
} else if (manifest.artifacts && manifest.artifacts.length > 0) {
|
|
197
|
-
// Has artifacts entries but directory doesn't exist - benign state for
|
|
198
|
-
// runs still in progress.
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
|
|
205
|
-
if (!cwd || typeof cwd !== "string") {
|
|
206
|
-
throw new Error(`Invalid cwd: ${cwd}`);
|
|
207
|
-
}
|
|
208
|
-
assertSafePathId("runId", runId);
|
|
209
|
-
const baseRoot = scopeBaseRoot(cwd);
|
|
210
|
-
const stateRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir), runId, "runId");
|
|
211
|
-
const artifactsRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
|
|
212
|
-
return {
|
|
213
|
-
runId,
|
|
214
|
-
stateRoot,
|
|
215
|
-
artifactsRoot,
|
|
216
|
-
manifestPath: path.join(stateRoot, DEFAULT_PATHS.state.manifestFile),
|
|
217
|
-
tasksPath: path.join(stateRoot, DEFAULT_PATHS.state.tasksFile),
|
|
218
|
-
eventsPath: path.join(stateRoot, DEFAULT_PATHS.state.eventsFile),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig, team: TeamConfig, cwd: string): TeamTaskState[] {
|
|
223
|
-
const stepToTaskId = new Map(workflow.steps.map((step, index) => [step.id, createTaskId(step.id, index)]));
|
|
224
|
-
return workflow.steps.map((step, index) => {
|
|
225
|
-
const role = team.roles.find((candidate) => candidate.name === step.role);
|
|
226
|
-
const id = stepToTaskId.get(step.id) ?? createTaskId(step.id, index);
|
|
227
|
-
const dependencies = step.dependsOn ?? [];
|
|
228
|
-
const children = workflow.steps
|
|
229
|
-
.filter((candidate) => candidate.dependsOn?.includes(step.id))
|
|
230
|
-
.map((candidate) => stepToTaskId.get(candidate.id))
|
|
231
|
-
.filter((childId): childId is string => childId !== undefined);
|
|
232
|
-
return {
|
|
233
|
-
id,
|
|
234
|
-
runId,
|
|
235
|
-
stepId: step.id,
|
|
236
|
-
role: step.role,
|
|
237
|
-
agent: role?.agent ?? step.role,
|
|
238
|
-
title: step.id,
|
|
239
|
-
status: "queued",
|
|
240
|
-
dependsOn: dependencies,
|
|
241
|
-
cwd,
|
|
242
|
-
model: step.model,
|
|
243
|
-
graph: {
|
|
244
|
-
taskId: id,
|
|
245
|
-
parentId: dependencies[0] ? stepToTaskId.get(dependencies[0]) : undefined,
|
|
246
|
-
children,
|
|
247
|
-
dependencies: dependencies.map((dep) => stepToTaskId.get(dep) ?? dep),
|
|
248
|
-
queue: dependencies.length ? "blocked" : "ready",
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function createRunManifest(params: {
|
|
255
|
-
cwd: string;
|
|
256
|
-
team: TeamConfig;
|
|
257
|
-
workflow?: WorkflowConfig;
|
|
258
|
-
goal: string;
|
|
259
|
-
workspaceMode?: "single" | "worktree";
|
|
260
|
-
ownerSessionId?: string;
|
|
261
|
-
runKind?: "team-run" | "goal-loop" | "dynamic-workflow";
|
|
262
|
-
/** round-14 P1-5: typed workflow arguments for .dwf.ts scripts (ctx.args<T>()). */
|
|
263
|
-
args?: unknown;
|
|
264
|
-
}): { manifest: TeamRunManifest; tasks: TeamTaskState[]; paths: RunPaths } {
|
|
265
|
-
const paths = createRunPaths(params.cwd);
|
|
266
|
-
const now = new Date().toISOString();
|
|
267
|
-
const tasks = params.workflow ? createTasksFromWorkflow(paths.runId, params.workflow, params.team, params.cwd) : [];
|
|
268
|
-
const manifest: TeamRunManifest = {
|
|
269
|
-
schemaVersion: 1,
|
|
270
|
-
runId: paths.runId,
|
|
271
|
-
sessionId: toPiSessionId(paths.runId),
|
|
272
|
-
team: params.team.name,
|
|
273
|
-
workflow: params.workflow?.name,
|
|
274
|
-
goal: params.goal,
|
|
275
|
-
status: "queued",
|
|
276
|
-
workspaceMode: params.workspaceMode ?? params.team.workspaceMode ?? "single",
|
|
277
|
-
createdAt: now,
|
|
278
|
-
updatedAt: now,
|
|
279
|
-
cwd: params.cwd,
|
|
280
|
-
stateRoot: paths.stateRoot,
|
|
281
|
-
artifactsRoot: paths.artifactsRoot,
|
|
282
|
-
tasksPath: paths.tasksPath,
|
|
283
|
-
eventsPath: paths.eventsPath,
|
|
284
|
-
artifacts: [],
|
|
285
|
-
...(params.ownerSessionId ? { ownerSessionId: params.ownerSessionId } : {}),
|
|
286
|
-
runKind: params.runKind ?? "team-run",
|
|
287
|
-
...(params.args !== undefined ? { args: params.args } : {}),
|
|
288
|
-
};
|
|
289
|
-
fs.mkdirSync(paths.stateRoot, { recursive: true });
|
|
290
|
-
fs.mkdirSync(paths.artifactsRoot, { recursive: true });
|
|
291
|
-
// FIX: Use saveManifestAndTasksAtomicSync and check result to detect
|
|
292
|
-
// partial failure. If tasksWritten=false when manifestWritten=true,
|
|
293
|
-
// throw to ensure manifest and tasks are always consistent.
|
|
294
|
-
const result = saveManifestAndTasksAtomicSync(manifest, tasks);
|
|
295
|
-
if (!result.manifestWritten || !result.tasksWritten) {
|
|
296
|
-
// Surface the underlying error message (result.error is String(err) from
|
|
297
|
-
// saveManifestAndTasksAtomicSync). Passing it through errors.fileWrite as a
|
|
298
|
-
// fake ErrnoException loses the message (reads .code → undefined →
|
|
299
|
-
// "unknown"). Include it explicitly in the thrown message so CI logs and
|
|
300
|
-
// production callers can see WHY the write failed instead of ": unknown".
|
|
301
|
-
const cause = result.error ? `: ${result.error}` : "";
|
|
302
|
-
throw errors
|
|
303
|
-
.fileWrite(paths.stateRoot, {
|
|
304
|
-
code: "EWRITEFAIL",
|
|
305
|
-
} as NodeJS.ErrnoException)
|
|
306
|
-
.withContext(
|
|
307
|
-
`saveManifestAndTasksAtomicSync: manifestWritten=${result.manifestWritten}, tasksWritten=${result.tasksWritten}${cause}`,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
appendEvent(paths.eventsPath, {
|
|
311
|
-
type: "run.created",
|
|
312
|
-
runId: paths.runId,
|
|
313
|
-
data: { team: params.team.name, workflow: params.workflow?.name },
|
|
314
|
-
metadata: {
|
|
315
|
-
seq: 1,
|
|
316
|
-
provenance: "team_runner",
|
|
317
|
-
sessionIdentity: {
|
|
318
|
-
title: params.team.name,
|
|
319
|
-
workspace: params.cwd,
|
|
320
|
-
purpose: params.goal,
|
|
321
|
-
},
|
|
322
|
-
ownership: {
|
|
323
|
-
owner: params.team.name,
|
|
324
|
-
workflowScope: params.workflow?.name ?? "manual",
|
|
325
|
-
watcherAction: "act",
|
|
326
|
-
},
|
|
327
|
-
confidence: "high",
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
invalidateRunCache(paths.stateRoot);
|
|
331
|
-
return { manifest, tasks, paths };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function saveRunManifest(manifest: TeamRunManifest): void {
|
|
335
|
-
// FIX: Capture the cached tasks array + mtime/size BEFORE we invalidate the
|
|
336
|
-
// cache. The previous implementation re-read tasks.json from disk after the
|
|
337
|
-
// manifest write (a JSON.parse + fs.readFileSync per call), which defeated
|
|
338
|
-
// the whole point of the manifest cache on the hot update path. Reusing the
|
|
339
|
-
// already-cached entry is safe because:
|
|
340
|
-
// 1. The cache entry's tasks array matches the tasks array reflected by
|
|
341
|
-
// its tasksMtimeMs/tasksSize — the three values move together.
|
|
342
|
-
// 2. After invalidate, saveRunTasks (or any other writer) would bump the
|
|
343
|
-
// per-stateRoot generation, so a stale cache hit cannot serve.
|
|
344
|
-
// 3. On cache miss, we fall back to tasks: [] with mtime/size 0 — the same
|
|
345
|
-
// shape the original pre-FIX code produced — so behavior is unchanged.
|
|
346
|
-
const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
|
|
347
|
-
const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
|
|
348
|
-
const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
|
|
349
|
-
const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
|
|
350
|
-
// FIX: Invalidate cache BEFORE atomic write. The order matters for crash
|
|
351
|
-
// safety: if we invalidated after the write and crashed before invalidation,
|
|
352
|
-
// the stale cache entry (up to MANIFEST_CACHE_TTL_MS old) could be served.
|
|
353
|
-
// By invalidating first, the worst case is a cache miss forcing a disk read,
|
|
354
|
-
// which is always safe.
|
|
355
|
-
invalidateRunCache(manifest.stateRoot);
|
|
356
|
-
const manifestPath = path.join(manifest.stateRoot, "manifest.json");
|
|
357
|
-
atomicWriteJson(manifestPath, manifest);
|
|
358
|
-
// FIX: Re-populate cache with actual mtime/size so loadRunManifestById
|
|
359
|
-
// doesn't miss the cache on next read. Without this, every load until
|
|
360
|
-
// TTL expires would hit disk because cached 0 !== any real mtime.
|
|
361
|
-
// NOTE: tasks is reused from the pre-invalidate cache snapshot above; if no
|
|
362
|
-
// cache existed, tasks is [] (matching pre-FIX behavior). Callers that need
|
|
363
|
-
// fresh tasks should call saveRunTasks or loadRunTasks separately.
|
|
364
|
-
const manifestStat = fs.statSync(manifestPath);
|
|
365
|
-
setManifestCache(manifest.stateRoot, {
|
|
366
|
-
manifest,
|
|
367
|
-
tasks: cachedTasks,
|
|
368
|
-
manifestMtimeMs: manifestStat.mtimeMs,
|
|
369
|
-
manifestSize: manifestStat.size,
|
|
370
|
-
tasksMtimeMs: cachedTasksMtimeMs,
|
|
371
|
-
tasksSize: cachedTasksSize,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<void> {
|
|
376
|
-
// FIX: Capture cached tasks array + mtime/size BEFORE invalidating, same
|
|
377
|
-
// rationale as the sync saveRunManifest above. The async path previously
|
|
378
|
-
// always set tasks: [] with mtime/size 0, so any cache hit was guaranteed
|
|
379
|
-
// to look stale to loadRunManifestById until something else wrote tasks.json.
|
|
380
|
-
const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
|
|
381
|
-
const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
|
|
382
|
-
const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
|
|
383
|
-
const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
|
|
384
|
-
// FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving
|
|
385
|
-
// after a crash. See saveRunManifest for full explanation.
|
|
386
|
-
invalidateRunCache(manifest.stateRoot);
|
|
387
|
-
const manifestPath = path.join(manifest.stateRoot, "manifest.json");
|
|
388
|
-
await atomicWriteJsonAsync(manifestPath, manifest);
|
|
389
|
-
// FIX: Re-populate cache with actual mtime/size. See saveRunManifest.
|
|
390
|
-
// RACE GUARD: another concurrent async save (OPT-02) may unlink+rewrite
|
|
391
|
-
// manifest.json between our atomicWriteJsonAsync and this stat. If stat
|
|
392
|
-
// hits ENOENT, use fallback values — the cache will look stale on the
|
|
393
|
-
// next load and re-read from disk, which is correct.
|
|
394
|
-
let manifestStat: { mtimeMs: number; size: number };
|
|
395
|
-
try {
|
|
396
|
-
manifestStat = await fs.promises.stat(manifestPath);
|
|
397
|
-
} catch (statError) {
|
|
398
|
-
const code = String((statError as NodeJS.ErrnoException).code ?? "");
|
|
399
|
-
if (code !== "ENOENT") throw statError;
|
|
400
|
-
manifestStat = { mtimeMs: 0, size: 0 };
|
|
401
|
-
}
|
|
402
|
-
setManifestCache(manifest.stateRoot, {
|
|
403
|
-
manifest,
|
|
404
|
-
tasks: cachedTasks,
|
|
405
|
-
manifestMtimeMs: manifestStat.mtimeMs,
|
|
406
|
-
manifestSize: manifestStat.size,
|
|
407
|
-
tasksMtimeMs: cachedTasksMtimeMs,
|
|
408
|
-
tasksSize: cachedTasksSize,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): void {
|
|
413
|
-
// FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
|
|
414
|
-
invalidateRunCache(manifest.stateRoot);
|
|
415
|
-
|
|
416
|
-
// Guard: if the run state directory has been removed (prune/forget/cleanup),
|
|
417
|
-
// silently return — there is nothing to persist and the run is gone.
|
|
418
|
-
try {
|
|
419
|
-
fs.statSync(manifest.stateRoot);
|
|
420
|
-
} catch {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
atomicWriteJson(manifest.tasksPath, tasks);
|
|
425
|
-
// FIX: Re-populate cache with actual mtime/size for manifest and tasks.
|
|
426
|
-
// Note: We re-read manifest from disk to get its current mtime/size
|
|
427
|
-
// since we only wrote tasks here.
|
|
428
|
-
const manifestPath = path.join(manifest.stateRoot, "manifest.json");
|
|
429
|
-
let manifestStat: fs.Stats;
|
|
430
|
-
let tasksStat: fs.Stats;
|
|
431
|
-
try {
|
|
432
|
-
manifestStat = fs.statSync(manifestPath);
|
|
433
|
-
tasksStat = fs.statSync(manifest.tasksPath);
|
|
434
|
-
} catch {
|
|
435
|
-
// Run state disappeared between the guard above and now — give up gracefully.
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
// FIX: If cache was evicted, re-read manifest from disk rather than using
|
|
439
|
-
// a minimal fallback. A stale minimal manifest with only runId populated
|
|
440
|
-
// would cause manifest.status to be undefined, breaking status checks.
|
|
441
|
-
// If the manifest cannot be read, throw an error — callers using withRunLock
|
|
442
|
-
// should never hit this case, and the error indicates a serious problem.
|
|
443
|
-
// FIX: Also check that the re-read manifest has a status field. If not,
|
|
444
|
-
// fall back to the manifest parameter's status rather than serving a
|
|
445
|
-
// degraded manifest that would break status transition checks.
|
|
446
|
-
const cached = manifestCache.get(manifest.stateRoot);
|
|
447
|
-
const manifestEntry = cached?.manifest ?? readJsonFile<TeamRunManifest>(manifestPath);
|
|
448
|
-
if (!manifestEntry) {
|
|
449
|
-
return; // Run deleted between guard and read
|
|
450
|
-
}
|
|
451
|
-
// Preserve current status from the manifest parameter if the on-disk
|
|
452
|
-
// manifest is missing it (could be a partial write).
|
|
453
|
-
if (!manifestEntry.status) {
|
|
454
|
-
manifestEntry.status = manifest.status;
|
|
455
|
-
}
|
|
456
|
-
setManifestCache(manifest.stateRoot, {
|
|
457
|
-
manifest: manifestEntry,
|
|
458
|
-
tasks,
|
|
459
|
-
manifestMtimeMs: manifestStat.mtimeMs,
|
|
460
|
-
manifestSize: manifestStat.size,
|
|
461
|
-
tasksMtimeMs: tasksStat.mtimeMs,
|
|
462
|
-
tasksSize: tasksStat.size,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* 2.1 caller-migration helper: coalesced variant. Use only when the
|
|
468
|
-
* caller does NOT immediately read tasks.json afterwards (the read would
|
|
469
|
-
* see the previous on-disk content while the write is still buffered).
|
|
470
|
-
* Bulk update paths that fan out into multiple writer call sites are the
|
|
471
|
-
* intended use case. Single-update + read-update loops (e.g.
|
|
472
|
-
* persistSingleTaskUpdate) should keep using saveRunTasks — OR call
|
|
473
|
-
* `flushPendingAtomicWrites()` immediately before their read to force
|
|
474
|
-
* any pending coalesced writes to land on disk first.
|
|
475
|
-
*
|
|
476
|
-
* (perf review 2026-07 F4) — now used by persistSingleTaskUpdate's
|
|
477
|
-
* non-terminal checkpoint path; that caller calls flushPendingAtomicWrites()
|
|
478
|
-
* before its read-modify-write load to defeat the stale-read window.
|
|
479
|
-
*
|
|
480
|
-
* ST-7: pass `skipCoalesce: true` for terminal task status transitions
|
|
481
|
-
* (completed/failed/cancelled/needs_attention/skipped) to bypass the
|
|
482
|
-
* 50ms coalesce window — a SIGKILL in that window would otherwise leave
|
|
483
|
-
* tasks.json stale (showing "running") while events.jsonl already shows
|
|
484
|
-
* the terminal event, causing false zombie detection / double-execution
|
|
485
|
-
* on crash recovery.
|
|
486
|
-
*/
|
|
487
|
-
/** @internal */
|
|
488
|
-
export function saveRunTasksCoalesced(manifest: TeamRunManifest, tasks: TeamTaskState[], skipCoalesce: boolean = false): void {
|
|
489
|
-
// FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
|
|
490
|
-
invalidateRunCache(manifest.stateRoot);
|
|
491
|
-
try {
|
|
492
|
-
fs.statSync(manifest.stateRoot);
|
|
493
|
-
} catch {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
atomicWriteJsonCoalesced(manifest.tasksPath, tasks, undefined, undefined, skipCoalesce);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
|
|
500
|
-
// FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
|
|
501
|
-
invalidateRunCache(manifest.stateRoot);
|
|
502
|
-
try {
|
|
503
|
-
await fsp.access(manifest.stateRoot);
|
|
504
|
-
} catch {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
await atomicWriteJsonAsync(manifest.tasksPath, tasks);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Save manifest and tasks files with individual atomic writes.
|
|
512
|
-
* FIX: Changed from Promise.all (parallel, non-jointly-atomic) to sequential
|
|
513
|
-
* writes to ensure manifest is written before tasks. A crash between writes
|
|
514
|
-
* leaves them in a known state (manifest is older, tasks is newer) which
|
|
515
|
-
* loadRunManifestById's retry loop can detect via mtime comparison.
|
|
516
|
-
* NOTE: There is no stale-reconciler component — the retry loop provides
|
|
517
|
-
* best-effort detection of mid-write crashes by re-reading until mtime/size
|
|
518
|
-
* are stable. For strict atomicity, callers should use withRunLock().
|
|
519
|
-
* FIX: Returns a result object so callers know which write step failed.
|
|
520
|
-
* If manifest write succeeds but tasks write fails, the caller can recover.
|
|
521
|
-
*/
|
|
522
|
-
/** @internal */
|
|
523
|
-
interface SaveManifestAndTasksResult {
|
|
524
|
-
manifestWritten: boolean;
|
|
525
|
-
tasksWritten: boolean;
|
|
526
|
-
error?: string;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async function saveManifestAndTasksAtomic(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<SaveManifestAndTasksResult> {
|
|
530
|
-
let manifestWritten = false;
|
|
531
|
-
let tasksWritten = false;
|
|
532
|
-
try {
|
|
533
|
-
await withRunLock(manifest, async () => {
|
|
534
|
-
// FIX: Invalidate cache BEFORE writes to prevent stale cache serving.
|
|
535
|
-
// Sequential writes instead of Promise.all to ensure manifest is
|
|
536
|
-
// written before tasks. If a crash occurs between writes, manifest is
|
|
537
|
-
// the older timestamp which stale-reconciler uses to detect inconsistency.
|
|
538
|
-
invalidateRunCache(manifest.stateRoot);
|
|
539
|
-
await atomicWriteJsonAsync(path.join(manifest.stateRoot, "manifest.json"), manifest);
|
|
540
|
-
manifestWritten = true;
|
|
541
|
-
await atomicWriteJsonAsync(manifest.tasksPath, tasks);
|
|
542
|
-
tasksWritten = true;
|
|
543
|
-
});
|
|
544
|
-
} catch (err) {
|
|
545
|
-
return {
|
|
546
|
-
manifestWritten,
|
|
547
|
-
tasksWritten,
|
|
548
|
-
// FIX: Use String(err) to safely convert any thrown value (Error,
|
|
549
|
-
// string, number, null, undefined) to a string. err.message would be
|
|
550
|
-
// undefined for non-Error throwables, losing useful context.
|
|
551
|
-
error: String(err),
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
return { manifestWritten: true, tasksWritten: true };
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** @internal */
|
|
558
|
-
function saveManifestAndTasksAtomicSync(manifest: TeamRunManifest, tasks: TeamTaskState[]): SaveManifestAndTasksResult {
|
|
559
|
-
let manifestWritten = false;
|
|
560
|
-
let tasksWritten = false;
|
|
561
|
-
try {
|
|
562
|
-
withRunLockSync(manifest, () => {
|
|
563
|
-
// FIX: Invalidate cache BEFORE writes to prevent stale cache serving.
|
|
564
|
-
invalidateRunCache(manifest.stateRoot);
|
|
565
|
-
atomicWriteJson(path.join(manifest.stateRoot, "manifest.json"), manifest);
|
|
566
|
-
manifestWritten = true;
|
|
567
|
-
atomicWriteJson(manifest.tasksPath, tasks);
|
|
568
|
-
tasksWritten = true;
|
|
569
|
-
});
|
|
570
|
-
} catch (err) {
|
|
571
|
-
return {
|
|
572
|
-
manifestWritten,
|
|
573
|
-
tasksWritten,
|
|
574
|
-
// FIX: Use String(err) to safely convert any thrown value (Error,
|
|
575
|
-
// string, number, null, undefined) to a string. err.message would be
|
|
576
|
-
// undefined for non-Error throwables, losing useful context.
|
|
577
|
-
error: String(err),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
return { manifestWritten: true, tasksWritten: true };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
export interface UpdateRunStatusOptions {
|
|
584
|
-
data?: Record<string, unknown>;
|
|
585
|
-
metadata?: Parameters<typeof appendEvent>[1]["metadata"];
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
export function updateRunStatus(
|
|
589
|
-
manifest: TeamRunManifest,
|
|
590
|
-
status: TeamRunManifest["status"],
|
|
591
|
-
summary?: string,
|
|
592
|
-
options: UpdateRunStatusOptions = {},
|
|
593
|
-
): TeamRunManifest {
|
|
594
|
-
if (!canTransitionRunStatus(manifest.status, status)) {
|
|
595
|
-
throw errors.invalidStatusTransition(manifest.status, status);
|
|
596
|
-
}
|
|
597
|
-
const updated: TeamRunManifest = {
|
|
598
|
-
...manifest,
|
|
599
|
-
status,
|
|
600
|
-
updatedAt: new Date().toISOString(),
|
|
601
|
-
summary: summary ?? manifest.summary,
|
|
602
|
-
};
|
|
603
|
-
saveRunManifest(updated);
|
|
604
|
-
// Unregister from active-run-index when run reaches a terminal status.
|
|
605
|
-
// Without this, stale entries accumulate (e.g. integration tests in /tmp) and
|
|
606
|
-
// Pi UI shows ghost "queued" runs that are actually completed/failed/cancelled.
|
|
607
|
-
// Note: "blocked" is excluded because blocked runs can be unblocked later.
|
|
608
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
609
|
-
try {
|
|
610
|
-
unregisterActiveRun(updated.runId);
|
|
611
|
-
} catch {
|
|
612
|
-
/* non-critical */
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
appendEvent(updated.eventsPath, {
|
|
616
|
-
type: `run.${status}`,
|
|
617
|
-
runId: updated.runId,
|
|
618
|
-
message: summary,
|
|
619
|
-
...(options.data ? { data: options.data } : {}),
|
|
620
|
-
metadata: {
|
|
621
|
-
provenance: "team_runner",
|
|
622
|
-
sessionIdentity: {
|
|
623
|
-
title: updated.team,
|
|
624
|
-
workspace: updated.cwd,
|
|
625
|
-
purpose: updated.goal,
|
|
626
|
-
},
|
|
627
|
-
ownership: {
|
|
628
|
-
owner: updated.team,
|
|
629
|
-
workflowScope: updated.workflow ?? "manual",
|
|
630
|
-
watcherAction: "act",
|
|
631
|
-
},
|
|
632
|
-
confidence: "high",
|
|
633
|
-
...options.metadata,
|
|
634
|
-
},
|
|
635
|
-
});
|
|
636
|
-
return updated;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
export function __test__manifestCacheSize(): number {
|
|
640
|
-
return manifestCache.size;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
export function __test__clearManifestCache(): void {
|
|
644
|
-
manifestCache.clear();
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* OPT-08 additive helper: drop the manifest cache entry for `stateRoot` after
|
|
649
|
-
* flushing any pending coalesced atomic writes (process-wide). Forces the next
|
|
650
|
-
* `loadRunManifestById` / `loadRunManifestByIdAsync` for this run to hit disk
|
|
651
|
-
* instead of serving a possibly-stale cache hit. Idempotent: safe to call
|
|
652
|
-
* multiple times, including for unknown stateRoots.
|
|
653
|
-
*
|
|
654
|
-
* This is an additive helper ONLY — it does NOT replace the load-bearing CAS
|
|
655
|
-
* loop in `persistSingleTaskUpdate`, nor does it bypass `withRunLock` /
|
|
656
|
-
* `fs.statSync`-based invalidation. It is a safe exit hatch for callers that
|
|
657
|
-
* want a guaranteed cache drop (e.g. cleanup paths, end-of-run shutdown).
|
|
658
|
-
*
|
|
659
|
-
* Scope note: `flushPendingAtomicWrites()` flushes ALL pending coalesced
|
|
660
|
-
* writes process-wide, not just those under `stateRoot`. The coalescer has no
|
|
661
|
-
* per-stateRoot filter; flushing the whole queue is the simplest correct
|
|
662
|
-
* behavior and matches the existing `flushPendingAtomicWrites()` contract
|
|
663
|
-
* used by cleanupRuntime / process exit handlers.
|
|
664
|
-
*/
|
|
665
|
-
export async function unloadRun(stateRoot: string): Promise<void> {
|
|
666
|
-
// Flush first so any in-flight buffered write lands on disk before we drop
|
|
667
|
-
// the cache entry. Otherwise a coalesced write could fire AFTER unloadRun
|
|
668
|
-
// returns, re-populate the cache from disk, and re-stale it.
|
|
669
|
-
flushPendingAtomicWrites();
|
|
670
|
-
// invalidateRunCache bumps the per-stateRoot generation counter so even if
|
|
671
|
-
// some in-process reader has a stale reference, the next cache lookup
|
|
672
|
-
// misses (generation mismatch) and re-reads from disk.
|
|
673
|
-
invalidateRunCache(stateRoot);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* OPT-08 additive helper: read-only inspection of the manifest cache.
|
|
678
|
-
* Returns current size, configured limits, and per-stateRoot observability
|
|
679
|
-
* (cache generation + age in ms). Pure read — never mutates cache state.
|
|
680
|
-
* Useful for `tests`, dashboards, and pre-shutdown sanity checks.
|
|
681
|
-
*/
|
|
682
|
-
export interface ManifestCacheStats {
|
|
683
|
-
size: number;
|
|
684
|
-
maxEntries: number;
|
|
685
|
-
ttlMs: number;
|
|
686
|
-
perStateRoot: Record<string, { generation: number; ageMs: number }>;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export function getManifestCacheStats(): ManifestCacheStats {
|
|
690
|
-
const now = Date.now();
|
|
691
|
-
const perStateRoot: Record<string, { generation: number; ageMs: number }> = {};
|
|
692
|
-
for (const [stateRoot, entry] of manifestCache.entries()) {
|
|
693
|
-
perStateRoot[stateRoot] = {
|
|
694
|
-
generation: entry.generation ?? genOf(stateRoot),
|
|
695
|
-
// ageMs is 0 when cachedAt is unset (defensive — setManifestCache
|
|
696
|
-
// always sets it today, but a future regression would otherwise
|
|
697
|
-
// surface as `undefined` in the stats object).
|
|
698
|
-
ageMs: entry.cachedAt ? now - entry.cachedAt : 0,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
size: manifestCache.size,
|
|
703
|
-
maxEntries: DEFAULT_CACHE.manifestMaxEntries,
|
|
704
|
-
ttlMs: MANIFEST_CACHE_TTL_MS,
|
|
705
|
-
perStateRoot,
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
async function readJsonFileAsync<T>(filePath: string): Promise<T | undefined> {
|
|
710
|
-
try {
|
|
711
|
-
return JSON.parse(await fs.promises.readFile(filePath, "utf-8")) as T;
|
|
712
|
-
} catch (err) {
|
|
713
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
714
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
715
|
-
logInternalError("readJsonFileAsync", err, `filePath=${filePath}`);
|
|
716
|
-
}
|
|
717
|
-
return undefined;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Load a run manifest and its tasks by runId.
|
|
723
|
-
* WARNING: This function provides best-effort consistency only. The sentinel-based
|
|
724
|
-
* retry loop does NOT guarantee manifest/tasks consistency under contention —
|
|
725
|
-
* a concurrent writer can complete a full write cycle between the final stat
|
|
726
|
-
* and the read. For strict consistency, callers MUST wrap load+modify+save in
|
|
727
|
-
* withRunLock(). Callers that need guaranteed consistency should use the lock.
|
|
728
|
-
*/
|
|
729
|
-
export function loadRunManifestById(cwd: string, runId: string): { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined {
|
|
730
|
-
const stateRoot = resolveRunStateRoot(cwd, runId);
|
|
731
|
-
if (!stateRoot) return undefined;
|
|
732
|
-
const manifestPath = path.join(stateRoot, "manifest.json");
|
|
733
|
-
const tasksPath = path.join(stateRoot, "tasks.json");
|
|
734
|
-
|
|
735
|
-
let manifestStat: fs.Stats;
|
|
736
|
-
try {
|
|
737
|
-
manifestStat = statManifestWithWindowsRetry(manifestPath);
|
|
738
|
-
} catch {
|
|
739
|
-
return undefined;
|
|
740
|
-
}
|
|
741
|
-
const cached = manifestCache.get(stateRoot);
|
|
742
|
-
// Issue 1 fix: Note that the cache read-check-use sequence below is NOT atomic
|
|
743
|
-
// under concurrent modification. While individual JS Map operations are atomic,
|
|
744
|
-
// the sequence spanning multiple filesystem operations is not atomic. Callers MUST
|
|
745
|
-
// NOT invoke loadRunManifestById concurrently with cache-modifying operations
|
|
746
|
-
// (setManifestCache, invalidateRunCache) for the same stateRoot. The generation
|
|
747
|
-
// counter provides some protection, but does not make the read-check-use atomic.
|
|
748
|
-
let tasksStat: fs.Stats | undefined;
|
|
749
|
-
try {
|
|
750
|
-
tasksStat = fs.statSync(tasksPath);
|
|
751
|
-
} catch {
|
|
752
|
-
tasksStat = undefined;
|
|
753
|
-
}
|
|
754
|
-
const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
|
|
755
|
-
if (
|
|
756
|
-
cached &&
|
|
757
|
-
cached.manifestMtimeMs === manifestStat.mtimeMs &&
|
|
758
|
-
cached.manifestSize === manifestStat.size &&
|
|
759
|
-
cached.tasksMtimeMs === tasksMtimeMs &&
|
|
760
|
-
cached.tasksSize === (tasksStat?.size ?? 0) &&
|
|
761
|
-
cached.generation === genOf(stateRoot)
|
|
762
|
-
) {
|
|
763
|
-
// TTL eviction: expire stale entries even if mtime matches
|
|
764
|
-
// FIX: Also evict entries where cachedAt is undefined — such entries are
|
|
765
|
-
// effectively immortal otherwise (the `cached.cachedAt &&` check would skip
|
|
766
|
-
// them every time). This can happen if a cache entry was created by
|
|
767
|
-
// setManifestCache that didn't set cachedAt (shouldn't happen in current
|
|
768
|
-
// code, but defensive against future regressions).
|
|
769
|
-
if (!cached.cachedAt || Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
770
|
-
manifestCache.delete(stateRoot);
|
|
771
|
-
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
772
|
-
manifestCache.delete(stateRoot);
|
|
773
|
-
return undefined;
|
|
774
|
-
} else if (!fs.existsSync(tasksPath)) {
|
|
775
|
-
// Tasks file was deleted after cache was populated — this is a cache miss,
|
|
776
|
-
// not a manifest inconsistency. The cache check passes because
|
|
777
|
-
// tasksMtimeMs=0 and tasksSize=0 match a cache entry written when tasks.json
|
|
778
|
-
// didn't exist. We fall through to the retry loop which will correctly
|
|
779
|
-
// detect the missing file and return undefined. The alternative (treating
|
|
780
|
-
// this as an inconsistency) would require failing the cache hit path, which
|
|
781
|
-
// is unnecessary since the retry loop handles it correctly anyway.
|
|
782
|
-
manifestCache.delete(stateRoot);
|
|
783
|
-
return undefined;
|
|
784
|
-
} else {
|
|
785
|
-
return { manifest: cached.manifest, tasks: cached.tasks ?? [] };
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// FIX: Sentinel-based retry loop for best-effort consistency. Re-stat and
|
|
790
|
-
// re-read until mtime/size are stable. The retry limit is now configurable
|
|
791
|
-
// via LOAD_MANIFEST_RETRY_LIMIT (was hardcoded 3). High contention can still
|
|
792
|
-
// cause non-convergence — for strict consistency, callers MUST use withRunLock().
|
|
793
|
-
// Issue 3 fix: Made retry limit configurable instead of hardcoded 3.
|
|
794
|
-
let attempts = 0;
|
|
795
|
-
let manifest: TeamRunManifest | undefined;
|
|
796
|
-
let tasks: TeamTaskState[] | undefined;
|
|
797
|
-
while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
|
|
798
|
-
const freshStat = fs.statSync(manifestPath);
|
|
799
|
-
manifest = readJsonFile<TeamRunManifest>(manifestPath);
|
|
800
|
-
const freshTasksStat = fs.existsSync(tasksPath) ? fs.statSync(tasksPath) : undefined;
|
|
801
|
-
tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
|
|
802
|
-
// If size/mtime didn't change between stat and read, we're consistent.
|
|
803
|
-
if (
|
|
804
|
-
freshStat.mtimeMs === manifestStat.mtimeMs &&
|
|
805
|
-
freshStat.size === manifestStat.size &&
|
|
806
|
-
(!freshTasksStat || (freshTasksStat.mtimeMs === tasksStat?.mtimeMs && freshTasksStat.size === tasksStat?.size))
|
|
807
|
-
) {
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
attempts += 1;
|
|
811
|
-
manifestStat = freshStat;
|
|
812
|
-
tasksStat = freshTasksStat;
|
|
813
|
-
}
|
|
814
|
-
// WARNING: Best-effort consistency only — retry loop detected mtime/size
|
|
815
|
-
// instability. A concurrent writer can still complete a full write cycle
|
|
816
|
-
// between the final stat and the read. Callers needing strict consistency
|
|
817
|
-
// MUST use withRunLock() around load+modify+save.
|
|
818
|
-
if (attempts > 0) {
|
|
819
|
-
// Round 19: downgrade to debug — retry-loop instability is expected under
|
|
820
|
-
// concurrent writes (live team runs constantly append to tasks.json).
|
|
821
|
-
// This is best-effort by design; strict consistency requires withRunLock().
|
|
822
|
-
console.debug(
|
|
823
|
-
`[state-store] loadRunManifestById: retry loop detected instability for run ${runId} after ${attempts} attempt(s) — best-effort only, use withRunLock() for strict consistency`,
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
// NOTE: manifest mtime may legitimately be >= tasks mtime because
|
|
827
|
-
// saveManifestAndTasksAtomicSync writes manifest before tasks. However,
|
|
828
|
-
// if a crash occurs AFTER tasks is written but BEFORE manifest is written,
|
|
829
|
-
// tasks mtime would be > manifest mtime (the opposite). The retry loop
|
|
830
|
-
// above detects this crash state by re-reading until mtime/size are stable
|
|
831
|
-
// — the final stable state is what gets used. Because the retry loop
|
|
832
|
-
// handles the crash case, we do NOT fail based on this comparison alone.
|
|
833
|
-
// It does not indicate corruption on its own.
|
|
834
|
-
if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
|
|
835
|
-
setManifestCache(stateRoot, {
|
|
836
|
-
manifest,
|
|
837
|
-
tasks: tasks ?? [],
|
|
838
|
-
manifestMtimeMs: manifestStat.mtimeMs,
|
|
839
|
-
manifestSize: manifestStat.size,
|
|
840
|
-
tasksMtimeMs,
|
|
841
|
-
tasksSize: tasksStat?.size ?? 0,
|
|
842
|
-
});
|
|
843
|
-
return { manifest, tasks: tasks ?? [] };
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
export async function loadRunManifestByIdAsync(
|
|
847
|
-
cwd: string,
|
|
848
|
-
runId: string,
|
|
849
|
-
): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined> {
|
|
850
|
-
const stateRoot = resolveRunStateRoot(cwd, runId);
|
|
851
|
-
if (!stateRoot) return undefined;
|
|
852
|
-
const manifestPath = path.join(stateRoot, "manifest.json");
|
|
853
|
-
const tasksPath = path.join(stateRoot, "tasks.json");
|
|
854
|
-
|
|
855
|
-
let manifestStat: fs.Stats;
|
|
856
|
-
try {
|
|
857
|
-
manifestStat = await fs.promises.stat(manifestPath);
|
|
858
|
-
} catch {
|
|
859
|
-
return undefined;
|
|
860
|
-
}
|
|
861
|
-
const cached = manifestCache.get(stateRoot);
|
|
862
|
-
// Issue 1 fix: Note that the cache read-check-use sequence below is NOT atomic
|
|
863
|
-
// under concurrent modification. Same as sync version — see loadRunManifestById.
|
|
864
|
-
let tasksStat: fs.Stats | undefined;
|
|
865
|
-
try {
|
|
866
|
-
tasksStat = await fs.promises.stat(tasksPath);
|
|
867
|
-
} catch {
|
|
868
|
-
tasksStat = undefined;
|
|
869
|
-
}
|
|
870
|
-
const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
|
|
871
|
-
if (
|
|
872
|
-
cached &&
|
|
873
|
-
cached.manifestMtimeMs === manifestStat.mtimeMs &&
|
|
874
|
-
cached.manifestSize === manifestStat.size &&
|
|
875
|
-
cached.tasksMtimeMs === tasksMtimeMs &&
|
|
876
|
-
cached.tasksSize === (tasksStat?.size ?? 0) &&
|
|
877
|
-
cached.generation === genOf(stateRoot)
|
|
878
|
-
) {
|
|
879
|
-
// TTL eviction: expire stale entries even if mtime matches
|
|
880
|
-
// FIX: Also evict entries where cachedAt is undefined — such entries are
|
|
881
|
-
// effectively immortal otherwise (the `cached.cachedAt &&` check would skip
|
|
882
|
-
// them every time). This can happen if a cache entry was created by
|
|
883
|
-
// setManifestCache that didn't set cachedAt (shouldn't happen in current
|
|
884
|
-
// code, but defensive against future regressions).
|
|
885
|
-
if (!cached.cachedAt || Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
886
|
-
manifestCache.delete(stateRoot);
|
|
887
|
-
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
888
|
-
manifestCache.delete(stateRoot);
|
|
889
|
-
return undefined;
|
|
890
|
-
} else if (
|
|
891
|
-
!(await fsp.access(tasksPath).then(
|
|
892
|
-
() => true,
|
|
893
|
-
() => false,
|
|
894
|
-
))
|
|
895
|
-
) {
|
|
896
|
-
// Tasks file was deleted after cache was populated — do not serve stale cache.
|
|
897
|
-
manifestCache.delete(stateRoot);
|
|
898
|
-
return undefined;
|
|
899
|
-
} else {
|
|
900
|
-
return { manifest: cached.manifest, tasks: cached.tasks ?? [] };
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// FIX: Sentinel-based retry loop to close TOCTOU window between stat and read.
|
|
905
|
-
// Matches the pattern used in the sync loadRunManifestById.
|
|
906
|
-
// Issue 3 fix: Made retry limit configurable instead of hardcoded 3.
|
|
907
|
-
let manifest: TeamRunManifest | undefined;
|
|
908
|
-
let tasks: TeamTaskState[] | undefined;
|
|
909
|
-
let attempts = 0;
|
|
910
|
-
while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
|
|
911
|
-
const freshStat = await fs.promises.stat(manifestPath);
|
|
912
|
-
manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
|
|
913
|
-
const freshTasksStat = await fs.promises.stat(tasksPath).catch(() => undefined);
|
|
914
|
-
tasks = (await readJsonFileAsync<TeamTaskState[]>(tasksPath)) ?? [];
|
|
915
|
-
// If size/mtime didn't change between stat and read, we're consistent.
|
|
916
|
-
if (
|
|
917
|
-
freshStat.mtimeMs === manifestStat.mtimeMs &&
|
|
918
|
-
freshStat.size === manifestStat.size &&
|
|
919
|
-
(!freshTasksStat || (freshTasksStat.mtimeMs === tasksStat?.mtimeMs && freshTasksStat.size === tasksStat?.size))
|
|
920
|
-
) {
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
attempts += 1;
|
|
924
|
-
manifestStat = freshStat;
|
|
925
|
-
tasksStat = freshTasksStat;
|
|
926
|
-
}
|
|
927
|
-
// WARNING: Best-effort consistency only — retry loop detected mtime/size
|
|
928
|
-
// instability. A concurrent writer can still complete a full write cycle
|
|
929
|
-
// between the final stat and the read. Callers needing strict consistency
|
|
930
|
-
// MUST use withRunLock() around load+modify+save.
|
|
931
|
-
if (attempts > 0) {
|
|
932
|
-
// Round 19: downgrade to debug — retry-loop instability is expected under
|
|
933
|
-
// concurrent writes (live team runs constantly append to tasks.json).
|
|
934
|
-
// This is best-effort by design; strict consistency requires withRunLock().
|
|
935
|
-
console.debug(
|
|
936
|
-
`[state-store] loadRunManifestByIdAsync: retry loop detected instability for run ${runId} after ${attempts} attempt(s) — best-effort only, use withRunLock() for strict consistency`,
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
// NOTE: manifest mtime may legitimately be >= tasks mtime because
|
|
940
|
-
// saveManifestAndTasksAtomicSync writes manifest before tasks. However,
|
|
941
|
-
// if a crash occurs AFTER tasks is written but BEFORE manifest is written,
|
|
942
|
-
// tasks mtime would be > manifest mtime (the opposite). The retry loop
|
|
943
|
-
// above detects this crash state by re-reading until mtime/size are stable
|
|
944
|
-
// — the final stable state is what gets used. Because the retry loop
|
|
945
|
-
// handles the crash case, we do NOT fail based on this comparison alone.
|
|
946
|
-
// It does not indicate corruption on its own.
|
|
947
|
-
|
|
948
|
-
if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
|
|
949
|
-
setManifestCache(stateRoot, {
|
|
950
|
-
manifest,
|
|
951
|
-
tasks: tasks ?? [],
|
|
952
|
-
manifestMtimeMs: manifestStat.mtimeMs,
|
|
953
|
-
manifestSize: manifestStat.size,
|
|
954
|
-
tasksMtimeMs,
|
|
955
|
-
tasksSize: tasksStat?.size ?? 0,
|
|
956
|
-
});
|
|
957
|
-
return { manifest, tasks: tasks ?? [] };
|
|
958
|
-
}
|