@zhushanwen/subagent-core 0.4.0 → 0.6.0
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/dist/{chunk-4KN4TTG7.js → chunk-43ONBFZX.js} +1 -1
- package/dist/{chunk-X7SZ5HLQ.js → chunk-V3VHZ2VX.js} +20 -27
- package/dist/execution/engine/engines/zcode/constants.cjs +29 -41
- package/dist/execution/engine/engines/zcode/constants.d.cts +65 -53
- package/dist/execution/engine/engines/zcode/constants.d.ts +65 -53
- package/dist/execution/engine/engines/zcode/constants.js +17 -27
- 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 +2 -2
- package/dist/index.cjs +17100 -14082
- package/dist/index.d.cts +5336 -4033
- package/dist/index.d.ts +5336 -4033
- package/dist/index.js +17020 -14055
- package/dist/{types-DpUO16pj.d.cts → types-C3fE3R9x.d.cts} +280 -112
- package/dist/{types-DpUO16pj.d.ts → types-C3fE3R9x.d.ts} +280 -112
- package/dist.bundle/index.cjs +34933 -0
- package/package.json +3 -3
- 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 +4 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +10 -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/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 +4 -4
- 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 +2 -2
- package/src/execution/__tests__/alive-store.test.ts +1 -1
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +68 -33
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +4 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/collect-budget.test.ts +291 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +327 -0
- package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +262 -0
- package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
- package/src/execution/__tests__/config.test.ts +1 -1
- package/src/execution/__tests__/delivery-methods.test.ts +75 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +6 -6
- package/src/execution/__tests__/dialog-queue.test.ts +2 -2
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +29 -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 +5 -5
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +4 -4
- package/src/execution/__tests__/finalize-record.test.ts +173 -17
- package/src/execution/__tests__/finalized-marker.test.ts +1 -1
- package/src/execution/__tests__/gc-timer.test.ts +56 -3
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +12 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +1 -1
- 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 +50 -148
- package/src/execution/__tests__/kill-all-escalation.test.ts +2 -2
- 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 +1 -1
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +1 -1
- 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-batch.test.ts +500 -0
- package/src/execution/__tests__/notify-ledger.test.ts +1000 -18
- package/src/execution/__tests__/output-collector.test.ts +117 -6
- package/src/execution/__tests__/pi-invocation.test.ts +36 -4
- package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
- package/src/execution/__tests__/record-store.test.ts +10 -9
- 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__/relay-env.test.ts +12 -1
- 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 +15 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +4 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +17 -15
- package/src/execution/__tests__/run-spawn-integration.test.ts +2 -2
- 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 +4 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +7 -5
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
- package/src/execution/__tests__/session-file-gc.test.ts +36 -1
- package/src/execution/__tests__/session-pending.test.ts +257 -14
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +1 -1
- package/src/execution/__tests__/session-runner-close-prune.test.ts +2 -2
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +3 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +5 -4
- 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 +282 -47
- 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 +3 -3
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
- package/src/execution/__tests__/spawned-children.test.ts +2 -1
- package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +5 -5
- 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 +48 -22
- package/src/execution/__tests__/subagent-service-message-close.test.ts +29 -26
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +21 -8
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -22
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +30 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +45 -20
- 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__/sync-collect-recovery.test.ts +1117 -0
- package/src/execution/__tests__/temp-prompt.test.ts +1 -1
- package/src/execution/__tests__/timeout-integration.test.ts +6 -5
- package/src/execution/__tests__/tombstone-store.test.ts +1 -1
- 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 +12 -6
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +142 -2
- package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
- 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 +2 -2
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
- package/src/execution/__tests__/worktree-registry.test.ts +1 -1
- package/src/execution/agent-registry.ts +74 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/cold-resurrect.ts +199 -0
- package/src/execution/collect-coordinator.ts +200 -0
- package/src/execution/config.ts +77 -5
- 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/event-journal.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 +213 -53
- 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 +22 -112
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +15 -28
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +12 -12
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +2 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +21 -82
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +4 -7
- 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 +4 -2
- 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/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} +1 -1
- 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/{session-runner.ts → engine/engines/pi/session-runner.ts} +617 -412
- 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 +54 -27
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +13 -12
- 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 +20 -8
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +77 -110
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +26 -320
- 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 +48 -148
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +24 -11
- package/src/execution/engine/engines/zcode/constants.ts +75 -71
- package/src/execution/engine/engines/zcode/golden-sample.ts +8 -46
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +12 -143
- package/src/execution/engine/engines/zcode/session-channel.ts +351 -39
- package/src/execution/engine/engines/zcode/zcode-engine.ts +665 -760
- 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 +24 -6
- package/src/execution/engine/routing.ts +75 -1
- package/src/execution/engine/types.ts +30 -103
- package/src/execution/execution-record.ts +229 -144
- package/src/execution/finalize-record.ts +141 -99
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/notifier.ts +234 -25
- package/src/execution/notify-host.ts +232 -0
- package/src/execution/record-entry.ts +16 -0
- package/src/execution/record-store.ts +280 -103
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +117 -47
- package/src/execution/settled-watchdog.ts +264 -52
- package/src/execution/subagent-actions-core.ts +84 -28
- package/src/execution/subagent-service.ts +1225 -964
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/sync-rebuild.ts +87 -0
- package/src/execution/types.ts +72 -0
- package/src/execution/ui-request-handler-factory.ts +102 -74
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/index.ts +442 -316
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +1 -1
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/config-loader.test.ts +1 -1
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +1 -1
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +11 -9
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
- package/src/orchestration/__tests__/file-run-store.test.ts +7 -6
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +2 -2
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +1 -1
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +7 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +8 -5
- package/src/orchestration/__tests__/script-generate.test.ts +5 -5
- package/src/orchestration/__tests__/script-lint.test.ts +17 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
- 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} +27 -19
- 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 +1 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +1 -1
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +19 -73
- package/src/orchestration/launcher.ts +128 -81
- package/src/orchestration/lifecycle.ts +43 -45
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +2 -2
- package/src/orchestration/models/trace.ts +1 -1
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +1 -1
- package/src/orchestration/script-lint.ts +91 -58
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +157 -150
- package/src/shared/__tests__/atomic-write.test.ts +1 -1
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
- package/src/shared/__tests__/resource-discovery.test.ts +258 -215
- package/src/shared/atomic-write.ts +12 -9
- package/src/shared/meta-parser.ts +158 -84
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +9 -183
- package/src/shared/schema-jsonify.ts +1 -1
- package/workflows/review-fix-loop-utils.cjs +151 -89
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- 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__/preparer-appserver.test.ts +0 -387
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -710
- package/src/execution/engine/engines/zcode/appserver-home.ts +0 -442
- package/src/execution/engine/engines/zcode/appserver-probe.ts +0 -141
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/execution/engine/engines/pi/session-runner.ts
|
|
2
2
|
//
|
|
3
3
|
// spawn pi --mode rpc 子进程执行 session 的编排器。零 mode 感知。
|
|
4
4
|
//
|
|
@@ -14,56 +14,60 @@ import {
|
|
|
14
14
|
} from "node:child_process";
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
|
|
17
|
-
import { getLogger } from "
|
|
18
|
-
import { bestEffort } from "
|
|
19
|
-
import { disposeEngines } from "
|
|
20
|
-
import { armIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "
|
|
17
|
+
import { getLogger } from "../../../../core/logger.ts";
|
|
18
|
+
import { bestEffort } from "../../../best-effort.ts";
|
|
19
|
+
import { disposeEngines } from "../../registry.ts";
|
|
20
|
+
import { armIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "../../../lifecycle-manager.ts";
|
|
21
21
|
import {
|
|
22
22
|
listActivePendingFromSessionFile,
|
|
23
23
|
prunePendingCursor,
|
|
24
24
|
readActivePendingFromSessionFile,
|
|
25
25
|
type ActivePendingResult,
|
|
26
|
-
} from "
|
|
26
|
+
} from "../../../session-pending.ts";
|
|
27
|
+
import { killChain } from "../../common/kill-chain.ts";
|
|
27
28
|
|
|
28
|
-
import type { ExtensionMode } from "
|
|
29
|
+
import type { ExtensionMode } from "../../../host-mode.ts";
|
|
29
30
|
|
|
30
31
|
import { type MirrorFlags, mirrorMainProcessFlags } from "./argv-mirror.ts";
|
|
31
|
-
import { isProcessAlive, readAliveMarker, writeAliveMarker } from "
|
|
32
|
-
import { type DialogGlobalQueue, type UiRequestHandler } from "
|
|
33
|
-
import { updateFromEvent } from "
|
|
32
|
+
import { isProcessAlive, readAliveMarker, writeAliveMarker } from "../../../alive-store.ts";
|
|
33
|
+
import { type DialogGlobalQueue, type UiRequestHandler } from "../../../dialog-queue.ts";
|
|
34
|
+
import { updateFromEvent } from "../../../execution-record.ts";
|
|
34
35
|
import {
|
|
35
36
|
type AddGetStateResponseListener,
|
|
36
37
|
type GetStateResult,
|
|
37
38
|
performGetStateHandshake,
|
|
38
39
|
requestGetStateOnce,
|
|
39
40
|
} from "./get-state-handshake.ts";
|
|
40
|
-
import { willRespondToAskUser } from "
|
|
41
|
-
import type { AgentConfig, ResolvedModel } from "
|
|
41
|
+
import { willRespondToAskUser } from "../../../host-mode.ts";
|
|
42
|
+
import type { AgentConfig, ResolvedModel } from "../../../model-resolver.ts";
|
|
42
43
|
import { collectResult } from "./output-collector.ts";
|
|
43
|
-
import { getSubagentSessionDir } from "
|
|
44
|
+
import { getSubagentSessionDir } from "../../../path-encoding.ts";
|
|
44
45
|
import { getPiInvocation } from "./pi-invocation.ts";
|
|
45
|
-
import { isRelayActive, RELAY_ENV_RECORD_ID, RELAY_ENV_SESSION_ID } from "
|
|
46
|
-
import { assertThinkingLevel, type ThinkingLevel } from "
|
|
46
|
+
import { isRelayActive, RELAY_ENV_RECORD_ID, RELAY_ENV_SESSION_ID } from "../../../relay-env.ts";
|
|
47
|
+
import { assertThinkingLevel, type ThinkingLevel } from "../../../../shared/model-ref";
|
|
47
48
|
import {
|
|
48
49
|
SCHEMA_ENV_MAX_BYTES,
|
|
49
50
|
SCHEMA_ENV_VAR,
|
|
50
51
|
schemaEnvByteLength,
|
|
51
|
-
} from "
|
|
52
|
-
import { assertSafeTimerDelay } from "
|
|
52
|
+
} from "../../../../shared/schema-env.ts";
|
|
53
|
+
import { assertSafeTimerDelay } from "../../../../shared/timer-delay.ts";
|
|
53
54
|
import {
|
|
54
|
-
|
|
55
|
+
armMidRoundNoProgress,
|
|
55
56
|
disarmSettledWatchdog,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
handoverMidRoundToSettled,
|
|
58
|
+
refreshMidRoundNoProgress,
|
|
59
|
+
type SettledWatchdogFireInfo,
|
|
60
|
+
} from "../../../settled-watchdog.ts";
|
|
61
|
+
import { MAX_FORK_DEPTH } from "../../../session-context-resolver.ts";
|
|
59
62
|
import { EPIPE_FAILURE_THRESHOLD, recordEpipeFailure, sendPromptCommand } from "./stdin-writer.ts";
|
|
60
63
|
import {
|
|
61
64
|
deriveSessionFilePath,
|
|
62
65
|
findSessionFileByHeaderId,
|
|
63
66
|
parseSpawnLine,
|
|
67
|
+
type ParsedSpawnLine,
|
|
64
68
|
type SpawnSessionHeader,
|
|
65
69
|
} from "./spawn-event-adapter.ts";
|
|
66
|
-
import type { SubagentStream } from "
|
|
70
|
+
import type { SubagentStream } from "../../../stream-sink.ts";
|
|
67
71
|
import {
|
|
68
72
|
cleanupTempPrompt,
|
|
69
73
|
writePromptToTempFile,
|
|
@@ -74,9 +78,10 @@ import type {
|
|
|
74
78
|
ExecutionRecord,
|
|
75
79
|
SdkEvent,
|
|
76
80
|
WorktreeHandle,
|
|
77
|
-
} from "
|
|
81
|
+
} from "../../../types.ts";
|
|
78
82
|
import { createTurnLimiter, WRAP_UP_HINT } from "./turn-limiter.ts";
|
|
79
|
-
import { createUiRequestQueue } from "
|
|
83
|
+
import { createUiRequestQueue } from "../../../ui-request-queue.ts";
|
|
84
|
+
import { toErrorMessage } from "../../../../core/error-message.ts";
|
|
80
85
|
|
|
81
86
|
const logger = getLogger("subagents");
|
|
82
87
|
|
|
@@ -136,6 +141,28 @@ export function mapAssistantMessageDelta(
|
|
|
136
141
|
return null;
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/**
|
|
145
|
+
* [圈复杂度门禁提取] tool_execution_end 的 args 回填(自 createSpawnEventHandlers 的
|
|
146
|
+
* switch case 体迁出的纯函数,行为逐字节保持):tool_end 可能缺 args,用 tool_start
|
|
147
|
+
* 寄存进 pendingTools 的 args 回填,命中后即消费(delete)。
|
|
148
|
+
*
|
|
149
|
+
* 与 mapAssistantMessageDelta 同域(SdkEvent 翻译纯函数),提取为模块级便于单测。
|
|
150
|
+
*/
|
|
151
|
+
function resolveToolEndArgs(
|
|
152
|
+
raw: SdkEvent,
|
|
153
|
+
pendingTools: Map<string, { toolName: string; args?: unknown }>,
|
|
154
|
+
): unknown {
|
|
155
|
+
let args = raw.args;
|
|
156
|
+
if (raw.toolCallId) {
|
|
157
|
+
const pending = pendingTools.get(raw.toolCallId);
|
|
158
|
+
if (pending) {
|
|
159
|
+
if (args === undefined) args = pending.args;
|
|
160
|
+
pendingTools.delete(raw.toolCallId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return args;
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
// ============================================================
|
|
140
167
|
// 常量
|
|
141
168
|
// ============================================================
|
|
@@ -354,7 +381,7 @@ const STDERR_MAX_CHARS = 65_536;
|
|
|
354
381
|
* 抽叶子的目的:跨包契约测试从 session-runner import 会拖入整条 spawn/pi SDK 依赖树,
|
|
355
382
|
* 叶子模块提供稳定 import 点(导出名与值不变)。
|
|
356
383
|
*/
|
|
357
|
-
export { SCHEMA_ENV_MAX_BYTES
|
|
384
|
+
export { SCHEMA_ENV_MAX_BYTES };
|
|
358
385
|
|
|
359
386
|
// ============================================================
|
|
360
387
|
// W4: ask_user RPC 系统提示词
|
|
@@ -396,7 +423,7 @@ The \`ask_user\` tool is available in this session. When you call \`ask_user\`,
|
|
|
396
423
|
* 此提示在 worktree 模式下注入,明确告知子 agent:cwd 是含完整项目代码的 git worktree,
|
|
397
424
|
* 直接在此工作即可,不要 cd 别处找"真正的项目"。
|
|
398
425
|
*/
|
|
399
|
-
|
|
426
|
+
const WORKTREE_GUIDANCE_PROMPT = `
|
|
400
427
|
## Working Directory Is a Git Worktree
|
|
401
428
|
|
|
402
429
|
Your working directory (the "Working directory" in the environment block above) is a **dedicated git worktree** — an isolated checkout of the repository at HEAD, NOT a temporary sandbox. It contains the **complete project source code**.
|
|
@@ -488,7 +515,7 @@ export function killAllSpawnedChildren(signal: NodeJS.Signals = "SIGTERM"): numb
|
|
|
488
515
|
// debug 级留诊断线索即可,不刷 info/warn
|
|
489
516
|
logger.debug(
|
|
490
517
|
`[session-runner] killAllSpawnedChildren: kill failed (best-effort continue): ${
|
|
491
|
-
|
|
518
|
+
toErrorMessage(err)
|
|
492
519
|
}`,
|
|
493
520
|
);
|
|
494
521
|
}
|
|
@@ -520,8 +547,8 @@ export function getChildByRecord(recordId: string): ChildProcess | undefined {
|
|
|
520
547
|
* 背景:spawnedChildren 是 Map<recordId, ChildProcess>,resume spawn 会 set 覆盖旧句柄。
|
|
521
548
|
* 旧 child 的 close/error 事件异步到达(kill 后 close 回调可能晚数 tick),若 close/error
|
|
522
549
|
* handler 无条件 delete(record.id),会误删 resume spawn 刚注册的**新** child——
|
|
523
|
-
* 时序:idle timer 对旧 child SIGTERM(killed=true 但 close 未到)→
|
|
524
|
-
* child.killed
|
|
550
|
+
* 时序:idle timer 对旧 child SIGTERM(killed=true 但 close 未到)→ 投递方(PiEngine
|
|
551
|
+
* deliverPrompt)判 child.killed 走冷路径续轮 → spawn 新 child set 覆盖 → 旧 child close 此刻到达 →
|
|
525
552
|
* 误删新注册。后果:新活进程脱离记账(killAllSpawnedChildren 漏杀孤儿 + getChildByRecord
|
|
526
553
|
* undefined → busy 投递再走冷路径二次 resume → 两进程写同一 session 文件,v4 A-5 注释
|
|
527
554
|
* 自述的 P7 双写者事故模式)。按句相等守卫:set 覆盖后旧句柄 ≠ Map 当前值,天然跳过。
|
|
@@ -608,7 +635,7 @@ function killPidWithEscalation(pid: number, label: string): void {
|
|
|
608
635
|
// SIGTERM 发送即失败(进程恰死 / 权限):无需升级,诊断留痕
|
|
609
636
|
logger.debug(
|
|
610
637
|
`[session-runner] ${label}: SIGTERM to pid ${pid} failed (best-effort continue): ${
|
|
611
|
-
|
|
638
|
+
toErrorMessage(err)
|
|
612
639
|
}`,
|
|
613
640
|
);
|
|
614
641
|
return;
|
|
@@ -1020,7 +1047,7 @@ export async function buildEnvBlock(
|
|
|
1020
1047
|
// 非 git 目录 / git 不在 PATH 是高频正常路径,debug 级留诊断线索即可,不刷 info/warn
|
|
1021
1048
|
logger.debug(
|
|
1022
1049
|
`[session-runner] buildEnvBlock: git branch lookup failed for ${cwd}, fallback to empty: ${
|
|
1023
|
-
|
|
1050
|
+
toErrorMessage(err)
|
|
1024
1051
|
}`,
|
|
1025
1052
|
);
|
|
1026
1053
|
branch = "";
|
|
@@ -1180,7 +1207,7 @@ function writeAliveMarkerBestEffort(sessionFile: string, pid: number, id: string
|
|
|
1180
1207
|
// best-effort:alive marker 失败不影响执行;debug 级留诊断线索即可,不刷 info/warn
|
|
1181
1208
|
logger.debug(
|
|
1182
1209
|
`[session-runner] alive marker write failed (best-effort continue): ${
|
|
1183
|
-
|
|
1210
|
+
toErrorMessage(err)
|
|
1184
1211
|
}`,
|
|
1185
1212
|
);
|
|
1186
1213
|
}
|
|
@@ -1222,6 +1249,13 @@ function touchAliveMarkerForHeartbeat(sessionFile: string | undefined, pid: numb
|
|
|
1222
1249
|
// - buildSpawnInvocation:i. pi CLI 参数组装与入口解析
|
|
1223
1250
|
// - attachStdoutPump:stdout 逐行解析 + get_state 握手状态机 + agent_end keep-alive
|
|
1224
1251
|
// - waitForChildExit:close/error → exitCode(含统一 cleanup)
|
|
1252
|
+
// - setupFreshChild:spawn 后同步装配(worktree pid 补全 / 记账 / stdio / prompt 命令)
|
|
1253
|
+
// - armChatModeSettledWatchdog:chatMode 首轮 settled 两段守护挂载
|
|
1254
|
+
// - attachAbortAndSpawnWatchdog:abort 监听 + spawn watchdog(返回 onAbort 供注销)
|
|
1255
|
+
// - startGetStateHandshake:get_state 握手 fire-and-forget 启动(含 F2 catch 兜底)
|
|
1256
|
+
// - backfillSessionFileByLookup:收尾 sessionFile 兜底反查(LC-4)
|
|
1257
|
+
// - sweepDescendantsOnChildClose:收尾后代级联补杀(T2-② 后半)
|
|
1258
|
+
// - resolveRunOutcome:成功/失败四分支判定
|
|
1225
1259
|
// 拆分只移动代码不改行为——runSpawn 导出签名与事件语义不变。
|
|
1226
1260
|
|
|
1227
1261
|
/** runSpawn 各阶段共享的可变状态(原闭包变量收拢,经参数在阶段函数间传递)。 */
|
|
@@ -1233,9 +1267,6 @@ interface SpawnRunState {
|
|
|
1233
1267
|
proc: ChildProcess | undefined;
|
|
1234
1268
|
/** watchdog timer(stdout handler 的 agent_end keep-alive 分支重挂,收尾统一 clearTimeout)。 */
|
|
1235
1269
|
watchdog: NodeJS.Timeout | undefined;
|
|
1236
|
-
/** [race-F4] SIGKILL 升级 timer(killChildWithEscalation 挂载;exit 事件自动 clear,
|
|
1237
|
-
* 收尾统一 clearTimeout 兑底)。 */
|
|
1238
|
-
escalationTimer: NodeJS.Timeout | undefined;
|
|
1239
1270
|
/** stdout 首行 header(json mode 才有;RPC mode 恒 undefined)——收尾 sessionFile 兜底查找用。 */
|
|
1240
1271
|
sessionHeader: SpawnSessionHeader | undefined;
|
|
1241
1272
|
/** get_state 握手结果(RPC mode)——收尾 sessionFile 兜底查找用。 */
|
|
@@ -1260,12 +1291,17 @@ interface SpawnRunState {
|
|
|
1260
1291
|
*/
|
|
1261
1292
|
sweepDescendantsOnClose: boolean;
|
|
1262
1293
|
/**
|
|
1263
|
-
* [T2-③ /
|
|
1264
|
-
* settled-watchdog.ts 的
|
|
1265
|
-
* 热路径挂载在 subagent-service deliverMessage
|
|
1294
|
+
* [T2-③ / D9 两段式] chatMode 首轮 settled 等待守护(timer 句柄记账在
|
|
1295
|
+
* settled-watchdog.ts 的 armedEntries Map,按 recordId arm/disarm/交棒;双挂载原语,
|
|
1296
|
+
* 热路径挂载在 subagent-service deliverMessage)。中段无进展检测 + 收尾段固定上界
|
|
1297
|
+
* 两段合用本标记。
|
|
1298
|
+
*/
|
|
1299
|
+
/**
|
|
1300
|
+
* [T2-③] settled watchdog 已触发及触发段——runSpawn 收尾据此转 failed + 恢复指引
|
|
1301
|
+
* (非正常完成)。undefined = 未触发;mid-round = 中段无进展(30min 静默);
|
|
1302
|
+
* settled = 收尾段固定上界(agent_end 后 600s)。
|
|
1266
1303
|
*/
|
|
1267
|
-
|
|
1268
|
-
settledWatchdogFired: boolean;
|
|
1304
|
+
settledWatchdogFired: SettledWatchdogFireInfo | undefined;
|
|
1269
1305
|
}
|
|
1270
1306
|
|
|
1271
1307
|
/**
|
|
@@ -1275,46 +1311,63 @@ interface SpawnRunState {
|
|
|
1275
1311
|
* 对外只暴露 handleSdkEvent(stdout 解析出的 SdkEvent 的唯一喂入口)。
|
|
1276
1312
|
*/
|
|
1277
1313
|
|
|
1314
|
+
/**
|
|
1315
|
+
* [D3-① 杀链合一] pi 侧杀链参数(现状值,[race-F4]):SIGTERM 优雅窗口 30s——
|
|
1316
|
+
* 超窗未见 exit 视为 SIGTERM 被无视,升级 SIGKILL。秒数单提命名常量再组合 ms:
|
|
1317
|
+
* no-magic-numbers 豁免 const init 单字面量但不豁免乘法表达式中的裸字面量
|
|
1318
|
+
* (对齐上方 watchdog 系列的常量组合写法)。
|
|
1319
|
+
*/
|
|
1320
|
+
const PI_KILL_GRACE_SECS = 30;
|
|
1321
|
+
const PI_KILL_GRACE_MS = PI_KILL_GRACE_SECS * MS_PER_SECOND;
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* [D3-① 杀链合一 + race-F4] 杀 pi 子进程:发 SIGTERM,30s 优雅窗口后仍存活则 SIGKILL。
|
|
1325
|
+
*
|
|
1326
|
+
* 唯一实现 = common/kill-chain.killChain(本函数只是 pi 参数装配:grace 30s、timer
|
|
1327
|
+
* unref(不阻止主进程退出,dispose 路径 killAllSpawnedChildren 兜底)、SIGKILL 升级
|
|
1328
|
+
* warn 留痕含 record id 与来源)。fire-and-forget:调用方不 await 结果(子进程 close
|
|
1329
|
+
* 事件驱动后续收尾),killChain 内部 exit 事件自动清升级 timer。
|
|
1330
|
+
*
|
|
1331
|
+
* 背景(race-F4):watchdog/limiter/idle timer/abort 触发只发一次 SIGTERM——子进程若
|
|
1332
|
+
* 无视 SIGTERM(卡死在不可中断的 native 调用 / SIGTERM handler 挂死),close 永不触发
|
|
1333
|
+
* → runSpawn 悬挂、background 槽位/worktree/alive marker 泄漏。
|
|
1334
|
+
*
|
|
1335
|
+
* @param source 升级来源标识(warn 日志定位用,如 "spawn watchdog" / "turn limiter")
|
|
1336
|
+
*/
|
|
1337
|
+
function killPiChild(child: ChildProcess, recordId: string, source: string): void {
|
|
1338
|
+
void killChain(child, {
|
|
1339
|
+
graceMs: PI_KILL_GRACE_MS,
|
|
1340
|
+
unrefTimers: true,
|
|
1341
|
+
escalationNote: `child ${recordId} (source: ${source})`,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1278
1345
|
/**
|
|
1279
1346
|
* [race-F4] SIGTERM 后升级 SIGKILL 的等待窗口:30s 未见 exit 视为 SIGTERM 被无视,强杀。
|
|
1347
|
+
* (与上方 PI_KILL_GRACE 同值——runSpawn 内外两杀链入口的现状窗口一致。)
|
|
1280
1348
|
*/
|
|
1281
1349
|
const SIGKILL_ESCALATION_SECONDS = 30;
|
|
1282
1350
|
const SIGKILL_ESCALATION_MS = SIGKILL_ESCALATION_SECONDS * MS_PER_SECOND;
|
|
1283
1351
|
|
|
1284
1352
|
/**
|
|
1285
|
-
* [race-F4]
|
|
1286
|
-
*
|
|
1353
|
+
* [race-F4] runSpawn 作用域内 kill 收敛入口(持有 SpawnRunState 的调用方):
|
|
1354
|
+
* 发 SIGTERM 并武装 30s SIGKILL 升级。
|
|
1287
1355
|
*
|
|
1288
1356
|
* 背景:watchdog/limiter/idle timer/abort 触发只发一次 SIGTERM——子进程若无视
|
|
1289
1357
|
* SIGTERM(卡死在不可中断的 native 调用 / SIGTERM handler 挂死),close 永不触发
|
|
1290
1358
|
* → runSpawn 悬挂、background 槽位/worktree/alive marker 泄漏,旧实现永不回收。
|
|
1291
1359
|
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1360
|
+
* [D3-① 合并注] 实现已合一到公共杀链(killPiChild → common/kill-chain.killChain,
|
|
1361
|
+
* grace 30s 与原私有链现状逐字节保持)——升级 timer 的记账与清理由 killChain 内部
|
|
1362
|
+
* 承担(exit 事件 settle 自动 clear),原 state.escalationTimer 兜底句柄随旧私有
|
|
1363
|
+
* 链删除(runSpawn 收尾不再 clearTimeout)。本函数保留 state 形态签名:state 参数
|
|
1364
|
+
* 供日志语境(record id),调用点(settled watchdog / keep-alive no-progress /
|
|
1365
|
+
* idle timer / agent_end 处置链)语义不变。
|
|
1297
1366
|
*
|
|
1298
1367
|
* @param source 升级来源标识(warn 日志定位用,如 "spawn watchdog" / "turn limiter")
|
|
1299
1368
|
*/
|
|
1300
1369
|
function killChildWithEscalation(state: SpawnRunState, child: ChildProcess, source: string): void {
|
|
1301
|
-
child.
|
|
1302
|
-
if (state.escalationTimer) clearTimeout(state.escalationTimer);
|
|
1303
|
-
assertSafeTimerDelay(SIGKILL_ESCALATION_MS, `SIGKILL escalation (${source})`);
|
|
1304
|
-
const escalation = setTimeout(
|
|
1305
|
-
() => {
|
|
1306
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
1307
|
-
logger.warn(
|
|
1308
|
-
`[session-runner] child ${state.record.id} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND}s after SIGTERM, escalating to SIGKILL (source: ${source})`,
|
|
1309
|
-
);
|
|
1310
|
-
child.kill("SIGKILL");
|
|
1311
|
-
}
|
|
1312
|
-
},
|
|
1313
|
-
SIGKILL_ESCALATION_MS,
|
|
1314
|
-
);
|
|
1315
|
-
escalation.unref();
|
|
1316
|
-
child.once("exit", () => clearTimeout(escalation));
|
|
1317
|
-
state.escalationTimer = escalation;
|
|
1370
|
+
killPiChild(child, state.record.id, source);
|
|
1318
1371
|
}
|
|
1319
1372
|
|
|
1320
1373
|
/**
|
|
@@ -1508,7 +1561,7 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
|
|
|
1508
1561
|
},
|
|
1509
1562
|
abort: () => {
|
|
1510
1563
|
// [race-F4] 升级路径:SIGTERM 后 30s 未 exit 则 SIGKILL(挂住子进程永不回收防线)
|
|
1511
|
-
if (state.proc)
|
|
1564
|
+
if (state.proc) killPiChild(state.proc, record.id, "turn limiter abort");
|
|
1512
1565
|
},
|
|
1513
1566
|
});
|
|
1514
1567
|
|
|
@@ -1538,61 +1591,65 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
|
|
|
1538
1591
|
}
|
|
1539
1592
|
};
|
|
1540
1593
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
// [SP-9] chatMode 每轮 reset turn-limiter:新一轮开始(续聊)时,
|
|
1584
|
-
// maxTurns/graceTurns 不跨轮累计(续聊本质是无限轮,累计上限违背 G1)。
|
|
1585
|
-
// reset steered/aborted 标志 + turnCount 归零,下一轮独立计数。
|
|
1586
|
-
limiter.reset();
|
|
1587
|
-
record.turnCount = 0;
|
|
1588
|
-
ctx.onRoundSettled?.(record);
|
|
1589
|
-
// [V2 决策 2] chatMode 首轮:agent_settled = 本轮真空闲,提前 resolve runSpawn
|
|
1590
|
-
//(exit code 0,进程仍保活 idle timer armed)。runAndFinalize 拿到 result 后走
|
|
1591
|
-
// chatMode 首轮分支(不进 finalize 分流),onRoundSettled 已 notify 主 agent。
|
|
1592
|
-
state.resolveRun?.(0);
|
|
1594
|
+
/**
|
|
1595
|
+
* [圈复杂度门禁提取] agent_settled × chatMode 分支整体自 handleSdkEvent 迁入
|
|
1596
|
+
* (只移动代码不改行为)。含 idle timer 挂载降级链 + 本轮完成通知 + 首轮提前 resolve。
|
|
1597
|
+
*/
|
|
1598
|
+
const handleAgentSettledForChatMode = (): void => {
|
|
1599
|
+
// [T2-③ / D9 两段式] settled 到达:本轮等待结束,两段守护(中段无进展 +
|
|
1600
|
+
// 收尾段上界)一并即清(resolveRun 在本分支同点调用,天然同清)。清除必须
|
|
1601
|
+
// 先于后续逻辑——idle timer / 回调 / resolve 抛错时 watchdog 已确保撤下,
|
|
1602
|
+
// 不会误杀下一个正常轮次。
|
|
1603
|
+
disarmSettledWatchdog(record.id);
|
|
1604
|
+
// [F-R2] 本闭包经 stdout data 回调同步调用(handleSdkEvent ← attachStdoutPump):
|
|
1605
|
+
// armIdleTimer → assertSafeTimerDelay fail-fast 的 throw 若逃出回调 = uncaughtException
|
|
1606
|
+
// 崩宿主。包 try/catch 降级,错误经 bestEffort("error") 可见但不升级为进程崩溃;
|
|
1607
|
+
// 后续 limiter.reset / onRoundSettled / resolveRun 照常执行(本轮完成通知不因 GC
|
|
1608
|
+
// timer 故障丢失)。
|
|
1609
|
+
// [T4② / PS-4] 降级语义修正:旧降级「不挂 idle timer」保住了「不崩进程」,却丢掉
|
|
1610
|
+
// timer 承载的两个下游不变量——isIdle 放行门(hasIdleTimer=false → 轮次完成通知被
|
|
1611
|
+
// lifecycle-predicates 吞)与进程回收(进程活着却无 timer 永久泄漏)。现降级改为
|
|
1612
|
+
// 「挂 DEFAULT_IDLE_TIMEOUT_MS + warn 留痕」:非配置替换(配置错误已在 spawn 入口
|
|
1613
|
+
// fail-fast,见 subagent-service),此处是防御性兜底,兜底必须可见且保住不变量。
|
|
1614
|
+
const armIdleTimerOnTimeout = (): void => {
|
|
1615
|
+
// onTimeout 复用现有 kill 路径:child.kill("SIGTERM") 触发 close → close handler
|
|
1616
|
+
// 统一 cleanup(spawnedChildren.delete / get_stateListeners.clear / resolve)。
|
|
1617
|
+
// 与 agent_end handler 现有 SIGTERM 分支一致,不新造 cleanup。
|
|
1618
|
+
// [race-F4] 升级:idle timer SIGTERM 后挂住 → 30s 后 SIGKILL。
|
|
1619
|
+
const child = getChildByRecord(record.id);
|
|
1620
|
+
if (child && !child.killed) killChildWithEscalation(state, child, "idle timer");
|
|
1621
|
+
};
|
|
1622
|
+
try {
|
|
1623
|
+
armIdleTimer(record.id, armIdleTimerOnTimeout, record.idleTimeoutMs);
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
bestEffort(err, "armIdleTimer (agent_settled chatMode)", "error");
|
|
1626
|
+
try {
|
|
1627
|
+
armIdleTimer(record.id, armIdleTimerOnTimeout, DEFAULT_IDLE_TIMEOUT_MS);
|
|
1628
|
+
logger.warn(
|
|
1629
|
+
`[session-runner] idleTimeoutMs invalid for ${record.id}, fell back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms) — idle GC and round notification gate stay active`,
|
|
1630
|
+
);
|
|
1631
|
+
} catch (fallbackErr) {
|
|
1632
|
+
// 双重失败(理论上不可达:DEFAULT 恒在安全域内)——退回旧「不挂」语义但留痕。
|
|
1633
|
+
bestEffort(fallbackErr, "armIdleTimer fallback (agent_settled chatMode)", "error");
|
|
1593
1634
|
}
|
|
1594
|
-
return;
|
|
1595
1635
|
}
|
|
1636
|
+
// [SP-9] chatMode 每轮 reset turn-limiter:新一轮开始(续聊)时,
|
|
1637
|
+
// maxTurns/graceTurns 不跨轮累计(续聊本质是无限轮,累计上限违背 G1)。
|
|
1638
|
+
// reset steered/aborted 标志 + turnCount 归零,下一轮独立计数。
|
|
1639
|
+
limiter.reset();
|
|
1640
|
+
record.turnCount = 0;
|
|
1641
|
+
ctx.onRoundSettled?.(record);
|
|
1642
|
+
// [V2 决策 2] chatMode 首轮:agent_settled = 本轮真空闲,提前 resolve runSpawn
|
|
1643
|
+
//(exit code 0,进程仍保活 idle timer armed)。runAndFinalize 拿到 result 后走
|
|
1644
|
+
// chatMode 首轮分支(不进 finalize 分流),onRoundSettled 已 notify 主 agent。
|
|
1645
|
+
state.resolveRun?.(0);
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* [圈复杂度门禁提取] SdkEvent.type switch 分派整体自 handleSdkEvent 迁入
|
|
1650
|
+
* (只移动代码不改行为)。tool_end 的 args 回填另有模块级 resolveToolEndArgs。
|
|
1651
|
+
*/
|
|
1652
|
+
const dispatchSdkEventByType = (raw: SdkEvent): void => {
|
|
1596
1653
|
switch (raw.type) {
|
|
1597
1654
|
case "tool_execution_start": {
|
|
1598
1655
|
const toolName = raw.toolName ?? "";
|
|
@@ -1604,15 +1661,13 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
|
|
|
1604
1661
|
}
|
|
1605
1662
|
case "tool_execution_end": {
|
|
1606
1663
|
const toolName = raw.toolName ?? "";
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1615
|
-
agentEvent({ type: "tool_end", toolName, args, result: raw.result, isError: raw.isError });
|
|
1664
|
+
agentEvent({
|
|
1665
|
+
type: "tool_end",
|
|
1666
|
+
toolName,
|
|
1667
|
+
args: resolveToolEndArgs(raw, pendingTools),
|
|
1668
|
+
result: raw.result,
|
|
1669
|
+
isError: raw.isError,
|
|
1670
|
+
});
|
|
1616
1671
|
return;
|
|
1617
1672
|
}
|
|
1618
1673
|
case "message_update": {
|
|
@@ -1641,6 +1696,21 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
|
|
|
1641
1696
|
}
|
|
1642
1697
|
};
|
|
1643
1698
|
|
|
1699
|
+
const handleSdkEvent = (raw: SdkEvent): void => {
|
|
1700
|
+
// [T2-③ / D9 两段式] 有效协议事件行刷新中段无进展计时(本函数是 stdout pump
|
|
1701
|
+
// 解析出的合法 SdkEvent 的唯一喂入口——LC-9 invalid 行不进入,天然不刷新)。
|
|
1702
|
+
// 未挂载(非 chatMode / env 关闭)或已交棒(收尾段不刷新)时幂等 no-op。
|
|
1703
|
+
refreshMidRoundNoProgress(record.id);
|
|
1704
|
+
// [V2 模块 3] agent_settled:真空闲边界(agent_end 之后、post-run 完成后才 emit)。
|
|
1705
|
+
// chatMode:arm idle timer(超时 SIGTERM 回收)+ 通知本轮完成(onRoundSettled)。
|
|
1706
|
+
// 非 chatMode:忽略(agent_end handler 的一次性 kill 已处理,进程不会活到 agent_settled)。
|
|
1707
|
+
if (isAgentSettledEvt(raw)) {
|
|
1708
|
+
if (record.chatMode) handleAgentSettledForChatMode();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
dispatchSdkEventByType(raw);
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1644
1714
|
return handleSdkEvent;
|
|
1645
1715
|
}
|
|
1646
1716
|
|
|
@@ -2069,6 +2139,106 @@ function attachStdoutPump(
|
|
|
2069
2139
|
settleHandshakeNow();
|
|
2070
2140
|
};
|
|
2071
2141
|
|
|
2142
|
+
/**
|
|
2143
|
+
* [圈复杂度门禁提取] header 行处理(自 data handler 的 kind 分支整体迁入,
|
|
2144
|
+
* 只移动代码不改行为):sessionFile 推导回填 + alive marker + worktree pid 补全
|
|
2145
|
+
* + header 加速路径握手。
|
|
2146
|
+
*/
|
|
2147
|
+
const handleHeaderLine = (parsed: Extract<ParsedSpawnLine, { kind: "header" }>): void => {
|
|
2148
|
+
state.sessionHeader = parsed.header;
|
|
2149
|
+
// 回填 record.sessionFile(deriveSessionFilePath 推导路径)
|
|
2150
|
+
record.sessionFile = deriveSessionFilePath(parsed.header, sessionDir);
|
|
2151
|
+
// [持久化 C] alive marker:running 期间崩溃恢复用。子进程 pid + session id。
|
|
2152
|
+
// 与 in-process 逻辑对齐(记 sessionFile + pid),改为子进程 pid。
|
|
2153
|
+
if (record.sessionFile && child.pid) {
|
|
2154
|
+
writeAliveMarkerBestEffort(record.sessionFile, child.pid, parsed.header.id);
|
|
2155
|
+
}
|
|
2156
|
+
// [全局注册表] worktree 模式:补全注册表条目的 pid。
|
|
2157
|
+
// create 时 pid 未知写 0 占位,此处拿到 child.pid 后回调 WorktreeManager.registerPid。
|
|
2158
|
+
// 取代旧的 .session mapping sidecar——注册表是 reaper 的唯一数据源。
|
|
2159
|
+
// [D5a] fire-and-forget:回调内部走跨进程锁(毫秒级),且实现方保证不
|
|
2160
|
+
// reject(锁降级兜底);stdout data 回调是同步上下文,不 await。
|
|
2161
|
+
if (opts.worktree && child.pid) {
|
|
2162
|
+
// 透传 record.sessionFile:填入 registry entry(reaper 据 pid 死活判孤儿),
|
|
2163
|
+
// first header 时 sessionFile 已回填(deriveSessionFilePath 在本分支上方)。
|
|
2164
|
+
try {
|
|
2165
|
+
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid, record.sessionFile);
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
// 同步段异常(回调本身 throw)不阻断 stdout 解析;锁内错误由回调内部 warn。
|
|
2168
|
+
bestEffort(err, "onWorktreePid callback (first header)");
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
// FR-4 加速路径:header 到达即 finishHandshake(header 已提供 sessionId,
|
|
2172
|
+
// 足以推导 sessionFile + 兜底查找,无需等 get_state response)。
|
|
2173
|
+
// [#25] buildSpawnArgs 固定 --mode rpc,RPC mode 不发 header——此分支当前不触发,
|
|
2174
|
+
// 仅为未来 mode 回切(如 json mode 调试)保留:届时 header 先到可省去 get_state 握手等待。
|
|
2175
|
+
if (settleHandshake) {
|
|
2176
|
+
finishHandshake({
|
|
2177
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
2178
|
+
sessionId: parsed.header.id,
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* [圈复杂度门禁提取] agent_end 路由(自 data handler 的 event 分支迁入,只移动
|
|
2185
|
+
* 代码不改行为)。返回 true = chatMode 交棒形态——调用方据此跳过本行的
|
|
2186
|
+
* handleSdkEvent 喂入。原实现用 continue(跳过当前行剩余、继续 for(line) 后续行,
|
|
2187
|
+
* 不跳出整个 flush),提取后 return 于 for 循环体内同构。
|
|
2188
|
+
*/
|
|
2189
|
+
const routeAgentEnd = (evt: { type: "agent_end"; willRetry?: boolean }): boolean => {
|
|
2190
|
+
if (evt.willRetry) {
|
|
2191
|
+
// agent 会重试,不能 kill。
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
if (record.chatMode) {
|
|
2195
|
+
// [V2 决策 1] chatMode:对话模式进程不因轮次死。agent_end 不 kill、不 MF-3/MF-4。
|
|
2196
|
+
// 等待 agent_settled(真空闲信号)arm idle timer + notify(onRoundSettled)。
|
|
2197
|
+
// [T2-③ / D9 两段式] agent_end 到达 = 工作段结束:中段无进展计时让位收尾段
|
|
2198
|
+
// 固定上界(清中段、已过时间不继承,从 agent_end 起独立计时 600s;未挂载 /
|
|
2199
|
+
// 已交棒幂等 no-op——首轮与热路径轮共用本 pump 接线点)。
|
|
2200
|
+
handoverMidRoundToSettled(record.id);
|
|
2201
|
+
return true;
|
|
2202
|
+
}
|
|
2203
|
+
// [recursive-orchestration] 条件 kill:读子进程 session 文件算活跃后代
|
|
2204
|
+
// (pending:register − unregister 差集)。有活跃后代(background subagent /
|
|
2205
|
+
// workflow)→ 保持进程 idle,等后代完成时 notifier triggerTurn steer 唤醒;
|
|
2206
|
+
// 无 → 正常完成,kill 触发 close → runSpawn resolve。
|
|
2207
|
+
//
|
|
2208
|
+
// [T1/RC-1] 处置决策异步化(fire-and-forget):sessionFile 缺失(RC-1 握手失败
|
|
2209
|
+
// 形态)时现场惰性 get_state 回补后再判定,见 runAgentEndDisposition。原同步
|
|
2210
|
+
// 三分支逐行迁入该函数;异步化后决策最晚 1s(回补超时预算)落地,期间子进程
|
|
2211
|
+
// 仍在原 watchdog 保护下,kill 延迟无语义影响。
|
|
2212
|
+
void runAgentEndDisposition(state, child, sessionDir, registerGetStateListener);
|
|
2213
|
+
return false;
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* [圈复杂度门禁提取] 事件行处理(自 data handler 的 event 分支迁入):
|
|
2218
|
+
* agent_end 路由(keep-alive / chatMode 交棒 / 条件 kill)+ 其余事件喂 handleSdkEvent。
|
|
2219
|
+
*/
|
|
2220
|
+
const handleEventLine = (parsed: Extract<ParsedSpawnLine, { kind: "event" }>): void => {
|
|
2221
|
+
// agent_end(willRetry=false)= agent 自然完成。rpc mode 子进程不自动退出
|
|
2222
|
+
//(runRpcMode 末尾 return new Promise(() => {}) 长驻等命令),需主动 kill
|
|
2223
|
+
// 触发 close → runSpawn resolve。willRetry=true 时 agent 会重试,不能 kill。
|
|
2224
|
+
if (isAgentEndEvt(parsed.event) && routeAgentEnd(parsed.event)) return;
|
|
2225
|
+
if (isSdkEvent(parsed.event)) handleSdkEvent(parsed.event);
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* [圈复杂度门禁提取] RPC response 行处理(自 data handler 的 response 分支迁入):
|
|
2230
|
+
* FR-4 get_state response 按 id 匹配 resolver。
|
|
2231
|
+
*/
|
|
2232
|
+
const handleResponseLine = (parsed: Extract<ParsedSpawnLine, { kind: "response" }>): void => {
|
|
2233
|
+
if (parsed.command === "get_state" && parsed.success && parsed.id) {
|
|
2234
|
+
const resolver = get_stateListeners.get(parsed.id);
|
|
2235
|
+
if (resolver) {
|
|
2236
|
+
get_stateListeners.delete(parsed.id);
|
|
2237
|
+
resolver(parsed.data);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2072
2242
|
child.stdout.on("data", (data: string) => {
|
|
2073
2243
|
// [T2-① / P-T2 降级 B] 子进程有输出 = keep-alive 仍有进展迹象:刷新静默计时。
|
|
2074
2244
|
// 任何 stdout 活动(header / 事件行 / invalid 调试行)都算——keep-alive 的合法性
|
|
@@ -2081,81 +2251,18 @@ function attachStdoutPump(
|
|
|
2081
2251
|
stdoutBuffer += data;
|
|
2082
2252
|
const lines = stdoutBuffer.split("\n");
|
|
2083
2253
|
stdoutBuffer = lines.pop() ?? ""; // 保留最后未完整行
|
|
2254
|
+
// [圈复杂度门禁提取] 各 kind 分支体整体迁入上方 handleHeaderLine /
|
|
2255
|
+
// handleEventLine / handleResponseLine(行为逐字节保持),本 handler 只留
|
|
2256
|
+
// 缓冲切分 + kind 分派。
|
|
2084
2257
|
for (const line of lines) {
|
|
2085
2258
|
const parsed = parseSpawnLine(line);
|
|
2086
2259
|
if (!parsed) continue;
|
|
2087
2260
|
if (parsed.kind === "header") {
|
|
2088
|
-
|
|
2089
|
-
// 回填 record.sessionFile(deriveSessionFilePath 推导路径)
|
|
2090
|
-
record.sessionFile = deriveSessionFilePath(parsed.header, sessionDir);
|
|
2091
|
-
// [持久化 C] alive marker:running 期间崩溃恢复用。子进程 pid + session id。
|
|
2092
|
-
// 与 in-process 逻辑对齐(记 sessionFile + pid),改为子进程 pid。
|
|
2093
|
-
if (record.sessionFile && child.pid) {
|
|
2094
|
-
writeAliveMarkerBestEffort(record.sessionFile, child.pid, parsed.header.id);
|
|
2095
|
-
}
|
|
2096
|
-
// [全局注册表] worktree 模式:补全注册表条目的 pid。
|
|
2097
|
-
// create 时 pid 未知写 0 占位,此处拿到 child.pid 后回调 WorktreeManager.registerPid。
|
|
2098
|
-
// 取代旧的 .session mapping sidecar——注册表是 reaper 的唯一数据源。
|
|
2099
|
-
// [D5a] fire-and-forget:回调内部走跨进程锁(毫秒级),且实现方保证不
|
|
2100
|
-
// reject(锁降级兜底);stdout data 回调是同步上下文,不 await。
|
|
2101
|
-
if (opts.worktree && child.pid) {
|
|
2102
|
-
// 透传 record.sessionFile:填入 registry entry(reaper 据 pid 死活判孤儿),
|
|
2103
|
-
// first header 时 sessionFile 已回填(deriveSessionFilePath 在本分支上方)。
|
|
2104
|
-
try {
|
|
2105
|
-
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid, record.sessionFile);
|
|
2106
|
-
} catch (err) {
|
|
2107
|
-
// 同步段异常(回调本身 throw)不阻断 stdout 解析;锁内错误由回调内部 warn。
|
|
2108
|
-
bestEffort(err, "onWorktreePid callback (first header)");
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
// FR-4 加速路径:header 到达即 finishHandshake(header 已提供 sessionId,
|
|
2112
|
-
// 足以推导 sessionFile + 兜底查找,无需等 get_state response)。
|
|
2113
|
-
// [#25] buildSpawnArgs 固定 --mode rpc,RPC mode 不发 header——此分支当前不触发,
|
|
2114
|
-
// 仅为未来 mode 回切(如 json mode 调试)保留:届时 header 先到可省去 get_state 握手等待。
|
|
2115
|
-
if (settleHandshake) {
|
|
2116
|
-
finishHandshake({
|
|
2117
|
-
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
2118
|
-
sessionId: parsed.header.id,
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2261
|
+
handleHeaderLine(parsed);
|
|
2121
2262
|
} else if (parsed.kind === "event") {
|
|
2122
|
-
|
|
2123
|
-
// agent_end(willRetry=false)= agent 自然完成。rpc mode 子进程不自动退出
|
|
2124
|
-
//(runRpcMode 末尾 return new Promise(() => {}) 长驻等命令),需主动 kill
|
|
2125
|
-
// 触发 close → runSpawn resolve。willRetry=true 时 agent 会重试,不能 kill。
|
|
2126
|
-
if (isAgentEndEvt(evt)) {
|
|
2127
|
-
if (evt.willRetry) {
|
|
2128
|
-
// agent 会重试,不能 kill。
|
|
2129
|
-
} else if (record.chatMode) {
|
|
2130
|
-
// [V2 决策 1] chatMode:对话模式进程不因轮次死。agent_end 不 kill、不 MF-3/MF-4。
|
|
2131
|
-
// 等待 agent_settled(真空闲信号)arm idle timer + notify(onRoundSettled)。
|
|
2132
|
-
// 用 continue 而非 return:return 会跳出 stdout data handler 的 for(line) 循环,
|
|
2133
|
-
// 丢弃同一 flush 内 agent_end 之后的事件(如紧随的 agent_settled)。continue 只
|
|
2134
|
-
// 跳过当前行剩余(handleSdkEvent 对 agent_end 是 no-op),继续处理后续行。
|
|
2135
|
-
continue;
|
|
2136
|
-
} else {
|
|
2137
|
-
// [recursive-orchestration] 条件 kill:读子进程 session 文件算活跃后代
|
|
2138
|
-
// (pending:register − unregister 差集)。有活跃后代(background subagent /
|
|
2139
|
-
// workflow)→ 保持进程 idle,等后代完成时 notifier triggerTurn steer 唤醒;
|
|
2140
|
-
// 无 → 正常完成,kill 触发 close → runSpawn resolve。
|
|
2141
|
-
//
|
|
2142
|
-
// [T1/RC-1] 处置决策异步化(fire-and-forget):sessionFile 缺失(RC-1 握手失败
|
|
2143
|
-
// 形态)时现场惰性 get_state 回补后再判定,见 runAgentEndDisposition。原同步
|
|
2144
|
-
// 三分支逐行迁入该函数;异步化后决策最晚 1s(回补超时预算)落地,期间子进程
|
|
2145
|
-
// 仍在原 watchdog 保护下,kill 延迟无语义影响。
|
|
2146
|
-
void runAgentEndDisposition(state, child, sessionDir, registerGetStateListener);
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
if (isSdkEvent(parsed.event)) handleSdkEvent(parsed.event);
|
|
2263
|
+
handleEventLine(parsed);
|
|
2150
2264
|
} else if (parsed.kind === "response") {
|
|
2151
|
-
|
|
2152
|
-
if (parsed.command === "get_state" && parsed.success && parsed.id) {
|
|
2153
|
-
const resolver = get_stateListeners.get(parsed.id);
|
|
2154
|
-
if (resolver) {
|
|
2155
|
-
get_stateListeners.delete(parsed.id);
|
|
2156
|
-
resolver(parsed.data);
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2265
|
+
handleResponseLine(parsed);
|
|
2159
2266
|
} else if (parsed.kind === "extension_ui_request") {
|
|
2160
2267
|
// W3: 子进程发 UI 请求(ask_user)。入队 FIFO 串行处理,防止并发询问用户。
|
|
2161
2268
|
enqueueUiRequest(parsed.id, parsed.request);
|
|
@@ -2274,6 +2381,298 @@ function waitForChildExit(
|
|
|
2274
2381
|
});
|
|
2275
2382
|
}
|
|
2276
2383
|
|
|
2384
|
+
/**
|
|
2385
|
+
* [圈复杂度门禁提取] spawn 后同步装配阶段(自 runSpawn 迁入,只移动代码不改行为):
|
|
2386
|
+
* worktree pid 同步补全 + spawnedChildren 记账 + record.pid + stdio 编码 +
|
|
2387
|
+
* stdin 异步 error listener + 首条 prompt 命令。顺序敏感(见各段注释),整段迁移。
|
|
2388
|
+
*/
|
|
2389
|
+
function setupFreshChild(
|
|
2390
|
+
state: SpawnRunState,
|
|
2391
|
+
child: ChildProcessWithoutNullStreams,
|
|
2392
|
+
task: string,
|
|
2393
|
+
): void {
|
|
2394
|
+
const { record, opts, ctx } = state;
|
|
2395
|
+
// [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
|
|
2396
|
+
// 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
|
|
2397
|
+
// 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
|
|
2398
|
+
// SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
|
|
2399
|
+
// header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
|
|
2400
|
+
if (opts.worktree && child.pid) {
|
|
2401
|
+
// [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
|
|
2402
|
+
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
|
|
2403
|
+
}
|
|
2404
|
+
// [C1] track 子进程供 dispose 兜底 kill(sync + background 均注册——sync 无 controller,
|
|
2405
|
+
// abortRunningControllers 跳过它,靠本 Map 兜底)。close/error 后按句守卫移除(已退出无需再 kill)。
|
|
2406
|
+
spawnedChildren.set(record.id, child);
|
|
2407
|
+
// [V2 决策 3] spawn 后 child.pid 同步立即可得,记录到 record 内存(lifecycle-manager
|
|
2408
|
+
// 孤儿扫描用,Step 5 接入持久化)。resume spawn 时此处同样覆盖更新(pid 可能已变)。
|
|
2409
|
+
if (child.pid !== undefined) record.pid = child.pid;
|
|
2410
|
+
|
|
2411
|
+
// [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
|
|
2412
|
+
// 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
|
|
2413
|
+
// 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
|
|
2414
|
+
// SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
|
|
2415
|
+
// header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
|
|
2416
|
+
// [S1] 防御:必须放在 spawnedChildren.add 之后(onWorktreePid 抛错时子进程已被跟踪,
|
|
2417
|
+
// dispose 兜底 kill 不会泄漏),且包 try/catch(补全失败不阻断 spawn 主流程——
|
|
2418
|
+
// 注册表写失败最坏后果是条目停留 pid=0,由 reaper 宽限回收兜底)。
|
|
2419
|
+
if (opts.worktree && child.pid) {
|
|
2420
|
+
try {
|
|
2421
|
+
// [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
|
|
2422
|
+
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
logger.warn("[worktree] worktree pid registration failed (defensive)", {
|
|
2425
|
+
branch: opts.worktree.branch,
|
|
2426
|
+
pid: child.pid,
|
|
2427
|
+
err: toErrorMessage(err),
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// stdout/stderr 用 utf8 编码:stream 自动按字符边界切分,避免多字节
|
|
2433
|
+
// UTF-8(CJK/emoji)跨 chunk 时 toString() 产生 U+FFFD 替换符导致 JSON.parse 失败。
|
|
2434
|
+
// [m2] 先 setEncoding 再注册 signal listener/watchdog:若 setEncoding 抛错,try/finally
|
|
2435
|
+
//(下方)只清理 tempPromptFile,watchdog/signal listener 尚未注册则无需清理——避免泄漏。
|
|
2436
|
+
child.stdout.setEncoding("utf8");
|
|
2437
|
+
child.stderr.setEncoding("utf8");
|
|
2438
|
+
|
|
2439
|
+
// [v4 A-1] 异步 stdin 'error' listener。writeStdinLine 的 try/catch 只覆盖同步 write 抛错;
|
|
2440
|
+
// 子进程退出后内核回写 EPIPE 会以异步 stream 'error' event 到达,若无 listener 会让 Node
|
|
2441
|
+
// 抛 unhandled 'error' 崩主进程(P1)。必须在首次 stdin 写入(下方 sendPromptCommand)前注册。
|
|
2442
|
+
// handler 行为:①移出 spawnedChildren 标记 dead(与 child.on('error') 同模式);
|
|
2443
|
+
// ②recordEpipeFailure 合并同步/异步计数;③logger.warn 记录一次。
|
|
2444
|
+
// [v4 A-1 裁决] handler 内**不 throw**——stream 'error' listener 内 throw 会经 Node 内部
|
|
2445
|
+
// emit() 传播为 uncaughtException 崩主进程,违背 A-1 防崩核心目标。达阈值的 throw 留给
|
|
2446
|
+
// 同步路径(PiEngine.deliverPrompt 的 catch 合并计数达 EPIPE_FAILURE_THRESHOLD 时同步 throw,不崩)。
|
|
2447
|
+
// async handler 只移句柄 + 计数 + warn;进程已 dead(移句柄),下次投递检测
|
|
2448
|
+
// dead 走冷路径,冷路径 write EPIPE 同步计数达阈值同步 throw——防死循环且不崩。
|
|
2449
|
+
child.stdin.on("error", (err: Error) => {
|
|
2450
|
+
// [M4] 按值守卫:resume spawn 已覆盖注册时不误删新 child(见 removeChildRegistration)
|
|
2451
|
+
removeChildRegistration(record.id, child);
|
|
2452
|
+
const count = recordEpipeFailure(record.id);
|
|
2453
|
+
logger.warn(`[subagents] async stdin error for ${record.id}`, {
|
|
2454
|
+
detail: err.message,
|
|
2455
|
+
epipeCount: count,
|
|
2456
|
+
threshold: EPIPE_FAILURE_THRESHOLD,
|
|
2457
|
+
hint:
|
|
2458
|
+
count >= EPIPE_FAILURE_THRESHOLD
|
|
2459
|
+
? "sync path will throw on next write EPIPE"
|
|
2460
|
+
: undefined,
|
|
2461
|
+
});
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
// 喂 prompt 命令驱动子进程开始处理 task。pi runRpcMode 只消费 stdin RpcCommand,
|
|
2465
|
+
// 不读 positional arg;必须在 spawn 后主动写,否则子进程阻塞、totalTokens 恒 0。
|
|
2466
|
+
// 时机安全:pipe 内核缓冲不丢;pi 在 rebindSession 后才挂 stdin reader。
|
|
2467
|
+
sendPromptCommand(child, task);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* [圈复杂度门禁提取] chatMode 首轮 settled 等待守护挂载(自 runSpawn 迁入)。
|
|
2472
|
+
*/
|
|
2473
|
+
function armChatModeSettledWatchdog(state: SpawnRunState, child: ChildProcess): void {
|
|
2474
|
+
const { record } = state;
|
|
2475
|
+
// [T2-③ / D9 两段式] chatMode 首轮 settled 等待守护(双挂载原语之首轮调用点;
|
|
2476
|
+
// 热路径调用点在 subagent-service deliverMessage)。prompt 发出后挂**中段**无进展
|
|
2477
|
+
// 检测(有效协议事件行刷新,连续静默 30min 判 wedged);agent_end 到达时经 stdout
|
|
2478
|
+
// pump 交棒**收尾段**固定上界(agent_end 后 600s,P-T2c 定案);settled 到达
|
|
2479
|
+
//(handleSdkEvent)/ close(waitForChildExit)/ resolveRun 任一发生即清——两段
|
|
2480
|
+
// 合起来覆盖 LC-1 三 wedged 形态(agent_end 永不到达 / 收尾卡死 / settled 行丢失)。
|
|
2481
|
+
// 到期处置(两段同构):kill 层主 + settledWatchdogFired 段信息(收尾据此转 failed
|
|
2482
|
+
// + 恢复指引,见 resolveRunOutcome),与被信号终止视为正常完成的既有语义区分。
|
|
2483
|
+
if (record.chatMode) {
|
|
2484
|
+
armMidRoundNoProgress(record.id, {
|
|
2485
|
+
onMidTimeout: (fire) => {
|
|
2486
|
+
logger.warn(
|
|
2487
|
+
`[session-runner] settled watchdog (mid-round) fired for ${record.id}: no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min, terminating (LC-1 wedge recovery)`,
|
|
2488
|
+
);
|
|
2489
|
+
state.settledWatchdogFired = fire;
|
|
2490
|
+
killChildWithEscalation(state, child, "settled watchdog");
|
|
2491
|
+
},
|
|
2492
|
+
onSettleTimeout: (fire) => {
|
|
2493
|
+
logger.warn(
|
|
2494
|
+
`[session-runner] settled watchdog (settled phase) fired for ${record.id}: no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end, terminating (LC-1 wedge recovery)`,
|
|
2495
|
+
);
|
|
2496
|
+
state.settledWatchdogFired = fire;
|
|
2497
|
+
killChildWithEscalation(state, child, "settled watchdog");
|
|
2498
|
+
},
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* [圈复杂度门禁提取] abort 监听 + spawn watchdog 挂载(自 runSpawn 迁入,只移动
|
|
2505
|
+
* 代码不改行为)。返回 onAbort 供收尾 removeEventListener(注册与注销同一句柄)。
|
|
2506
|
+
*/
|
|
2507
|
+
function attachAbortAndSpawnWatchdog(state: SpawnRunState, child: ChildProcess): () => void {
|
|
2508
|
+
const { record, opts } = state;
|
|
2509
|
+
// d. signal → proc.kill 监听(一次性,替代 session.abort)
|
|
2510
|
+
// [race-F4] 用户取消同样升级:SIGTERM 后挂住 → 30s 后 SIGKILL 兑现取消语义。
|
|
2511
|
+
const onAbort = (): void => {
|
|
2512
|
+
killPiChild(child, record.id, "abort signal");
|
|
2513
|
+
};
|
|
2514
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2515
|
+
// 前置检查:signal 在 spawn 前已 aborted 时 addEventListener 不会触发 onAbort,
|
|
2516
|
+
// 子进程会跑到自然结束。立即 kill 兑现取消语义。
|
|
2517
|
+
if (opts.signal?.aborted) onAbort();
|
|
2518
|
+
|
|
2519
|
+
// e. watchdog:子进程整体超时兜底。卡死在单 tool 内(turn_end 永不触发)时
|
|
2520
|
+
// limiter 失效,此 timer 保证最终 SIGTERM,防止 background 槽位/资源泄漏。
|
|
2521
|
+
// [M-1] timeout 基于 maxTurns 动态计算(maxTurnsToWatchdogMs):旧实现固定 30 分钟
|
|
2522
|
+
// 误杀长任务,现按 maxTurns 线性估算(每 turn ~5 分钟,下限 30 分钟)。
|
|
2523
|
+
// [预算语义对齐] maxTurns 未传/<=0 → 不挂 watchdog(不限,用户明确裁决——旧实现按
|
|
2524
|
+
// 10 turns 估出 50min 默认 SIGTERM 且 maxTurns:0 也关不掉,已废);
|
|
2525
|
+
// SPAWN_WATCHDOG_ENV 设置时按绝对时限兑底挂载(hang 泄漏防线 opt-in)。
|
|
2526
|
+
// [R0] unref:不阻止 Node 进程退出。安全性由 SubagentService.dispose 保证——
|
|
2527
|
+
// 主进程退出时(session_shutdown reason=quit)dispose 会 abort running controller
|
|
2528
|
+
// → 本监听器 kill 子进程。无此 unref,watchdog timer 会拖住 event loop 阻止退出。
|
|
2529
|
+
const watchdogMs = resolveSpawnWatchdogMs(opts.maxTurns);
|
|
2530
|
+
if (watchdogMs !== undefined) {
|
|
2531
|
+
state.watchdog = setTimeout(
|
|
2532
|
+
() => killPiChild(child, record.id, "spawn watchdog"),
|
|
2533
|
+
watchdogMs,
|
|
2534
|
+
);
|
|
2535
|
+
state.watchdog.unref();
|
|
2536
|
+
}
|
|
2537
|
+
return onAbort;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/**
|
|
2541
|
+
* [圈复杂度门禁提取] get_state RPC 握手启动(自 runSpawn 迁入,fire-and-forget)。
|
|
2542
|
+
*/
|
|
2543
|
+
function startGetStateHandshake(
|
|
2544
|
+
child: ChildProcessWithoutNullStreams,
|
|
2545
|
+
pump: StdoutPumpHandles,
|
|
2546
|
+
): void {
|
|
2547
|
+
// FR-4: get_state RPC 握手——spawn 后无条件启动。
|
|
2548
|
+
// RPC mode(pi --mode rpc)不向 stdout 输出 header,record.sessionFile 无法靠 header
|
|
2549
|
+
// 推导,必须通过 get_state RPC 查询子进程回填。json mode 下 header 会先到达触发提前
|
|
2550
|
+
// resolve(加速路径),握手仍启动但无害——response 到达时外层已 resolve,resolver
|
|
2551
|
+
// 因 resolved=true 直接 return。
|
|
2552
|
+
//
|
|
2553
|
+
// 时序:握手在 stdout pump(get_stateListeners 已就绪)之后启动。get_state 命令写入
|
|
2554
|
+
// stdin,pi rebindSession 后读取并返回 response,经 stdout pump 匹配 resolver 触发 resolve。
|
|
2555
|
+
// close handler await handshakeSettled,保证无论 task 多快结束,close 时 sessionFile 已回填。
|
|
2556
|
+
// [#18] 握手状态变量已在 attachStdoutPump 内部(stdout handler 注册前)定义,此处直接发起握手。
|
|
2557
|
+
// [F2] .catch 兜底:performGetStateHandshake 的 Promise executor 同步调 tryOnce →
|
|
2558
|
+
// sendGetStateCommand → writeStdinLine 在 stdin 已断(EPIPE/ERR_STREAM_DESTROYED)时同步
|
|
2559
|
+
// throw → executor 内同步异常被 Promise 构造器转为 reject。若无人接(旧实现只有 .then),
|
|
2560
|
+
// reject 无人消费 → unhandledRejection(Node 15+ 默认 mode=throw)可崩父 pi 进程。
|
|
2561
|
+
// catch 内:logger.error 留证 + pump.abandonHandshake 记录握手失败终态——close handler
|
|
2562
|
+
// await 的 handshakeSettled 立即 settle,isHandshakePending() 归 false,不阻塞收尾链路
|
|
2563
|
+
//(sessionFile 兜底由收尾时的 existsSync 校验 + findSessionFileByHeaderId 承担)。
|
|
2564
|
+
void performGetStateHandshake(child, pump.registerGetStateListener).then((r) => {
|
|
2565
|
+
// header 加速路径下 settleHandshake 已 undefined,跳过(避免覆盖 header 结果)。
|
|
2566
|
+
// 超时兜底(r 为空对象)也经此分支 settle,但 record.sessionFile 不回填。
|
|
2567
|
+
if (pump.isHandshakePending()) pump.finishHandshake(r);
|
|
2568
|
+
}).catch((err: unknown) => {
|
|
2569
|
+
const m = toErrorMessage(err);
|
|
2570
|
+
logger.error(`[session-runner] get_state handshake failed: ${m}`);
|
|
2571
|
+
pump.abandonHandshake();
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/**
|
|
2576
|
+
* [圈复杂度门禁提取] sessionFile 兜底反查(LC-4,自 runSpawn 收尾迁入)。
|
|
2577
|
+
*/
|
|
2578
|
+
function backfillSessionFileByLookup(state: SpawnRunState, sessionDir: string): void {
|
|
2579
|
+
const { record } = state;
|
|
2580
|
+
// [持久化 A] sessionFile 兜底校验。
|
|
2581
|
+
// identity custom entry 已改由子进程 session_start hook 写(M4 / V2 决策 5),
|
|
2582
|
+
// 父进程不再 fs 补写——fs 补写的 entry 缺 id/parentId 污染 pi _buildIndex。
|
|
2583
|
+
// 此处仅保留 sessionFile 路径兜底(deriveSessionFilePath/握手路径可能不准)。
|
|
2584
|
+
//
|
|
2585
|
+
// [T1/LC-4] findSessionFileByHeaderId 兜底查找移出 `if (record.sessionFile)` 守卫:
|
|
2586
|
+
// 原守卫条件恰等于它要兜底的缺失本身——RC-1 握手失败留下的「sessionId 有、
|
|
2587
|
+
// sessionFile 无」形态(两字段独立采集)永远不可达本兜底,下游 finalize marker /
|
|
2588
|
+
// alive marker / identity 写入全部失去依据(PS-9 放大)。现在两种形态都反查:
|
|
2589
|
+
// sessionFile 有但路径不存在(推导错 / pi 命名变化);sessionFile 无而 sessionId 有
|
|
2590
|
+
// (header 或握手部分成功,含 agent_end 惰性回补补入的 handshakeResult.sessionId)。
|
|
2591
|
+
const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
|
|
2592
|
+
const needsLookup = record.sessionFile
|
|
2593
|
+
? !fs.existsSync(record.sessionFile)
|
|
2594
|
+
: lookupId !== undefined;
|
|
2595
|
+
if (lookupId && needsLookup) {
|
|
2596
|
+
const actual = findSessionFileByHeaderId(sessionDir, lookupId);
|
|
2597
|
+
if (actual && actual !== record.sessionFile) record.sessionFile = actual;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* [圈复杂度门禁提取] keep-alive 上界处置的两步时序后半(T2-② 后代级联补杀,
|
|
2603
|
+
* 自 runSpawn 收尾迁入):close 确认死亡 + sessionFile 冻结为最终快照后 sweep。
|
|
2604
|
+
*/
|
|
2605
|
+
function sweepDescendantsOnChildClose(state: SpawnRunState, sessionDir: string): void {
|
|
2606
|
+
const { record } = state;
|
|
2607
|
+
// [T2-② / P-T2b 主路径] keep-alive 上界处置层主的两步时序后半:此刻 close 已发生
|
|
2608
|
+
//(waitForChildExit 已 resolve = 层主确认死亡),且 sessionFile 已完成 LC-4 反查
|
|
2609
|
+
// = 冻结为最终快照(pending entries 最完整,避开「kill 前采集」的垂死窗口漏项)。
|
|
2610
|
+
// 从层主 sessionFile 差集采集活跃后代清单,对清单内每个后代迭代展开至叶(递归读
|
|
2611
|
+
// 各后代的 pending 差集),kill 前做存活 + cmdline(pi/--mode rpc)校验防 pid 复用
|
|
2612
|
+
// 误杀,逐个 escalation kill(SIGTERM→SIGKILL)。同步执行:后代树规模有限 + 每
|
|
2613
|
+
// 步有界(ps 探测 3s 超时),不阻塞 runSpawn 收尾的可感时长。
|
|
2614
|
+
if (state.sweepDescendantsOnClose) {
|
|
2615
|
+
try {
|
|
2616
|
+
const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
|
|
2617
|
+
if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
|
|
2618
|
+
logger.warn(
|
|
2619
|
+
`[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`,
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
// sweep 整体失败不掩盖层主自身的收尾结果(best-effort 可见)
|
|
2624
|
+
bestEffort(err, "descendant sweep (keep-alive watchdog)", "error");
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* [圈复杂度门禁提取] 成功/失败判定(三来源:exitCode + record.lastError + abort
|
|
2631
|
+
* 原因;自 runSpawn 迁入,四分支行为逐字节保持)。
|
|
2632
|
+
*/
|
|
2633
|
+
function resolveRunOutcome(
|
|
2634
|
+
state: SpawnRunState,
|
|
2635
|
+
exitCode: number,
|
|
2636
|
+
stderrBuffer: string,
|
|
2637
|
+
): { success: boolean; error: string | undefined } {
|
|
2638
|
+
const { record, opts } = state;
|
|
2639
|
+
let success: boolean;
|
|
2640
|
+
let error: string | undefined;
|
|
2641
|
+
if (state.settledWatchdogFired) {
|
|
2642
|
+
// [T2-③ / D9] settled watchdog 到期回收 ≠ 正常完成:被信号终止视为正常完成的
|
|
2643
|
+
// 既有语义(maxTurns 达限 kill)不适用于本形态——runSpawn 以错误返回(设计
|
|
2644
|
+
// §6.2),错误消息含恢复指引(S-B 验收判据)。closedReason 的 "watchdog" 映射
|
|
2645
|
+
// 由 finalize 侧按 error 标记承接(ClosedReason 枚举封闭,不在此擅自扩枚举)。
|
|
2646
|
+
// 文案按触发段分叉窗长语义(D9 失败路径总表:中段标注 mid-round no-progress)。
|
|
2647
|
+
const fire = state.settledWatchdogFired;
|
|
2648
|
+
const windowDesc =
|
|
2649
|
+
fire.phase === "mid-round"
|
|
2650
|
+
? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min after prompt (mid-round no-progress)`
|
|
2651
|
+
: `no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end (settled phase)`;
|
|
2652
|
+
success = false;
|
|
2653
|
+
error =
|
|
2654
|
+
`subagent did not reach agent_settled (${windowDesc}; settled watchdog); the process was terminated to bound the wait. ` +
|
|
2655
|
+
`Recovery: check state with subagents action:'list', then re-send your message to continue.`;
|
|
2656
|
+
} else if (record.lastError) {
|
|
2657
|
+
// LLM/provider error 或 abort error 已收口进 record.lastError
|
|
2658
|
+
success = false;
|
|
2659
|
+
error = record.lastError;
|
|
2660
|
+
} else if (exitCode !== 0 && exitCode < SIGNAL_EXIT_CODE_THRESHOLD) {
|
|
2661
|
+
// 非信号退出的非零 exit code = 子进程自身报错
|
|
2662
|
+
success = false;
|
|
2663
|
+
error = stderrBuffer.trim() || `pi subprocess exited with code ${exitCode}`;
|
|
2664
|
+
} else if (opts.signal?.aborted) {
|
|
2665
|
+
// 用户/调用方 signal 取消(非 maxTurns)——不算成功,但也不算 error
|
|
2666
|
+
success = false;
|
|
2667
|
+
error = undefined;
|
|
2668
|
+
} else {
|
|
2669
|
+
// exitCode === 0 或被信号终止(maxTurns 达限 kill)——均视为正常完成
|
|
2670
|
+
success = true;
|
|
2671
|
+
error = record.lastError;
|
|
2672
|
+
}
|
|
2673
|
+
return { success, error };
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2277
2676
|
/**
|
|
2278
2677
|
* spawn pi 子进程执行 session。
|
|
2279
2678
|
*
|
|
@@ -2307,13 +2706,12 @@ export async function runSpawn(
|
|
|
2307
2706
|
ctx,
|
|
2308
2707
|
proc: undefined,
|
|
2309
2708
|
watchdog: undefined,
|
|
2310
|
-
escalationTimer: undefined,
|
|
2311
2709
|
sessionHeader: undefined,
|
|
2312
2710
|
handshakeResult: undefined,
|
|
2313
2711
|
resolveRun: undefined,
|
|
2314
2712
|
keepAliveNoProgressTimer: undefined,
|
|
2315
2713
|
sweepDescendantsOnClose: false,
|
|
2316
|
-
settledWatchdogFired:
|
|
2714
|
+
settledWatchdogFired: undefined,
|
|
2317
2715
|
};
|
|
2318
2716
|
|
|
2319
2717
|
// a/b. 事件累积器(pendingTools 寄存器 + turnLimiter + handleSdkEvent/agentEvent 闭包)
|
|
@@ -2370,124 +2768,16 @@ export async function runSpawn(
|
|
|
2370
2768
|
env: childEnv,
|
|
2371
2769
|
});
|
|
2372
2770
|
state.proc = child;
|
|
2373
|
-
// [
|
|
2374
|
-
//
|
|
2375
|
-
|
|
2376
|
-
// SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
|
|
2377
|
-
// header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
|
|
2378
|
-
if (opts.worktree && child.pid) {
|
|
2379
|
-
// [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
|
|
2380
|
-
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
|
|
2381
|
-
}
|
|
2382
|
-
// [C1] track 子进程供 dispose 兜底 kill(sync + background 均注册——sync 无 controller,
|
|
2383
|
-
// abortRunningControllers 跳过它,靠本 Map 兜底)。close/error 后按句守卫移除(已退出无需再 kill)。
|
|
2384
|
-
spawnedChildren.set(record.id, child);
|
|
2385
|
-
// [V2 决策 3] spawn 后 child.pid 同步立即可得,记录到 record 内存(lifecycle-manager
|
|
2386
|
-
// 孤儿扫描用,Step 5 接入持久化)。resume spawn 时此处同样覆盖更新(pid 可能已变)。
|
|
2387
|
-
if (child.pid !== undefined) record.pid = child.pid;
|
|
2388
|
-
|
|
2389
|
-
// [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
|
|
2390
|
-
// 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
|
|
2391
|
-
// 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
|
|
2392
|
-
// SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
|
|
2393
|
-
// header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
|
|
2394
|
-
// [S1] 防御:必须放在 spawnedChildren.add 之后(onWorktreePid 抛错时子进程已被跟踪,
|
|
2395
|
-
// dispose 兜底 kill 不会泄漏),且包 try/catch(补全失败不阻断 spawn 主流程——
|
|
2396
|
-
// 注册表写失败最坏后果是条目停留 pid=0,由 reaper 宽限回收兜底)。
|
|
2397
|
-
if (opts.worktree && child.pid) {
|
|
2398
|
-
try {
|
|
2399
|
-
// [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
|
|
2400
|
-
void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
|
|
2401
|
-
} catch (err) {
|
|
2402
|
-
logger.warn("[worktree] worktree pid registration failed (defensive)", {
|
|
2403
|
-
branch: opts.worktree.branch,
|
|
2404
|
-
pid: child.pid,
|
|
2405
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2771
|
+
// [圈复杂度门禁提取] spawn 后同步装配(worktree pid 补全 ×2 / 记账 / record.pid /
|
|
2772
|
+
// stdio 编码 / stdin error listener / prompt 命令)整段迁入 setupFreshChild。
|
|
2773
|
+
setupFreshChild(state, child, task);
|
|
2409
2774
|
|
|
2410
|
-
//
|
|
2411
|
-
|
|
2412
|
-
// [m2] 先 setEncoding 再注册 signal listener/watchdog:若 setEncoding 抛错,try/finally
|
|
2413
|
-
//(下方)只清理 tempPromptFile,watchdog/signal listener 尚未注册则无需清理——避免泄漏。
|
|
2414
|
-
child.stdout.setEncoding("utf8");
|
|
2415
|
-
child.stderr.setEncoding("utf8");
|
|
2416
|
-
|
|
2417
|
-
// [v4 A-1] 异步 stdin 'error' listener。writeStdinLine 的 try/catch 只覆盖同步 write 抛错;
|
|
2418
|
-
// 子进程退出后内核回写 EPIPE 会以异步 stream 'error' event 到达,若无 listener 会让 Node
|
|
2419
|
-
// 抛 unhandled 'error' 崩主进程(P1)。必须在首次 stdin 写入(下方 sendPromptCommand)前注册。
|
|
2420
|
-
// handler 行为:①移出 spawnedChildren 标记 dead(与 child.on('error') 同模式);
|
|
2421
|
-
// ②recordEpipeFailure 合并同步/异步计数;③logger.warn 记录一次。
|
|
2422
|
-
// [v4 A-1 裁决] handler 内**不 throw**——stream 'error' listener 内 throw 会经 Node 内部
|
|
2423
|
-
// emit() 传播为 uncaughtException 崩主进程,违背 A-1 防崩核心目标。达阈值的 throw 留给
|
|
2424
|
-
// 同步路径(deliverMessage catch 合并计数达 EPIPE_FAILURE_THRESHOLD 时同步 throw,不崩)。
|
|
2425
|
-
// async handler 只移句柄 + 计数 + warn;进程已 dead(移句柄),下次 deliverMessage 检测
|
|
2426
|
-
// dead 走冷路径,冷路径 write EPIPE 同步计数达阈值同步 throw——防死循环且不崩。
|
|
2427
|
-
child.stdin.on("error", (err: Error) => {
|
|
2428
|
-
// [M4] 按值守卫:resume spawn 已覆盖注册时不误删新 child(见 removeChildRegistration)
|
|
2429
|
-
removeChildRegistration(record.id, child);
|
|
2430
|
-
const count = recordEpipeFailure(record.id);
|
|
2431
|
-
logger.warn(`[subagents] async stdin error for ${record.id}`, {
|
|
2432
|
-
detail: err.message,
|
|
2433
|
-
epipeCount: count,
|
|
2434
|
-
threshold: EPIPE_FAILURE_THRESHOLD,
|
|
2435
|
-
hint:
|
|
2436
|
-
count >= EPIPE_FAILURE_THRESHOLD
|
|
2437
|
-
? "sync path will throw on next write EPIPE"
|
|
2438
|
-
: undefined,
|
|
2439
|
-
});
|
|
2440
|
-
});
|
|
2441
|
-
|
|
2442
|
-
// 喂 prompt 命令驱动子进程开始处理 task。pi runRpcMode 只消费 stdin RpcCommand,
|
|
2443
|
-
// 不读 positional arg;必须在 spawn 后主动写,否则子进程阻塞、totalTokens 恒 0。
|
|
2444
|
-
// 时机安全:pipe 内核缓冲不丢;pi 在 rebindSession 后才挂 stdin reader。
|
|
2445
|
-
sendPromptCommand(child, task);
|
|
2446
|
-
|
|
2447
|
-
// [T2-③ / LC-1] chatMode 首轮 settled 等待固定硬上限(双挂载原语之首轮调用点;
|
|
2448
|
-
// 热路径调用点在 subagent-service deliverMessage,u-t2b 接线)。prompt 发出后挂、
|
|
2449
|
-
// settled 到达(handleSdkEvent)/ close(waitForChildExit)/ resolveRun 任一发生
|
|
2450
|
-
// 即清——settled 永不到达(事件行丢失 / 子进程 wedged)时本 timer 是唯一独立
|
|
2451
|
-
// 回收通道。到期处置:kill 层主 + settledWatchdogFired 标记(收尾据此转 failed
|
|
2452
|
-
// + 恢复指引,见下方 success 判定),与被信号终止视为正常完成的既有语义区分。
|
|
2453
|
-
if (record.chatMode) {
|
|
2454
|
-
armSettledWatchdog(record.id, () => {
|
|
2455
|
-
logger.warn(
|
|
2456
|
-
`[session-runner] settled watchdog fired for ${record.id}: no agent_settled within ${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min of first-round prompt, terminating (LC-1 wedge recovery)`,
|
|
2457
|
-
);
|
|
2458
|
-
state.settledWatchdogFired = true;
|
|
2459
|
-
killChildWithEscalation(state, child, "settled watchdog");
|
|
2460
|
-
});
|
|
2461
|
-
}
|
|
2775
|
+
// [圈复杂度门禁提取] chatMode 首轮 settled 守护挂载迁入 armChatModeSettledWatchdog。
|
|
2776
|
+
armChatModeSettledWatchdog(state, child);
|
|
2462
2777
|
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
const onAbort = ()
|
|
2466
|
-
killChildWithEscalation(state, child, "abort signal");
|
|
2467
|
-
};
|
|
2468
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2469
|
-
// 前置检查:signal 在 spawn 前已 aborted 时 addEventListener 不会触发 onAbort,
|
|
2470
|
-
// 子进程会跑到自然结束。立即 kill 兑现取消语义。
|
|
2471
|
-
if (opts.signal?.aborted) onAbort();
|
|
2472
|
-
|
|
2473
|
-
// e. watchdog:子进程整体超时兜底。卡死在单 tool 内(turn_end 永不触发)时
|
|
2474
|
-
// limiter 失效,此 timer 保证最终 SIGTERM,防止 background 槽位/资源泄漏。
|
|
2475
|
-
// [M-1] timeout 基于 maxTurns 动态计算(maxTurnsToWatchdogMs):旧实现固定 30 分钟
|
|
2476
|
-
// 误杀长任务,现按 maxTurns 线性估算(每 turn ~5 分钟,下限 30 分钟)。
|
|
2477
|
-
// [预算语义对齐] maxTurns 未传/<=0 → 不挂 watchdog(不限,用户明确裁决——旧实现按
|
|
2478
|
-
// 10 turns 估出 50min 默认 SIGTERM 且 maxTurns:0 也关不掉,已废);
|
|
2479
|
-
// SPAWN_WATCHDOG_ENV 设置时按绝对时限兑底挂载(hang 泄漏防线 opt-in)。
|
|
2480
|
-
// [R0] unref:不阻止 Node 进程退出。安全性由 SubagentService.dispose 保证——
|
|
2481
|
-
// 主进程退出时(session_shutdown reason=quit)dispose 会 abort running controller
|
|
2482
|
-
// → 本监听器 kill 子进程。无此 unref,watchdog timer 会拖住 event loop 阻止退出。
|
|
2483
|
-
const watchdogMs = resolveSpawnWatchdogMs(opts.maxTurns);
|
|
2484
|
-
if (watchdogMs !== undefined) {
|
|
2485
|
-
state.watchdog = setTimeout(
|
|
2486
|
-
() => killChildWithEscalation(state, child, "spawn watchdog"),
|
|
2487
|
-
watchdogMs,
|
|
2488
|
-
);
|
|
2489
|
-
state.watchdog.unref();
|
|
2490
|
-
}
|
|
2778
|
+
// [圈复杂度门禁提取] abort 监听 + spawn watchdog 挂载迁入 attachAbortAndSpawnWatchdog,
|
|
2779
|
+
// 返回 onAbort 供下方收尾 removeEventListener(注册与注销同一句柄)。
|
|
2780
|
+
const onAbort = attachAbortAndSpawnWatchdog(state, child);
|
|
2491
2781
|
|
|
2492
2782
|
// [recursive-orchestration] agent_end 有活跃后代时的等待超时(不 kill 分支的兑底)。
|
|
2493
2783
|
// 层主 subagent 空闲等待后代完成(steer 唤醒)期间不产生 turn,原 watchdog 已清;
|
|
@@ -2497,32 +2787,9 @@ export async function runSpawn(
|
|
|
2497
2787
|
// stdout pump:逐行解析 → handleSdkEvent / enqueueUiRequest + get_state 握手状态机
|
|
2498
2788
|
const pump = attachStdoutPump(child, state, sessionDir, handleSdkEvent);
|
|
2499
2789
|
|
|
2500
|
-
//
|
|
2501
|
-
//
|
|
2502
|
-
|
|
2503
|
-
// resolve(加速路径),握手仍启动但无害——response 到达时外层已 resolve,resolver
|
|
2504
|
-
// 因 resolved=true 直接 return。
|
|
2505
|
-
//
|
|
2506
|
-
// 时序:握手在 stdout pump(get_stateListeners 已就绪)之后启动。get_state 命令写入
|
|
2507
|
-
// stdin,pi rebindSession 后读取并返回 response,经 stdout pump 匹配 resolver 触发 resolve。
|
|
2508
|
-
// close handler await handshakeSettled,保证无论 task 多快结束,close 时 sessionFile 已回填。
|
|
2509
|
-
// [#18] 握手状态变量已在 attachStdoutPump 内部(stdout handler 注册前)定义,此处直接发起握手。
|
|
2510
|
-
// [F2] .catch 兜底:performGetStateHandshake 的 Promise executor 同步调 tryOnce →
|
|
2511
|
-
// sendGetStateCommand → writeStdinLine 在 stdin 已断(EPIPE/ERR_STREAM_DESTROYED)时同步
|
|
2512
|
-
// throw → executor 内同步异常被 Promise 构造器转为 reject。若无人接(旧实现只有 .then),
|
|
2513
|
-
// reject 无人消费 → unhandledRejection(Node 15+ 默认 mode=throw)可崩父 pi 进程。
|
|
2514
|
-
// catch 内:logger.error 留证 + pump.abandonHandshake 记录握手失败终态——close handler
|
|
2515
|
-
// await 的 handshakeSettled 立即 settle,isHandshakePending() 归 false,不阻塞收尾链路
|
|
2516
|
-
//(sessionFile 兜底由收尾时的 existsSync 校验 + findSessionFileByHeaderId 承担)。
|
|
2517
|
-
void performGetStateHandshake(child, pump.registerGetStateListener).then((r) => {
|
|
2518
|
-
// header 加速路径下 settleHandshake 已 undefined,跳过(避免覆盖 header 结果)。
|
|
2519
|
-
// 超时兜底(r 为空对象)也经此分支 settle,但 record.sessionFile 不回填。
|
|
2520
|
-
if (pump.isHandshakePending()) pump.finishHandshake(r);
|
|
2521
|
-
}).catch((err: unknown) => {
|
|
2522
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
2523
|
-
logger.error(`[session-runner] get_state handshake failed: ${m}`);
|
|
2524
|
-
pump.abandonHandshake();
|
|
2525
|
-
});
|
|
2790
|
+
// [圈复杂度门禁提取] get_state 握手启动(fire-and-forget + F2 catch 兜底)迁入
|
|
2791
|
+
// startGetStateHandshake。
|
|
2792
|
+
startGetStateHandshake(child, pump);
|
|
2526
2793
|
|
|
2527
2794
|
child.stderr.on("data", (data: string) => {
|
|
2528
2795
|
// 截断防 OOM:失控子进程持续打 stderr 会耗尽父进程内存。保留尾部便于诊断。
|
|
@@ -2534,85 +2801,23 @@ export async function runSpawn(
|
|
|
2534
2801
|
|
|
2535
2802
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
2536
2803
|
clearTimeout(state.watchdog);
|
|
2537
|
-
// [
|
|
2538
|
-
//
|
|
2539
|
-
|
|
2804
|
+
// [D3-①] SIGKILL 升级 timer 的清理由公共 kill-chain 内部承担(exit 事件 settle
|
|
2805
|
+
// 等待 promise → raceTimeout clearTimeout;且链持有 child 闭包,不误伤后续同
|
|
2806
|
+
// state 子进程)——旧 escalationTimer 兜底句柄随旧私有杀链一并删除。
|
|
2540
2807
|
// [T2-①③] 收尾兜底清新增 timer(正常清除点已覆盖;防收尾路径遗漏泄漏)
|
|
2541
2808
|
disarmKeepAliveNoProgressTimer(state);
|
|
2542
2809
|
disarmSettledWatchdog(record.id);
|
|
2543
2810
|
|
|
2544
|
-
// [
|
|
2545
|
-
|
|
2546
|
-
// 父进程不再 fs 补写——fs 补写的 entry 缺 id/parentId 污染 pi _buildIndex。
|
|
2547
|
-
// 此处仅保留 sessionFile 路径兜底(deriveSessionFilePath/握手路径可能不准)。
|
|
2548
|
-
//
|
|
2549
|
-
// [T1/LC-4] findSessionFileByHeaderId 兜底查找移出 `if (record.sessionFile)` 守卫:
|
|
2550
|
-
// 原守卫条件恰等于它要兜底的缺失本身——RC-1 握手失败留下的「sessionId 有、
|
|
2551
|
-
// sessionFile 无」形态(两字段独立采集)永远不可达本兜底,下游 finalize marker /
|
|
2552
|
-
// alive marker / identity 写入全部失去依据(PS-9 放大)。现在两种形态都反查:
|
|
2553
|
-
// sessionFile 有但路径不存在(推导错 / pi 命名变化);sessionFile 无而 sessionId 有
|
|
2554
|
-
// (header 或握手部分成功,含 agent_end 惰性回补补入的 handshakeResult.sessionId)。
|
|
2555
|
-
{
|
|
2556
|
-
const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
|
|
2557
|
-
const needsLookup = record.sessionFile
|
|
2558
|
-
? !fs.existsSync(record.sessionFile)
|
|
2559
|
-
: lookupId !== undefined;
|
|
2560
|
-
if (lookupId && needsLookup) {
|
|
2561
|
-
const actual = findSessionFileByHeaderId(sessionDir, lookupId);
|
|
2562
|
-
if (actual && actual !== record.sessionFile) record.sessionFile = actual;
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2811
|
+
// [圈复杂度门禁提取] sessionFile 兜底反查(LC-4)迁入 backfillSessionFileByLookup。
|
|
2812
|
+
backfillSessionFileByLookup(state, sessionDir);
|
|
2565
2813
|
|
|
2566
|
-
// [
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
// 从层主 sessionFile 差集采集活跃后代清单,对清单内每个后代迭代展开至叶(递归读
|
|
2570
|
-
// 各后代的 pending 差集),kill 前做存活 + cmdline(pi/--mode rpc)校验防 pid 复用
|
|
2571
|
-
// 误杀,逐个 escalation kill(SIGTERM→SIGKILL)。同步执行:后代树规模有限 + 每
|
|
2572
|
-
// 步有界(ps 探测 3s 超时),不阻塞 runSpawn 收尾的可感时长。
|
|
2573
|
-
if (state.sweepDescendantsOnClose) {
|
|
2574
|
-
try {
|
|
2575
|
-
const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
|
|
2576
|
-
if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
|
|
2577
|
-
logger.warn(
|
|
2578
|
-
`[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`,
|
|
2579
|
-
);
|
|
2580
|
-
}
|
|
2581
|
-
} catch (err) {
|
|
2582
|
-
// sweep 整体失败不掩盖层主自身的收尾结果(best-effort 可见)
|
|
2583
|
-
bestEffort(err, "descendant sweep (keep-alive watchdog)", "error");
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2814
|
+
// [圈复杂度门禁提取] 后代级联补杀 sweep(T2-② 两步时序后半)迁入
|
|
2815
|
+
// sweepDescendantsOnChildClose。
|
|
2816
|
+
sweepDescendantsOnChildClose(state, sessionDir);
|
|
2586
2817
|
|
|
2587
2818
|
// 判定成功/失败(三来源:exitCode + record.lastError + abort 原因)
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
if (state.settledWatchdogFired) {
|
|
2591
|
-
// [T2-③ / LC-1] settled 硬上限到期回收 ≠ 正常完成:被信号终止视为正常完成的
|
|
2592
|
-
// 既有语义(maxTurns 达限 kill)不适用于本形态——runSpawn 以错误返回(设计
|
|
2593
|
-
// §6.2),错误消息含恢复指引(S-B 验收判据)。closedReason 的 "watchdog" 映射
|
|
2594
|
-
// 由 finalize 侧按 error 标记承接(ClosedReason 枚举封闭,不在此擅自扩枚举)。
|
|
2595
|
-
success = false;
|
|
2596
|
-
error =
|
|
2597
|
-
`subagent did not reach agent_settled within ${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min (settled watchdog); the process was terminated to bound the wait. ` +
|
|
2598
|
-
`Recovery: check state with subagents action:'list', then re-send your message to continue.`;
|
|
2599
|
-
} else if (record.lastError) {
|
|
2600
|
-
// LLM/provider error 或 abort error 已收口进 record.lastError
|
|
2601
|
-
success = false;
|
|
2602
|
-
error = record.lastError;
|
|
2603
|
-
} else if (exitCode !== 0 && exitCode < SIGNAL_EXIT_CODE_THRESHOLD) {
|
|
2604
|
-
// 非信号退出的非零 exit code = 子进程自身报错
|
|
2605
|
-
success = false;
|
|
2606
|
-
error = stderrBuffer.trim() || `pi subprocess exited with code ${exitCode}`;
|
|
2607
|
-
} else if (opts.signal?.aborted) {
|
|
2608
|
-
// 用户/调用方 signal 取消(非 maxTurns)——不算成功,但也不算 error
|
|
2609
|
-
success = false;
|
|
2610
|
-
error = undefined;
|
|
2611
|
-
} else {
|
|
2612
|
-
// exitCode === 0 或被信号终止(maxTurns 达限 kill)——均视为正常完成
|
|
2613
|
-
success = true;
|
|
2614
|
-
error = record.lastError;
|
|
2615
|
-
}
|
|
2819
|
+
// [圈复杂度门禁提取] 四分支判定迁入 resolveRunOutcome(行为逐字节保持)。
|
|
2820
|
+
const { success, error } = resolveRunOutcome(state, exitCode, stderrBuffer);
|
|
2616
2821
|
|
|
2617
2822
|
// g. collectResult(完全复用——全部从 record 读)
|
|
2618
2823
|
// [F-1] schemaExpected:schema/schemaEnv 任一存在即要求结构化产出(耦合形态
|