@zhushanwen/subagent-core 0.2.0 → 0.5.1
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/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-43ONBFZX.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-V3VHZ2VX.js +59 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +62 -9
- package/dist/execution/engine/engines/zcode/constants.d.cts +116 -12
- package/dist/execution/engine/engines/zcode/constants.d.ts +116 -12
- package/dist/execution/engine/engines/zcode/constants.js +39 -7
- package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +18234 -1277
- package/dist/index.d.cts +5646 -673
- package/dist/index.d.ts +5646 -673
- package/dist/index.js +17828 -1277
- package/dist/{types-BxyAidGf.d.cts → types-Dv4QhSJ_.d.cts} +678 -112
- package/dist/{types-BxyAidGf.d.ts → types-Dv4QhSJ_.d.ts} +678 -112
- package/package.json +7 -4
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
- package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
- package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
- package/src/__tests__/m2-append-content-probe.test.ts +0 -1
- package/src/__tests__/manifest-store.test.ts +10 -9
- package/src/__tests__/record-store-cache.test.ts +0 -2
- package/src/__tests__/record-store-index.test.ts +1 -2
- package/src/__tests__/review-fix-loop-script.test.ts +93 -3
- package/src/__tests__/review-fix-loop-utils.test.ts +31 -10
- package/src/__tests__/robustness-low-batch1.test.ts +6 -5
- package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
- package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
- package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
- package/src/__tests__/session-runner.test.ts +1 -1
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/core/__tests__/host-services.test.ts +2 -2
- package/src/core/__tests__/logger.test.ts +1 -1
- package/src/core/error-message.ts +9 -0
- package/src/core/host-services.ts +21 -5
- package/src/core/notify-ports.ts +18 -5
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agent-registry.test.ts +370 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +136 -30
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +5 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +77 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +31 -24
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
- package/src/execution/__tests__/execute-nesting.test.ts +34 -188
- package/src/execution/__tests__/execution-record.test.ts +217 -60
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
- package/src/execution/__tests__/finalize-record.test.ts +278 -16
- package/src/execution/__tests__/gc-timer.test.ts +57 -2
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +14 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +280 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
- package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/model-resolver.test.ts +2 -2
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
- package/src/execution/__tests__/nested-visibility.test.ts +2 -0
- package/src/execution/__tests__/notify-ledger.test.ts +1249 -0
- package/src/execution/__tests__/output-collector.test.ts +117 -6
- package/src/execution/__tests__/pi-invocation.test.ts +1 -1
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +89 -5
- package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
- package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
- package/src/execution/__tests__/rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +19 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +5 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +418 -26
- package/src/execution/__tests__/run-spawn-integration.test.ts +3 -3
- package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +16 -8
- package/src/execution/__tests__/service-kill-escalation.test.ts +91 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
- package/src/execution/__tests__/session-file-gc.test.ts +35 -0
- package/src/execution/__tests__/session-pending.test.ts +471 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +4 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +227 -0
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
- package/src/execution/__tests__/settled-watchdog.test.ts +524 -0
- package/src/execution/__tests__/spawn-args.test.ts +11 -10
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
- package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
- package/src/execution/__tests__/spawned-children.test.ts +155 -11
- package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
- package/src/execution/__tests__/stdin-writer.test.ts +6 -4
- package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
- package/src/execution/__tests__/subagent-actions-core.test.ts +1017 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +99 -35
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +271 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +33 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +341 -0
- package/src/execution/__tests__/subagent-service.test.ts +37 -30
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
- package/src/execution/__tests__/temp-prompt.test.ts +1 -1
- package/src/execution/__tests__/timeout-integration.test.ts +13 -12
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
- package/src/execution/__tests__/turn-limiter.test.ts +1 -1
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +107 -7
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +167 -2
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-manager.test.ts +14 -5
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +298 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/cold-resurrect.ts +199 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
- package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +56 -99
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +88 -15
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +33 -7
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +75 -54
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +128 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
- package/src/execution/engine/__tests__/registry.test.ts +94 -3
- package/src/execution/engine/__tests__/routing.test.ts +115 -6
- package/src/execution/engine/common/capability-gate.ts +92 -0
- package/src/execution/engine/common/journal-wiring.ts +97 -0
- package/src/execution/engine/common/kill-chain.ts +42 -10
- package/src/execution/engine/common/nesting-guard.ts +66 -0
- package/src/execution/engine/common/persona-router.ts +18 -8
- package/src/execution/engine/common/pool-manager.ts +332 -78
- package/src/execution/engine/common/session-view-service.ts +506 -0
- package/src/execution/engine/common/session-view-types.ts +125 -0
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
- package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
- package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +86 -11
- package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
- package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
- package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
- package/src/execution/engine/engines/pi/reader.ts +11 -0
- package/src/execution/engine/engines/pi/registration.ts +4 -2
- package/src/execution/engine/engines/pi/session-runner.ts +2840 -0
- package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
- package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
- package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
- package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +301 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +473 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +792 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +782 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +168 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +65 -47
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +598 -0
- package/src/execution/engine/engines/zcode/constants.ts +147 -14
- package/src/execution/engine/engines/zcode/golden-sample.ts +42 -34
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +22 -150
- package/src/execution/engine/engines/zcode/session-channel.ts +968 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +1023 -250
- package/src/execution/engine/host-task-spec.ts +32 -36
- package/src/execution/engine/model-validation.ts +177 -0
- package/src/execution/engine/port.ts +65 -7
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/engine/routing.ts +75 -1
- package/src/execution/engine/types.ts +30 -103
- package/src/execution/execution-record.ts +222 -144
- package/src/execution/finalize-record.ts +181 -85
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-host.ts +212 -0
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +148 -88
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +127 -102
- package/src/execution/settled-watchdog.ts +311 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +1106 -907
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/types.ts +9 -0
- package/src/execution/ui-request-handler-factory.ts +126 -75
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +542 -9
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +3 -5
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +384 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +390 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +84 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +356 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/script-lint.test.ts +17 -0
- package/src/orchestration/__tests__/test-mocks.ts +2 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
- package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
- package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +28 -27
- package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
- package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
- package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +8 -8
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +25 -79
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +160 -104
- package/src/orchestration/lifecycle.ts +371 -97
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +15 -15
- package/src/orchestration/models/trace.ts +9 -9
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +28 -28
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +123 -90
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +498 -221
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +261 -217
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +323 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +182 -68
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +97 -204
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/schema-jsonify.ts +1 -1
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +165 -92
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/execution/session-runner.ts +0 -1789
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/execution/__tests__/helpers/subagent-service-mocks.ts
|
|
2
|
+
//
|
|
3
|
+
// SubagentService 编排层测试(execute-and-await-worktree / execute-nesting /
|
|
4
|
+
// nested-visibility-env-propagation / recursive-visibility-baseline 四文件)共享的
|
|
5
|
+
// spawn 链 mock 模块工厂。原四文件各自内联 ~85 行逐字重复的 vi.mock 工厂体(fallow
|
|
6
|
+
// duplication 告警),收敛到本 module 单源——桩形变更改此文件,全量消费方同步生效。
|
|
7
|
+
//
|
|
8
|
+
// 消费方式(与 __tests__/helpers/system-page-mount.ts、spawn-mock.ts 同款先例):
|
|
9
|
+
// vi.mock 调用留在测试文件(mock 注册是文件作用域),import 本 module 的语句写在
|
|
10
|
+
// vi.mock 调用之前(工厂执行发生在被测模块链请求被 mock 模块时,届时本 module 已加载)。
|
|
11
|
+
//
|
|
12
|
+
// node:fs / ../alive-store.ts 两个工厂需要真实模块做基底(...actual 展开):真实实现
|
|
13
|
+
// 经工厂的 importOriginal 参数由测试文件传入——本 module 不能顶层静态 import 它们
|
|
14
|
+
// (测试文件的 vi.mock 注册先于一切 import,本 module 属其依赖图,静态 import 会拿到
|
|
15
|
+
// mock 版自身 → 展开循环)。
|
|
16
|
+
//
|
|
17
|
+
// FakeChild 复用 ./spawn-mock.ts 的 class(结构超集:多 stdin / exitCode / signalCode
|
|
18
|
+
// 字段,四文件原内联 FakeChild 只访问 stdout/stderr/kill/emit,行为等价)。
|
|
19
|
+
|
|
20
|
+
import { vi } from "vitest";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
import { FakeChild, emitStdoutLine, sessionHeader } from "./spawn-mock.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* node:child_process mock 模块。
|
|
28
|
+
* spawn → FakeChild(测试经 mockSpawn.mock.results 取回控制器);
|
|
29
|
+
* execFile → err-first 兜底(buildEnvBlock 的 git branch 调用失败 → catch → branch="")。
|
|
30
|
+
*/
|
|
31
|
+
export function childProcessModule() {
|
|
32
|
+
return {
|
|
33
|
+
spawn: vi.fn(() => new FakeChild()),
|
|
34
|
+
execFile: vi.fn(
|
|
35
|
+
(
|
|
36
|
+
_cmd: string,
|
|
37
|
+
_args: readonly string[],
|
|
38
|
+
_opts: unknown,
|
|
39
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
40
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* node:fs mock 模块:同步方法 mock(runSpawn 用到的全部),promises 保留真实实现
|
|
47
|
+
* (temp-prompt 已整体 mock,不触发真实 I/O)。default 与顶层具名是两套独立 vi.fn
|
|
48
|
+
* 实例(与收敛前四文件内联形态同构)。
|
|
49
|
+
*/
|
|
50
|
+
// 显式返回类型(typeof import 表达式可命名)——否则推导的返回类型引用 fs 内部
|
|
51
|
+
// 未导出的 ReadStreamOptions/WriteStreamOptions,触发 TS4058 可命名性检查
|
|
52
|
+
export function fsSyncModule(
|
|
53
|
+
actual: typeof import("node:fs"),
|
|
54
|
+
): {
|
|
55
|
+
/** 真实 fs + 同步 mock 的合并视图(vi.mock 工厂消费面宽松) */
|
|
56
|
+
default: unknown;
|
|
57
|
+
promises: typeof import("node:fs")["promises"];
|
|
58
|
+
mkdirSync: ReturnType<typeof vi.fn>;
|
|
59
|
+
existsSync: ReturnType<typeof vi.fn>;
|
|
60
|
+
appendFileSync: ReturnType<typeof vi.fn>;
|
|
61
|
+
writeFileSync: ReturnType<typeof vi.fn>;
|
|
62
|
+
readdirSync: ReturnType<typeof vi.fn>;
|
|
63
|
+
} {
|
|
64
|
+
const syncMocks = () => ({
|
|
65
|
+
mkdirSync: vi.fn(),
|
|
66
|
+
existsSync: vi.fn(() => false),
|
|
67
|
+
appendFileSync: vi.fn(),
|
|
68
|
+
writeFileSync: vi.fn(),
|
|
69
|
+
readdirSync: vi.fn(() => []),
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
default: { ...actual, ...syncMocks() },
|
|
73
|
+
...syncMocks(),
|
|
74
|
+
promises: actual.promises,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ../alive-store.ts mock 模块:writeAliveMarker(runSpawn 写 .alive sidecar)+
|
|
80
|
+
* removeAliveMarker(finalizeRecord 收尾删 .alive)mock;其余导出保留真实实现
|
|
81
|
+
* (worktree-manager / record-store 消费,本组用例不涉及但保留以避免间接报错)。
|
|
82
|
+
*/
|
|
83
|
+
export function aliveStoreModule(actual: typeof import("../../alive-store.ts")) {
|
|
84
|
+
return {
|
|
85
|
+
...actual,
|
|
86
|
+
writeAliveMarker: vi.fn(),
|
|
87
|
+
removeAliveMarker: vi.fn(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** ../finalized-marker.ts mock 模块(避免真实 fs 写 sidecar)。 */
|
|
92
|
+
export function finalizedMarkerModule() {
|
|
93
|
+
return {
|
|
94
|
+
writeFinalized: vi.fn(),
|
|
95
|
+
readFinalized: vi.fn(() => false),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ../manifest-store.ts mock 模块。vi.fn 包裹构造器(普通 function,箭头函数不能被 new):
|
|
101
|
+
* 构造参数 recordsDir 可从 mock.calls 断言([MF-3] 目录统一验证用)。
|
|
102
|
+
* no-op 化的根因:writeManifest 真实链路的异步 console 经 worker RPC 回流,与 vitest
|
|
103
|
+
* teardown 形成 race(unhandled rejection / exit 1)——本组用例测编排逻辑不测持久化。
|
|
104
|
+
*/
|
|
105
|
+
export function manifestStoreModule() {
|
|
106
|
+
class FakeManifestStore {
|
|
107
|
+
writeManifest = vi.fn(async () => {});
|
|
108
|
+
readManifest = vi.fn(async () => null);
|
|
109
|
+
listAllSync = vi.fn(() => []);
|
|
110
|
+
recoverTmpFiles = vi.fn(async () => []);
|
|
111
|
+
}
|
|
112
|
+
return { ManifestStore: vi.fn(function (_recordsDir: string) { return new FakeManifestStore(); }) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** ../engine/engines/pi/temp-prompt.ts mock 模块(固定路径,消除 fake-timers 下 flaky 竞态)。 */
|
|
116
|
+
export function tempPromptModule() {
|
|
117
|
+
return {
|
|
118
|
+
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
119
|
+
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
120
|
+
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
121
|
+
}),
|
|
122
|
+
cleanupTempPrompt: vi.fn(async () => {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 驱动 FakeChild 完成 session:写 header + 可选事件 + close(0)。
|
|
128
|
+
* runSpawn 在 close 后判定 success(exitCode=0),并跑 identity 补写 + finalizeRecord
|
|
129
|
+
* ——「让 runSpawn 自然 resolve」的标准收尾路径。
|
|
130
|
+
*/
|
|
131
|
+
export async function driveChildToCompletion(child: FakeChild, events: Record<string, unknown>[] = []): Promise<void> {
|
|
132
|
+
emitStdoutLine(child, sessionHeader());
|
|
133
|
+
for (const e of events) emitStdoutLine(child, e);
|
|
134
|
+
child.stdout.end();
|
|
135
|
+
child.stderr.end();
|
|
136
|
+
child.emit("close", 0);
|
|
137
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/execution/__tests__/keep-alive-no-progress.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [T2-① / P-T2 降级路径 B] keep-alive 裸缺省无进展检测上界。
|
|
4
|
+
//
|
|
5
|
+
// 设计:docs/design/subagent-core-unbounded-wait-audit.md §7.2 T2-① + impl-plan §5
|
|
6
|
+
// P-T2 裁决(probe/p-t2-report.md):历史 89 样本 96.6% keep-alive 窗口 >30min、
|
|
7
|
+
// 长尾 95.5h 合法(parent-shutdown 收敛)——固定 30min 上限被数据否定,按设计降级
|
|
8
|
+
// 路径 B 落地为无进展检测语义:
|
|
9
|
+
// - 裸缺省(无 maxTurns 无 env):keep-alive 期间任何子进程 stdout 活动刷新计时,
|
|
10
|
+
// 连续静默达 KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS(30min)且 fire 时惰性复核无存活
|
|
11
|
+
// 后代(A1-2)才处置;
|
|
12
|
+
// - 显式 maxTurns / env:行为不变(既有固定时长等待,不挂无进展 timer——opt-out
|
|
13
|
+
// 语义保留,A1-1:显式 maxTurns<=0 与 fail-fast 降级也不挂)。
|
|
14
|
+
//
|
|
15
|
+
// mock 布局与 run-spawn-edges.test.ts 一致(FakeChild + mock session-pending)。
|
|
16
|
+
// mock 工厂与 loggerMock 单源收敛 helpers/spawn-mock.ts;vi.mocked 取回与
|
|
17
|
+
// spawn/keep-alive 前奏收敛 helpers/session-runner-mocks.ts(分工见后者头注释)。
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it, vi } from "vitest";
|
|
20
|
+
|
|
21
|
+
// [A1-2③] Mock 共享 logger:复核失败的 warn 留痕可被断言(对齐 run-spawn-edges 模式)。
|
|
22
|
+
vi.mock("../../core/logger.ts", async () =>
|
|
23
|
+
(await import("./helpers/spawn-mock.ts")).coreLoggerModule());
|
|
24
|
+
|
|
25
|
+
vi.mock("node:child_process", async () =>
|
|
26
|
+
(await import("./helpers/spawn-mock.ts")).childProcessModule());
|
|
27
|
+
vi.mock("node:fs", async () => (await import("./helpers/spawn-mock.ts")).fsModule());
|
|
28
|
+
vi.mock("../alive-store.ts", async () =>
|
|
29
|
+
(await import("./helpers/spawn-mock.ts")).aliveStoreModule());
|
|
30
|
+
|
|
31
|
+
// keep-alive 判定:本文件统一 count>0(有活跃后代 → keep-alive 分支)。
|
|
32
|
+
vi.mock("../session-pending.ts", async () =>
|
|
33
|
+
(await import("./helpers/spawn-mock.ts")).sessionPendingModule());
|
|
34
|
+
|
|
35
|
+
vi.mock("../engine/engines/pi/temp-prompt.ts", async () =>
|
|
36
|
+
(await import("./helpers/spawn-mock.ts")).tempPromptModule());
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS,
|
|
40
|
+
maxTurnsToWatchdogMs,
|
|
41
|
+
SPAWN_WATCHDOG_ENV,
|
|
42
|
+
} from "../engine/engines/pi/session-runner.ts";
|
|
43
|
+
import {
|
|
44
|
+
emitStdoutLine,
|
|
45
|
+
type FakeChild,
|
|
46
|
+
lastSpawnedChild as lastSpawnedChildOf,
|
|
47
|
+
loggerMock,
|
|
48
|
+
makeOpts,
|
|
49
|
+
waitForSpawn as waitForSpawnOf,
|
|
50
|
+
} from "./helpers/spawn-mock.ts";
|
|
51
|
+
import { keepAliveTestHooks, spawnAndReachKeepAlive, takeSessionRunnerMocks } from "./helpers/session-runner-mocks.ts";
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
mockSpawn,
|
|
55
|
+
mockPending,
|
|
56
|
+
mockListPending,
|
|
57
|
+
mockReadAliveMarker,
|
|
58
|
+
mockWriteAliveMarker,
|
|
59
|
+
mockIsProcessAlive,
|
|
60
|
+
mockReaddirSync,
|
|
61
|
+
} = takeSessionRunnerMocks();
|
|
62
|
+
|
|
63
|
+
/** A1-2 复核用例的后代 pid(存/死两形态,对齐 descendant-sweep.test.ts 数值风格)。 */
|
|
64
|
+
const LIVE_DESCENDANT_PID = 424_242;
|
|
65
|
+
const DEAD_DESCENDANT_PID = 999_999;
|
|
66
|
+
|
|
67
|
+
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
68
|
+
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
69
|
+
|
|
70
|
+
/** A1-2 复核用例的差集后代装配:pending 项 + session 文件名 + marker + pid 存活形态。 */
|
|
71
|
+
function mockDescendant(sessionId: string, pid: number, alive: boolean): void {
|
|
72
|
+
mockListPending.mockReturnValue({
|
|
73
|
+
items: [{ id: "bg-1", sessionId, type: "session" }],
|
|
74
|
+
});
|
|
75
|
+
mockReaddirSync.mockReturnValue([`20260901T000000-000_${sessionId}.jsonl`]);
|
|
76
|
+
mockReadAliveMarker.mockImplementation((f: unknown) =>
|
|
77
|
+
String(f).endsWith(`_${sessionId}.jsonl`)
|
|
78
|
+
? { pid, id: sessionId, startedAt: 0 }
|
|
79
|
+
: undefined,
|
|
80
|
+
);
|
|
81
|
+
mockIsProcessAlive.mockImplementation((p: number) => (p === pid ? alive : false));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("[T2-①] keep-alive 裸缺省无进展检测上界(P-T2 降级路径 B)", () => {
|
|
85
|
+
keepAliveTestHooks(mockPending);
|
|
86
|
+
|
|
87
|
+
it("裸缺省:agent_end keep-alive 挂无进展 timer,连续静默 30min 触发 SIGTERM 处置", async () => {
|
|
88
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
89
|
+
|
|
90
|
+
// keep-alive 落位:进程保活
|
|
91
|
+
expect(child.killed).toBe(false);
|
|
92
|
+
|
|
93
|
+
// 静默 30min-1ms:未达阈值,不处置
|
|
94
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS - 1);
|
|
95
|
+
expect(child.killed).toBe(false);
|
|
96
|
+
|
|
97
|
+
// 连续静默达 30min:处置(SIGTERM,升级由 killChildWithEscalation 承接)
|
|
98
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
99
|
+
expect(child.killed).toBe(true);
|
|
100
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
101
|
+
|
|
102
|
+
const result = await finish();
|
|
103
|
+
expect(result.success).toBe(true); // 信号终止视为正常完成(既有语义,T2-③ 形态除外)
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("裸缺省:keep-alive 期间 stdout 活动刷新计时——持续输出的层主不被时长上限误杀", async () => {
|
|
107
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
108
|
+
expect(child.killed).toBe(false);
|
|
109
|
+
|
|
110
|
+
// 静默 29min 后子进程产生一次输出(任意行):静默计时被刷新
|
|
111
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS - 60_000);
|
|
112
|
+
emitStdoutLine(child, { type: "turn_end" });
|
|
113
|
+
await new Promise((r) => setImmediate(r));
|
|
114
|
+
expect(child.killed).toBe(false);
|
|
115
|
+
|
|
116
|
+
// 再静默 29min(相对上次活动 <30min):若无刷新语义此刻已累计 58min 会被误杀
|
|
117
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS - 60_000);
|
|
118
|
+
expect(child.killed).toBe(false);
|
|
119
|
+
|
|
120
|
+
// 刷新后再静默满 30min:连续静默达阈值,处置
|
|
121
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
122
|
+
expect(child.killed).toBe(true);
|
|
123
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
124
|
+
|
|
125
|
+
await finish();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("显式 maxTurns:行为不变——无进展 timer 不挂,30min 静默点不处置,动态超时到期才 kill", async () => {
|
|
129
|
+
const maxTurns = 20; // maxTurnsToWatchdogMs(20) = 100min
|
|
130
|
+
const { child, finish } = await spawnAndReachKeepAlive(makeOpts({ maxTurns }));
|
|
131
|
+
|
|
132
|
+
// 关键反证断言:裸缺省阈值(30min)处连续静默也不 kill——无进展 timer 未挂载
|
|
133
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
134
|
+
expect(child.killed).toBe(false);
|
|
135
|
+
|
|
136
|
+
// 既有 MF-4 动态超时行为不变:100min 到期才 kill
|
|
137
|
+
await vi.advanceTimersByTimeAsync(maxTurnsToWatchdogMs(maxTurns) - KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS - 1);
|
|
138
|
+
expect(child.killed).toBe(false);
|
|
139
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
140
|
+
expect(child.killed).toBe(true);
|
|
141
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
142
|
+
|
|
143
|
+
await finish();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("显式 env 兑底:行为不变——按 env 绝对时限固定等待,不挂无进展 timer", async () => {
|
|
147
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "600000"); // 10min(< 30min 静默阈值,反证无进展 timer 未挂)
|
|
148
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
149
|
+
|
|
150
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
151
|
+
expect(child.killed).toBe(true);
|
|
152
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
153
|
+
|
|
154
|
+
await finish();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("keep-alive 结束(后代全部完成 → final kill)即撤无进展 timer,无残留误杀", async () => {
|
|
158
|
+
// 先走 keep-alive(count>0)挂无进展 timer,再被唤醒重评估为 count=0 → final kill
|
|
159
|
+
mockPending
|
|
160
|
+
.mockReturnValueOnce({ count: 1, recentUnregister: false })
|
|
161
|
+
.mockReturnValueOnce({ count: 0, recentUnregister: false });
|
|
162
|
+
const { child, promise } = await spawnAndReachKeepAlive(undefined, "Task: keep-then-final");
|
|
163
|
+
expect(child.killed).toBe(false);
|
|
164
|
+
|
|
165
|
+
// 第二次 agent_end:后代已完成 → final kill(keep-alive 结束,无进展 timer 同撤)
|
|
166
|
+
emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
|
|
167
|
+
await new Promise((r) => setImmediate(r));
|
|
168
|
+
expect(child.killed).toBe(true);
|
|
169
|
+
|
|
170
|
+
// 反证 timer 已撤:advance 30min 不再有任何残留 kill 副作用(进程已 close 收尾)
|
|
171
|
+
// 真实 ChildProcess 语义:被 SIGTERM 杀死 → 先 exit(null,'SIGTERM') 再 close。只发
|
|
172
|
+
// close 不发 exit 时 killChain 升级窗口不结算(waitForExit 挂 exit 事件),下方
|
|
173
|
+
// advance 30min+ 会越过 30s grace 误触发 SIGKILL 升级——补 exit 忠实模拟进程已死。
|
|
174
|
+
child.signalCode = "SIGTERM";
|
|
175
|
+
child.emit("exit", null, "SIGTERM");
|
|
176
|
+
child.stdout.end();
|
|
177
|
+
child.stderr.end();
|
|
178
|
+
child.emit("close", 143);
|
|
179
|
+
const result = await promise;
|
|
180
|
+
expect(result.success).toBe(true);
|
|
181
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS + 60_000);
|
|
182
|
+
expect(child.killSignal).toBe("SIGTERM"); // 仅 final kill 一次,无二次信号
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── [A1-1] 挂载面收窄:只有裸缺省(maxTurns 未传且 env 未设)挂无进展 timer ──
|
|
186
|
+
|
|
187
|
+
it("A1-1: 显式 maxTurns:0(显式不限时)→ 不挂无进展 timer,静默超阈值不处置", async () => {
|
|
188
|
+
const { child, finish } = await spawnAndReachKeepAlive(makeOpts({ maxTurns: 0 }));
|
|
189
|
+
expect(child.killed).toBe(false);
|
|
190
|
+
|
|
191
|
+
// 无进展阈值(30min)处静默不 kill:timer 未挂(旧实现 keepAliveMs === undefined
|
|
192
|
+
// 即挂 timer,30min 处会误 kill——本断言守护挂载面收窄)
|
|
193
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
194
|
+
expect(child.killed).toBe(false);
|
|
195
|
+
|
|
196
|
+
// 显式 opt-out 旧语义 = 等待后代不限时:远超阈值仍无任何 timer 处置
|
|
197
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS * 3);
|
|
198
|
+
expect(child.killed).toBe(false);
|
|
199
|
+
|
|
200
|
+
await finish();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("A1-1: env 已设 + maxTurns:0(参数显式压过 env,U5)→ 同样不挂无进展 timer", async () => {
|
|
204
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "600000"); // 10min
|
|
205
|
+
const { child, finish } = await spawnAndReachKeepAlive(makeOpts({ maxTurns: 0 }));
|
|
206
|
+
|
|
207
|
+
// 同时越过 env 时限(10min)与无进展阈值(30min)仍不 kill:显式 maxTurns<=0
|
|
208
|
+
// 压过 env(U5),env 固定 watchdog 与无进展 timer 均未挂
|
|
209
|
+
await vi.advanceTimersByTimeAsync(Math.max(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS, 600_000) + 60_000);
|
|
210
|
+
expect(child.killed).toBe(false);
|
|
211
|
+
|
|
212
|
+
await finish();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── [A1-2] fire 惰性复核:stdout 静默 ≠ 无进展,存活后代是刷新信号源的另一半 ──
|
|
216
|
+
|
|
217
|
+
it("A1-2①: fire 复核有存活活跃后代 → 视为有进展重挂(30min 固定复核节奏),不处置层主", async () => {
|
|
218
|
+
// 后代 pending 差集非空 + pid 存活(与 descendant sweep 同源判据)
|
|
219
|
+
mockDescendant("ka-desc-sess", LIVE_DESCENDANT_PID, true);
|
|
220
|
+
|
|
221
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
222
|
+
expect(child.killed).toBe(false);
|
|
223
|
+
|
|
224
|
+
// [A1-2 补修] 清掉 agent_end 处置写点的心跳基线:只统计 fire 重挂分支随行的心跳
|
|
225
|
+
mockWriteAliveMarker.mockClear();
|
|
226
|
+
|
|
227
|
+
// 第 1 次连续静默 30min fire:复核发现存活后代 → 重挂不处置
|
|
228
|
+
//(「直接后代跑 >30min、层主静默」的合法形态,P-T2 数据 85/89 parent-shutdown)
|
|
229
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
230
|
+
expect(child.killed).toBe(false);
|
|
231
|
+
|
|
232
|
+
// 第 2 个 30min 再 fire(固定复核节奏直到后代死光):后代仍活 → 仍不处置
|
|
233
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
234
|
+
expect(child.killed).toBe(false);
|
|
235
|
+
|
|
236
|
+
// [A1-2 补修] 每次复核重挂随行一次心跳:合法化形态「层主静默 + 后代长跑数小时」
|
|
237
|
+
// 期间层主无 agent_end(原心跳写点不再触达),重挂分支是唯一心跳源——缺失则
|
|
238
|
+
// marker 超 1h 软超时判陈旧 → 透明重生放行双写 / 孤儿恢复误终态活 record
|
|
239
|
+
expect(mockWriteAliveMarker).toHaveBeenCalledTimes(2);
|
|
240
|
+
// 心跳写点 = 层主 sessionFile + 子进程 pid(marker 软超时基准刷新语义)
|
|
241
|
+
expect(mockWriteAliveMarker).toHaveBeenCalledWith(
|
|
242
|
+
expect.any(String),
|
|
243
|
+
expect.objectContaining({ pid: child.pid }),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await finish();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("A1-2②: fire 复核后代差集非空但 pid 全部已死 → 真静默,执行处置", async () => {
|
|
250
|
+
mockDescendant("ka-dead-sess", DEAD_DESCENDANT_PID, false); // 差集非空但 pid 全部已死
|
|
251
|
+
|
|
252
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
253
|
+
|
|
254
|
+
// fire:差集非空但无任何存活 pid → 真静默 → 处置(SIGTERM + sweep 标志承接)
|
|
255
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
256
|
+
expect(child.killed).toBe(true);
|
|
257
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
258
|
+
|
|
259
|
+
await finish();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("A1-2③: fire 复核失败(sessionFile 读不出)→ 按无后代处置 + warn 留痕", async () => {
|
|
263
|
+
mockListPending.mockReturnValue({
|
|
264
|
+
items: [],
|
|
265
|
+
error: "session file unreadable: ENOENT",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const { child, finish } = await spawnAndReachKeepAlive();
|
|
269
|
+
|
|
270
|
+
await vi.advanceTimersByTimeAsync(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS);
|
|
271
|
+
expect(child.killed).toBe(true);
|
|
272
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
273
|
+
// 复核失败 warn 留痕(与既有保守分支一致的可观测性)
|
|
274
|
+
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
275
|
+
expect.stringContaining("re-check failed (treating as no live descendants)"),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
await finish();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/execution/__tests__/kill-all-escalation.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [T2-⑤ / LC-2] killAllSpawnedChildren 的跳过条件收敛:「killed 且已确认死亡才跳过」。
|
|
4
|
+
//
|
|
5
|
+
// 设计:docs/design/subagent-core-unbounded-wait-audit.md §7.2 T2-⑤。旧实现按
|
|
6
|
+
// child.killed 跳过——killed=true 只表示「发过 kill 请求」≠「已死」,SIGTERM 被无视
|
|
7
|
+
// 的进程(卡死在不可中断 native 调用)会脱离最后一次回收窗口泄漏。新规则:
|
|
8
|
+
// - 已确认死亡(exitCode/signalCode 任一非 null)→ 跳过;
|
|
9
|
+
// - killed 但未确认死亡 → 补 escalation 检查:直接 SIGKILL(dispose 是最后兜底,
|
|
10
|
+
// 没有 30s 升级窗口可等);
|
|
11
|
+
// - 未 killed 且未确认死亡 → 发调用方指定 signal(既有行为)。
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
|
|
18
|
+
vi.mock("node:child_process", async () => {
|
|
19
|
+
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
20
|
+
return {
|
|
21
|
+
spawn: vi.fn(() => new FakeChild()),
|
|
22
|
+
execFile: vi.fn(
|
|
23
|
+
(
|
|
24
|
+
_cmd: string,
|
|
25
|
+
_args: readonly string[],
|
|
26
|
+
_opts: unknown,
|
|
27
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
28
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
vi.mock("node:fs", async () => {
|
|
34
|
+
const actual = await import("node:fs");
|
|
35
|
+
return {
|
|
36
|
+
default: {
|
|
37
|
+
...actual,
|
|
38
|
+
mkdirSync: vi.fn(),
|
|
39
|
+
existsSync: vi.fn(() => false),
|
|
40
|
+
appendFileSync: vi.fn(),
|
|
41
|
+
writeFileSync: vi.fn(),
|
|
42
|
+
readdirSync: vi.fn(() => []),
|
|
43
|
+
},
|
|
44
|
+
mkdirSync: vi.fn(),
|
|
45
|
+
existsSync: vi.fn(() => false),
|
|
46
|
+
appendFileSync: vi.fn(),
|
|
47
|
+
writeFileSync: vi.fn(),
|
|
48
|
+
readdirSync: vi.fn(() => []),
|
|
49
|
+
promises: actual.promises,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
|
|
54
|
+
vi.mock("../session-pending.ts", () => ({
|
|
55
|
+
readActivePendingFromSessionFile: vi.fn(() => ({ count: 0, recentUnregister: false })),
|
|
56
|
+
prunePendingCursor: vi.fn(),
|
|
57
|
+
listActivePendingFromSessionFile: vi.fn(() => ({ items: [] })),
|
|
58
|
+
}));
|
|
59
|
+
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
60
|
+
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
61
|
+
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
62
|
+
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
63
|
+
}),
|
|
64
|
+
cleanupTempPrompt: vi.fn(async () => {}),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
import { killAllSpawnedChildren, runSpawn, spawnedChildren } from "../engine/engines/pi/session-runner.ts";
|
|
68
|
+
import {
|
|
69
|
+
emitStdoutLine,
|
|
70
|
+
type FakeChild,
|
|
71
|
+
lastSpawnedChild as lastSpawnedChildOf,
|
|
72
|
+
makeCtx,
|
|
73
|
+
makeOpts,
|
|
74
|
+
makeRecord,
|
|
75
|
+
waitForSpawn as waitForSpawnOf,
|
|
76
|
+
} from "./helpers/spawn-mock.ts";
|
|
77
|
+
|
|
78
|
+
const mockSpawn = vi.mocked(spawn);
|
|
79
|
+
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
80
|
+
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
81
|
+
|
|
82
|
+
describe("[T2-⑤ / LC-2] killAllSpawnedChildren 跳过条件收敛", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
vi.clearAllMocks();
|
|
85
|
+
killAllSpawnedChildren(); // 清可能残留的 Map 条目(前序用例/文件)
|
|
86
|
+
spawnedChildren.clear();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("killed 但未 close(SIGTERM 发过、进程未确认死亡)→ 不跳过,补 SIGKILL 升级", async () => {
|
|
94
|
+
const record = makeRecord("esc-1");
|
|
95
|
+
const promise = runSpawn(record, "Task: esc", makeOpts(), makeCtx());
|
|
96
|
+
await waitForSpawn();
|
|
97
|
+
const child = lastSpawnedChild();
|
|
98
|
+
|
|
99
|
+
// 模拟 SIGTERM 已发但进程仍在(LC-2 缺陷形态:killed=发过请求≠已死)
|
|
100
|
+
child.killed = true;
|
|
101
|
+
child.killSignal = "SIGTERM";
|
|
102
|
+
expect(child.exitCode).toBeNull();
|
|
103
|
+
expect(child.signalCode).toBeNull();
|
|
104
|
+
|
|
105
|
+
const n = killAllSpawnedChildren();
|
|
106
|
+
expect(n).toBe(1);
|
|
107
|
+
// 补 escalation:dispose 是最后兜底,直接 SIGKILL(不再赌 SIGTERM 生效)
|
|
108
|
+
expect(child.killSignal).toBe("SIGKILL");
|
|
109
|
+
|
|
110
|
+
// 收尾:emit close 让 runSpawn resolve
|
|
111
|
+
emitStdoutLine(child, { type: "session", id: "s", timestamp: "t", cwd: "/tmp" });
|
|
112
|
+
child.stdout.end();
|
|
113
|
+
child.emit("close", 137);
|
|
114
|
+
const result = await promise;
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("killed 且已确认死亡(signalCode 非 null)→ 跳过", async () => {
|
|
119
|
+
const record = makeRecord("esc-2");
|
|
120
|
+
const promise = runSpawn(record, "Task: dead-skip", makeOpts(), makeCtx());
|
|
121
|
+
await waitForSpawn();
|
|
122
|
+
const child = lastSpawnedChild();
|
|
123
|
+
|
|
124
|
+
// SIGTERM 已发(经 child.kill 记录信号)且进程已确认死亡(内核回收完成、
|
|
125
|
+
// close 事件尚未回调的窗口)
|
|
126
|
+
child.kill("SIGTERM");
|
|
127
|
+
child.signalCode = "SIGTERM";
|
|
128
|
+
|
|
129
|
+
const n = killAllSpawnedChildren();
|
|
130
|
+
expect(n).toBe(0); // 跳过,不重复发信号
|
|
131
|
+
expect(child.killSignal).toBe("SIGTERM"); // 保持原信号(未被改写为 SIGKILL)
|
|
132
|
+
|
|
133
|
+
child.stdout.end();
|
|
134
|
+
child.emit("close", 143);
|
|
135
|
+
await promise;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("未 killed 且未确认死亡 → 发调用方 signal(既有行为不变)", async () => {
|
|
139
|
+
const record = makeRecord("esc-3");
|
|
140
|
+
const promise = runSpawn(record, "Task: alive", makeOpts(), makeCtx());
|
|
141
|
+
await waitForSpawn();
|
|
142
|
+
const child = lastSpawnedChild();
|
|
143
|
+
expect(child.killed).toBe(false);
|
|
144
|
+
|
|
145
|
+
const n = killAllSpawnedChildren("SIGTERM");
|
|
146
|
+
expect(n).toBe(1);
|
|
147
|
+
expect(child.killed).toBe(true);
|
|
148
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
149
|
+
|
|
150
|
+
child.stdout.end();
|
|
151
|
+
child.emit("close", 143);
|
|
152
|
+
await promise;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("未 killed 但已确认死亡(自然退出、close 未到)→ 跳过不补信号", async () => {
|
|
156
|
+
const record = makeRecord("esc-4");
|
|
157
|
+
const promise = runSpawn(record, "Task: natural", makeOpts(), makeCtx());
|
|
158
|
+
await waitForSpawn();
|
|
159
|
+
const child = lastSpawnedChild();
|
|
160
|
+
child.exitCode = 0; // 自然退出
|
|
161
|
+
|
|
162
|
+
const n = killAllSpawnedChildren();
|
|
163
|
+
expect(n).toBe(0);
|
|
164
|
+
expect(child.killed).toBe(false);
|
|
165
|
+
|
|
166
|
+
child.stdout.end();
|
|
167
|
+
child.emit("close", 0);
|
|
168
|
+
await promise;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("混合形态:各分支独立判定(killed-not-dead 升级 / alive 首杀 / dead 跳过)", async () => {
|
|
172
|
+
const mk = async (id: string): Promise<{ child: FakeChild; promise: Promise<unknown> }> => {
|
|
173
|
+
const promise = runSpawn(makeRecord(id), `Task: ${id}`, makeOpts(), makeCtx());
|
|
174
|
+
await waitForSpawn();
|
|
175
|
+
return { child: lastSpawnedChild(), promise };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const a = await mk("mix-a");
|
|
179
|
+
a.child.killed = true; // killed-not-dead → SIGKILL
|
|
180
|
+
const b = await mk("mix-b"); // alive → SIGTERM
|
|
181
|
+
const c = await mk("mix-c");
|
|
182
|
+
c.child.signalCode = "SIGKILL"; // dead → skip
|
|
183
|
+
|
|
184
|
+
const n = killAllSpawnedChildren();
|
|
185
|
+
expect(n).toBe(2);
|
|
186
|
+
expect(a.child.killSignal).toBe("SIGKILL");
|
|
187
|
+
expect(b.child.killSignal).toBe("SIGTERM");
|
|
188
|
+
expect(c.child.killSignal).toBeUndefined();
|
|
189
|
+
|
|
190
|
+
for (const { child, promise } of [a, b, c]) {
|
|
191
|
+
child.stdout.end();
|
|
192
|
+
child.emit("close", 143);
|
|
193
|
+
await promise;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|