@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
|
@@ -13,25 +13,33 @@ import type { ChildProcess } from "node:child_process";
|
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
14
|
|
|
15
15
|
// mock logger(doFinalizeRecord manifest 写入降级路径用)
|
|
16
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
16
|
+
const { loggerMock, killEscalationSpy } = vi.hoisted(() => ({
|
|
17
17
|
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
18
|
+
killEscalationSpy: vi.fn(),
|
|
18
19
|
}));
|
|
19
20
|
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
20
21
|
|
|
21
22
|
// mock session-runner(import 链需要 runSpawn/killAllSpawnedChildren/getChildByRecord 存在)
|
|
22
|
-
vi.mock("../session-runner.ts", () => ({
|
|
23
|
+
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
23
24
|
runSpawn: vi.fn(),
|
|
24
25
|
killAllSpawnedChildren: vi.fn(),
|
|
25
26
|
getChildByRecord: vi.fn(() => undefined),
|
|
27
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
28
|
+
killRecordChildWithEscalation: killEscalationSpy,
|
|
29
|
+
spawnedChildren: new Map(),
|
|
26
30
|
}));
|
|
27
31
|
|
|
28
32
|
import { createRecord, updateFromEvent } from "../execution-record.ts";
|
|
33
|
+
import { createNotifyHost } from "../notify-host.ts";
|
|
29
34
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
30
35
|
import { RecordStore } from "../record-store.ts";
|
|
31
36
|
import { SubagentService } from "../subagent-service.ts";
|
|
32
37
|
import type { PiLike } from "../subagent-service.ts";
|
|
33
38
|
import type { ExecutionRecord } from "../types.ts";
|
|
34
|
-
import { getChildByRecord } from "../session-runner.ts";
|
|
39
|
+
import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
|
|
40
|
+
|
|
41
|
+
// [T2④] kill 收敛入口 spy(工厂引用,断言服务侧三条路径收敛到它)
|
|
42
|
+
const killRecordChildWithEscalationMock = killEscalationSpy;
|
|
35
43
|
import {
|
|
36
44
|
armIdleTimer,
|
|
37
45
|
disarmIdleTimer,
|
|
@@ -88,7 +96,7 @@ function setup(): {
|
|
|
88
96
|
pi: MockPi;
|
|
89
97
|
} {
|
|
90
98
|
const agentDir = makeTmpAgentDir();
|
|
91
|
-
const modelService = new ModelConfigService({ agentDir });
|
|
99
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
92
100
|
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
93
101
|
// [C2] pi 提为外层引用(对照 real-chain :139-140 形态)——close 现状语义断言需读
|
|
94
102
|
// pi.sendMessage.mock.calls(末条通知 content 指针行),内联构造不保留引用。
|
|
@@ -114,23 +122,23 @@ describe("getRecordForAction 归属守卫(决策 3)", () => {
|
|
|
114
122
|
|
|
115
123
|
afterEach(() => {
|
|
116
124
|
service.dispose();
|
|
117
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
125
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
118
126
|
});
|
|
119
127
|
|
|
120
128
|
it("正常(rootSessionId 匹配)→ 返回可变 record", () => {
|
|
121
129
|
const record = makeRecord({ rootSessionId: sessionRootId });
|
|
122
130
|
store.register(record);
|
|
123
|
-
expect(service.getRecordForAction(record.id)).toBe(record);
|
|
131
|
+
expect(service.chatActions.getRecordForAction(record.id)).toBe(record);
|
|
124
132
|
});
|
|
125
133
|
|
|
126
134
|
it("不存在 → throw not found or not owned", () => {
|
|
127
|
-
expect(() => service.getRecordForAction("sa-nonexistent")).toThrow(/not found or not owned/);
|
|
135
|
+
expect(() => service.chatActions.getRecordForAction("sa-nonexistent")).toThrow(/not found or not owned/);
|
|
128
136
|
});
|
|
129
137
|
|
|
130
138
|
it("rootSessionId 不匹配 → throw not found or not owned(不区分 not found vs not owned,防信息泄露)", () => {
|
|
131
139
|
const record = makeRecord({ id: "sa-other", rootSessionId: "other-session" });
|
|
132
140
|
store.register(record);
|
|
133
|
-
expect(() => service.getRecordForAction("sa-other")).toThrow(/not found or not owned/);
|
|
141
|
+
expect(() => service.chatActions.getRecordForAction("sa-other")).toThrow(/not found or not owned/);
|
|
134
142
|
});
|
|
135
143
|
});
|
|
136
144
|
|
|
@@ -150,7 +158,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
150
158
|
|
|
151
159
|
afterEach(() => {
|
|
152
160
|
service.dispose();
|
|
153
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
161
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
154
162
|
});
|
|
155
163
|
|
|
156
164
|
it("running + force:false + 有活进程 → 置 closeAfterRound=true(不立即终态,等轮完)", async () => {
|
|
@@ -160,7 +168,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
160
168
|
const record = makeRecord({ status: "running" });
|
|
161
169
|
store.register(record);
|
|
162
170
|
|
|
163
|
-
await service.closeSubagent(record, false);
|
|
171
|
+
await service.chatActions.closeSubagent(record, false);
|
|
164
172
|
|
|
165
173
|
expect(record.closeAfterRound).toBe(true);
|
|
166
174
|
expect(record.status).toBe("running"); // 仍 running
|
|
@@ -174,7 +182,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
174
182
|
record.sessionFile = path.join(agentDir, "test.jsonl");
|
|
175
183
|
store.register(record);
|
|
176
184
|
|
|
177
|
-
await service.closeSubagent(record, false);
|
|
185
|
+
await service.chatActions.closeSubagent(record, false);
|
|
178
186
|
|
|
179
187
|
expect(record.status).toBe("closed");
|
|
180
188
|
expect(store.getMutable(record.id)).toBeUndefined(); // archived
|
|
@@ -184,7 +192,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
184
192
|
const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "run.jsonl") });
|
|
185
193
|
store.register(record);
|
|
186
194
|
|
|
187
|
-
await service.closeSubagent(record, true);
|
|
195
|
+
await service.chatActions.closeSubagent(record, true);
|
|
188
196
|
|
|
189
197
|
// v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消语义)
|
|
190
198
|
expect(record.status).toBe("closed");
|
|
@@ -196,7 +204,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
196
204
|
const record = makeRecord({ status: "closed" });
|
|
197
205
|
store.register(record);
|
|
198
206
|
|
|
199
|
-
await service.closeSubagent(record, false);
|
|
207
|
+
await service.chatActions.closeSubagent(record, false);
|
|
200
208
|
|
|
201
209
|
expect(record.status).toBe("closed"); // 不变
|
|
202
210
|
});
|
|
@@ -207,9 +215,8 @@ describe("closeSubagent 行为分流", () => {
|
|
|
207
215
|
// 旧代码 Path A 走 else 置 closeAfterRound,但 chatMode 轮完成恒经 agent_settled →
|
|
208
216
|
// onRoundSettled → runAndFinalize early return(isIdle 恒 true),标志无人消费,
|
|
209
217
|
// tool 却返回 {closed:true}(谎报)。
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
vi.mocked(getChildByRecord).mockReturnValueOnce(child);
|
|
218
|
+
// [T2④] closeChatIdle 的 kill 收敛到 killRecordChildWithEscalation(recordId 入口),
|
|
219
|
+
// 不再读 child 句柄——无 child mock(Once mock 不被消费会泄漏劫持后续用例)
|
|
213
220
|
const record = makeRecord({ status: "running", round: 1 });
|
|
214
221
|
record.sessionFile = path.join(agentDir, "path-a.jsonl");
|
|
215
222
|
store.register(record);
|
|
@@ -217,7 +224,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
217
224
|
expect(hasIdleTimer(record.id)).toBe(true);
|
|
218
225
|
|
|
219
226
|
try {
|
|
220
|
-
await service.closeSubagent(record, false);
|
|
227
|
+
await service.chatActions.closeSubagent(record, false);
|
|
221
228
|
} finally {
|
|
222
229
|
disarmIdleTimer(record.id); // 断言失败路径的兜底清理
|
|
223
230
|
}
|
|
@@ -226,7 +233,8 @@ describe("closeSubagent 行为分流", () => {
|
|
|
226
233
|
expect(record.status).toBe("closed");
|
|
227
234
|
expect(record.closedReason).toBe("user-close");
|
|
228
235
|
expect(store.getMutable(record.id)).toBeUndefined(); // archived
|
|
229
|
-
|
|
236
|
+
// [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 30s SIGKILL 升级)
|
|
237
|
+
expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeChatIdle");
|
|
230
238
|
expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
|
|
231
239
|
});
|
|
232
240
|
|
|
@@ -235,7 +243,7 @@ describe("closeSubagent 行为分流", () => {
|
|
|
235
243
|
record.sessionFile = path.join(agentDir, "path-b.jsonl");
|
|
236
244
|
store.register(record);
|
|
237
245
|
|
|
238
|
-
await service.closeSubagent(record, false);
|
|
246
|
+
await service.chatActions.closeSubagent(record, false);
|
|
239
247
|
|
|
240
248
|
expect(record.status).toBe("closed");
|
|
241
249
|
expect(record.closedReason).toBe("user-close");
|
|
@@ -253,9 +261,8 @@ describe("closeSubagent 行为分流", () => {
|
|
|
253
261
|
it("[M6] running + force:true + 活进程(热路径轮中)→ cancelBackground 显式 SIGTERM + disarm idle timer", async () => {
|
|
254
262
|
// chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0) 返回,
|
|
255
263
|
// abort→kill listener 已被 removeEventListener——热路径轮中 cancel 只能靠显式 kill。
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
vi.mocked(getChildByRecord).mockReturnValueOnce(child);
|
|
264
|
+
// [T2④] cancelBackground 的 kill 收敛到 killRecordChildWithEscalation(recordId 入口),
|
|
265
|
+
// 不再读 child 句柄
|
|
259
266
|
const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "hot.jsonl") });
|
|
260
267
|
record.controller = new AbortController();
|
|
261
268
|
store.register(record);
|
|
@@ -263,12 +270,13 @@ describe("closeSubagent 行为分流", () => {
|
|
|
263
270
|
expect(hasIdleTimer(record.id)).toBe(true);
|
|
264
271
|
|
|
265
272
|
try {
|
|
266
|
-
await service.closeSubagent(record, true);
|
|
273
|
+
await service.chatActions.closeSubagent(record, true);
|
|
267
274
|
} finally {
|
|
268
275
|
disarmIdleTimer(record.id); // 断言失败路径的兜底清理
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
|
|
278
|
+
// [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
|
|
279
|
+
expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "cancelBackground");
|
|
272
280
|
expect(record.status).toBe("closed");
|
|
273
281
|
expect(record.closedReason).toBe("cancelled");
|
|
274
282
|
expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
|
|
@@ -276,6 +284,59 @@ describe("closeSubagent 行为分流", () => {
|
|
|
276
284
|
});
|
|
277
285
|
});
|
|
278
286
|
|
|
287
|
+
// ============================================================
|
|
288
|
+
// [A2-1] cancel 迟到:record 已终态(detached 已 finalize)→ CAS 拒绝
|
|
289
|
+
// ============================================================
|
|
290
|
+
|
|
291
|
+
describe("cancelBackground CAS 抢锁(A2-1)——cancel 迟到不覆写已完成 record", () => {
|
|
292
|
+
let agentDir: string;
|
|
293
|
+
let service: SubagentService;
|
|
294
|
+
let store: RecordStore;
|
|
295
|
+
let pi: MockPi;
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
({ agentDir, service, store, pi } = setup());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
service.dispose();
|
|
303
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("record 已 closed 未 archive(doFinalizeRecord Step 0 await 窗口形态)→ cancel 返回 false,不覆写终态/不写 tombstone/不 notify/不 archive", () => {
|
|
307
|
+
const sessionFile = path.join(agentDir, "late-cancel.jsonl");
|
|
308
|
+
const record = makeRecord({ status: "closed", sessionFile });
|
|
309
|
+
// 模拟 detached 已 finalize 的终态快照(closedReason=gc 正常完成,非 cancelled)
|
|
310
|
+
record.closedReason = "gc";
|
|
311
|
+
record.endedAt = 5555;
|
|
312
|
+
record.result = "detached final result";
|
|
313
|
+
store.register(record); // 未 archive:恰好处于 finalize await 窗口(收尾后段才 archive)
|
|
314
|
+
|
|
315
|
+
const cancelled = service.cancel(record.id);
|
|
316
|
+
|
|
317
|
+
expect(cancelled).toBe(false); // CAS 没抢到——不谎报 true
|
|
318
|
+
expect(record.status).toBe("closed");
|
|
319
|
+
expect(record.closedReason).toBe("gc"); // 不被 completeRecord 改写为 cancelled
|
|
320
|
+
expect(record.endedAt).toBe(5555); // finalize 冻结的终态值不被重写
|
|
321
|
+
expect(record.result).toBe("detached final result"); // 不被空 cancelled result 覆写
|
|
322
|
+
// 不写 cancelled tombstone(与 finalized marker 双标 = record 双重终态化)
|
|
323
|
+
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
324
|
+
expect(store.getMutable(record.id)).toBe(record); // 不 archive(record 留内存)
|
|
325
|
+
expect(pi.sendMessage).not.toHaveBeenCalled(); // 不重复 notify(detached finalize 已发)
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("record running → cancel 返回 true 且终态化(CAS 正常路径不回归)", () => {
|
|
329
|
+
const sessionFile = path.join(agentDir, "cancel-ok.jsonl");
|
|
330
|
+
const record = makeRecord({ status: "running", sessionFile });
|
|
331
|
+
store.register(record);
|
|
332
|
+
|
|
333
|
+
expect(service.cancel(record.id)).toBe(true);
|
|
334
|
+
expect(record.status).toBe("closed");
|
|
335
|
+
expect(record.closedReason).toBe("cancelled");
|
|
336
|
+
expect(store.getMutable(record.id)).toBeUndefined(); // archived
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
279
340
|
// ============================================================
|
|
280
341
|
// [M5] closeAfterRound 消费点:onRoundSettled(chatMode 热路径轮完成)
|
|
281
342
|
// ============================================================
|
|
@@ -294,7 +355,7 @@ describe("[M5] closeAfterRound 消费点挂 onRoundSettled(chatMode 轮完成
|
|
|
294
355
|
afterEach(() => {
|
|
295
356
|
service.dispose();
|
|
296
357
|
_resetLifecycleState();
|
|
297
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
358
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
298
359
|
});
|
|
299
360
|
|
|
300
361
|
it("热路径轮完成(onRoundSettled)+ closeAfterRound=true → 消费标志终态化(closed+user-close + archive)", async () => {
|
|
@@ -325,7 +386,8 @@ describe("[M5] closeAfterRound 消费点挂 onRoundSettled(chatMode 轮完成
|
|
|
325
386
|
expect(record.closeAfterRound).toBeUndefined(); // 标志已消费
|
|
326
387
|
expect(record.status).toBe("closed");
|
|
327
388
|
expect(record.closedReason).toBe("user-close");
|
|
328
|
-
|
|
389
|
+
// [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
|
|
390
|
+
expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeAfterRoundSettled");
|
|
329
391
|
expect(hasIdleTimer(record.id)).toBe(false);
|
|
330
392
|
});
|
|
331
393
|
|
|
@@ -367,7 +429,7 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
|
|
|
367
429
|
afterEach(() => {
|
|
368
430
|
service.dispose();
|
|
369
431
|
_resetLifecycleState();
|
|
370
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
432
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
371
433
|
});
|
|
372
434
|
|
|
373
435
|
it("close 语义:轮次通知(含指针行)发出后 closeAfterRoundSettled 终态化 + 终态通知,两条都送达", async () => {
|
|
@@ -412,8 +474,10 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
|
|
|
412
474
|
|
|
413
475
|
// running + 无活进程(getChildByRecord mock undefined)→ isResumable 放行 →
|
|
414
476
|
// 非 chatMode 映射 closed 通知,立即 flush(无其他 running background)
|
|
415
|
-
|
|
416
|
-
|
|
477
|
+
// [D4-①] notifyComplete 已随通知簇搬至 notify-host——按轴直接构造(同 pi/store
|
|
478
|
+
// 注入,行为与经 Service 的原路径等价;dedup 集为空与单次调用断言相容)
|
|
479
|
+
createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
|
|
480
|
+
.notifyComplete(record);
|
|
417
481
|
|
|
418
482
|
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
|
|
419
483
|
const msg = pi.sendMessage.mock.calls[0]![0] as {
|
|
@@ -432,8 +496,8 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
|
|
|
432
496
|
record.sessionFile = path.join(agentDir, "c2-chat.jsonl");
|
|
433
497
|
store.register(record);
|
|
434
498
|
|
|
435
|
-
|
|
436
|
-
|
|
499
|
+
createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
|
|
500
|
+
.notifyComplete(record);
|
|
437
501
|
|
|
438
502
|
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
|
|
439
503
|
const msg = pi.sendMessage.mock.calls[0]![0] as {
|
|
@@ -465,7 +529,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
|
|
|
465
529
|
afterEach(() => {
|
|
466
530
|
service.dispose();
|
|
467
531
|
_resetLifecycleState();
|
|
468
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
532
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
469
533
|
});
|
|
470
534
|
|
|
471
535
|
/** 真实回调链(与生产一致:agent_settled → onRoundSettled),turns 经 updateFromEvent 累积。 */
|
|
@@ -540,7 +604,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
|
|
|
540
604
|
updateFromEvent(record, { type: "text_delta", delta: "LAST-ROUND-INCREMENT" });
|
|
541
605
|
updateFromEvent(record, { type: "turn_end" });
|
|
542
606
|
settleRound(record); // 第 1 条:轮次通知
|
|
543
|
-
await service.closeSubagent(record, false); // idle close(D2 路径②)
|
|
607
|
+
await service.chatActions.closeSubagent(record, false); // idle close(D2 路径②)
|
|
544
608
|
} finally {
|
|
545
609
|
disarmIdleTimer(record.id);
|
|
546
610
|
}
|
|
@@ -590,7 +654,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
|
|
|
590
654
|
record.sessionFile = path.join(agentDir, "oneshot-close.jsonl");
|
|
591
655
|
store.register(record);
|
|
592
656
|
|
|
593
|
-
await service.closeSubagent(record, false); // resumable → closeChatIdle → notifyClosed 拒绝
|
|
657
|
+
await service.chatActions.closeSubagent(record, false); // resumable → closeChatIdle → notifyClosed 拒绝
|
|
594
658
|
|
|
595
659
|
expect(record.status).toBe("closed");
|
|
596
660
|
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
@@ -611,7 +675,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
|
|
|
611
675
|
updateFromEvent(record, { type: "turn_end" });
|
|
612
676
|
settleRound(record); // 轮终:onRoundSettled 写 record.result = "ok"
|
|
613
677
|
expect(record.result).toBe("ok"); // 前提锚点:轮终真实值在位
|
|
614
|
-
await service.closeSubagent(record, false); // idle close → completeRecord + archive
|
|
678
|
+
await service.chatActions.closeSubagent(record, false); // idle close → completeRecord + archive
|
|
615
679
|
} finally {
|
|
616
680
|
disarmIdleTimer(record.id);
|
|
617
681
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/execution/__tests__/subagent-service-multiproc-guard.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [u-svc / T5] 多进程共享文件守卫(subagent-service 侧两项):
|
|
4
|
+
// - T5①/PS-8:子进程 initSession 跳过 recoverOrphanRecords(env PI_SUBAGENT_SELF_RECORD_ID
|
|
5
|
+
// 判子进程身份)——恢复机制假设「单扫描者」,env 贯穿让每个子进程都成了扫描者,
|
|
6
|
+
// 兄弟记录 marker 缺失/超软超时被无关进程盖终态 sidecar;
|
|
7
|
+
// - T5③/PS-7b:running 候选冷查补 findForeignLiveInstance 探针守卫——异进程活实例在册
|
|
8
|
+
// 时拒绝 resurrect + resume spawn(防双写者窗口)。
|
|
9
|
+
//
|
|
10
|
+
// mock 形态沿用 subagent-service-message-close.test.ts;alive-store mock(findForeignLiveInstance
|
|
11
|
+
// 换 spy 控制探针命中)。
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
20
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
23
|
+
|
|
24
|
+
const { killChildSpy, foreignLiveSpy } = vi.hoisted(() => ({
|
|
25
|
+
killChildSpy: vi.fn(),
|
|
26
|
+
foreignLiveSpy: vi.fn<() => { pid: number; id: string; startedAt: number } | undefined>(),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../alive-store.ts", () => ({
|
|
29
|
+
writeAliveMarker: vi.fn(),
|
|
30
|
+
removeAliveMarker: vi.fn(),
|
|
31
|
+
readAliveMarker: vi.fn(() => undefined),
|
|
32
|
+
isProcessAlive: vi.fn(() => false),
|
|
33
|
+
ALIVE_SOFT_TIMEOUT_MS: 3_600_000,
|
|
34
|
+
findForeignLiveInstance: foreignLiveSpy,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
38
|
+
runSpawn: vi.fn(),
|
|
39
|
+
killAllSpawnedChildren: vi.fn(),
|
|
40
|
+
getChildByRecord: vi.fn(() => undefined),
|
|
41
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
42
|
+
spawnedChildren: new Map(),
|
|
43
|
+
killRecordChildWithEscalation: killChildSpy,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
47
|
+
import type { RecordStore } from "../record-store.ts";
|
|
48
|
+
import { SubagentService } from "../subagent-service.ts";
|
|
49
|
+
import { ResurrectDeniedError } from "../types.ts";
|
|
50
|
+
import type { PiLike } from "../subagent-service.ts";
|
|
51
|
+
|
|
52
|
+
function makeTmpAgentDir(): string {
|
|
53
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "svc-multiproc-"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makePi(): PiLike {
|
|
57
|
+
return {
|
|
58
|
+
appendEntry: vi.fn(),
|
|
59
|
+
events: { emit: vi.fn() },
|
|
60
|
+
sendMessage: vi.fn(),
|
|
61
|
+
} as unknown as PiLike;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setup(): { agentDir: string; service: SubagentService; store: RecordStore } {
|
|
65
|
+
const agentDir = makeTmpAgentDir();
|
|
66
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
67
|
+
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
68
|
+
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
69
|
+
const store = (service as unknown as { store: RecordStore }).store;
|
|
70
|
+
return { agentDir, service, store };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("T5① child process skips orphan recovery scan", () => {
|
|
74
|
+
let agentDir: string;
|
|
75
|
+
|
|
76
|
+
/** 构造 service 并在其 store 上挂 spy 后再 initSession(spy 必须先于触发点)。 */
|
|
77
|
+
const makeServiceWithRecoverySpies = (): {
|
|
78
|
+
service: SubagentService;
|
|
79
|
+
recover: ReturnType<typeof vi.spyOn>;
|
|
80
|
+
entryOnly: ReturnType<typeof vi.spyOn>;
|
|
81
|
+
} => {
|
|
82
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
83
|
+
const svc = new SubagentService({ cwd: agentDir, modelService });
|
|
84
|
+
const store = (svc as unknown as { store: RecordStore }).store;
|
|
85
|
+
const recover = vi.spyOn(store, "recoverOrphanRecords").mockImplementation(() => {});
|
|
86
|
+
const entryOnly = vi.spyOn(store, "recoverEntryOnlyOrphans").mockImplementation(() => {});
|
|
87
|
+
return { service: svc, recover, entryOnly };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.stubEnv("PI_SUBAGENT_SELF_RECORD_ID", "");
|
|
92
|
+
agentDir = makeTmpAgentDir();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.unstubAllEnvs();
|
|
97
|
+
vi.restoreAllMocks();
|
|
98
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("root process (no self-record env) runs the orphan recovery scan", () => {
|
|
102
|
+
const { service: root, recover, entryOnly } = makeServiceWithRecoverySpies();
|
|
103
|
+
root.initSession({ pi: makePi(), sessionId: "root-session-2" });
|
|
104
|
+
expect(recover).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(entryOnly).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("child process (PI_SUBAGENT_SELF_RECORD_ID set) skips the scan entirely", () => {
|
|
109
|
+
vi.stubEnv("PI_SUBAGENT_SELF_RECORD_ID", "sa-child-self");
|
|
110
|
+
const { service: child, recover, entryOnly } = makeServiceWithRecoverySpies();
|
|
111
|
+
child.initSession({ pi: makePi(), sessionId: "child-session" });
|
|
112
|
+
expect(recover).not.toHaveBeenCalled();
|
|
113
|
+
expect(entryOnly).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("T5③ cold-lookup running candidate foreign-instance guard", () => {
|
|
118
|
+
let agentDir: string;
|
|
119
|
+
let service: SubagentService;
|
|
120
|
+
let store: RecordStore;
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
foreignLiveSpy.mockReset();
|
|
124
|
+
foreignLiveSpy.mockReturnValue(undefined);
|
|
125
|
+
({ agentDir, service, store } = setup());
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
vi.restoreAllMocks();
|
|
130
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const stubColdCandidate = (record: {
|
|
134
|
+
id: string;
|
|
135
|
+
status: "running";
|
|
136
|
+
sessionFile: string;
|
|
137
|
+
rootSessionId: string;
|
|
138
|
+
parentRecordId?: string;
|
|
139
|
+
}): void => {
|
|
140
|
+
vi.spyOn(store, "findLightById").mockReturnValue(undefined);
|
|
141
|
+
vi.spyOn(store, "collectRecords").mockReturnValue([record] as never);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it("denies resurrection with ResurrectDeniedError when a foreign live instance holds the session", () => {
|
|
145
|
+
stubColdCandidate({
|
|
146
|
+
id: "sa-foreign",
|
|
147
|
+
status: "running",
|
|
148
|
+
sessionFile: "/tmp/fake-session.jsonl",
|
|
149
|
+
rootSessionId: "root-session",
|
|
150
|
+
});
|
|
151
|
+
foreignLiveSpy.mockReturnValue({ pid: 4242, id: "sa-foreign", startedAt: Date.now() });
|
|
152
|
+
|
|
153
|
+
expect(() => service["getRecordForAction"]("sa-foreign")).toThrow(ResurrectDeniedError);
|
|
154
|
+
try {
|
|
155
|
+
service["getRecordForAction"]("sa-foreign");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
expect((err as Error).message).toContain("4242");
|
|
158
|
+
expect((err as Error).message).toContain("double-write");
|
|
159
|
+
expect((err as Error).message).toContain("Recovery");
|
|
160
|
+
}
|
|
161
|
+
expect(foreignLiveSpy).toHaveBeenCalledWith("/tmp/fake-session.jsonl");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("allows cold lookup when no foreign live instance (marker dead/stale)", () => {
|
|
165
|
+
stubColdCandidate({
|
|
166
|
+
id: "sa-cold",
|
|
167
|
+
status: "running",
|
|
168
|
+
sessionFile: "/tmp/fake-session-2.jsonl",
|
|
169
|
+
rootSessionId: "root-session",
|
|
170
|
+
});
|
|
171
|
+
const found = service["getRecordForAction"]("sa-cold");
|
|
172
|
+
expect(found.id).toBe("sa-cold");
|
|
173
|
+
});
|
|
174
|
+
});
|