@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,87 +1,85 @@
|
|
|
1
|
-
// 执行编排 +
|
|
2
|
-
//
|
|
1
|
+
// 执行编排 + 记录领域 Service(D4 按变化轴拆分后的编排核:execute/executeAndAwait 入口、
|
|
2
|
+
// record 生命周期、cancel)。通知簇 → notify-host.ts;轮次结算闭包 → round-settlement.ts;
|
|
3
|
+
// 冷路径复活 → cold-resurrect.ts。
|
|
4
|
+
// 上游:subagent-tool(execute/query/cancel)、TUI(onChange/collectRecords)。
|
|
3
5
|
// session_start 时经 initSession 注入 pi;modelRegistry/entries 归 ModelConfigService.initModel。
|
|
4
6
|
|
|
5
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
8
|
|
|
8
9
|
import { getLogger } from "../core/logger.ts";
|
|
9
10
|
|
|
10
11
|
import type { ExtensionMode } from "./host-mode.ts";
|
|
11
12
|
|
|
12
13
|
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
13
|
-
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
14
14
|
import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
|
|
15
15
|
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
16
16
|
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
17
|
-
import { removeAliveMarker
|
|
17
|
+
import { removeAliveMarker } from "./alive-store.ts";
|
|
18
18
|
import { bestEffort } from "./best-effort.ts";
|
|
19
|
+
import { CollectCoordinator } from "./collect-coordinator.ts";
|
|
20
|
+
import { DEFAULT_COLLECT_SYNC } from "./config.ts";
|
|
19
21
|
// [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程)。
|
|
20
|
-
// [
|
|
21
|
-
// [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
armIdleTimer,
|
|
25
|
-
disarmIdleTimer,
|
|
26
|
-
hasIdleTimer,
|
|
27
|
-
DEFAULT_IDLE_TIMEOUT_MS,
|
|
28
|
-
} from "./lifecycle-manager.ts";
|
|
22
|
+
// [T4②] DEFAULT_IDLE_TIMEOUT_MS:assertIdleTimeoutMsSafe 错误文案的缺省时长基准。
|
|
23
|
+
// ([M3] hasIdleTimer / hasLiveProcessHandle 的 piAdapter.hasRunningBackground 过滤随通知簇
|
|
24
|
+
// 在 notify-host.ts 消费,不经本文件。)
|
|
25
|
+
import { disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
|
|
29
26
|
import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
30
27
|
import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
|
|
28
|
+
import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "./cold-resurrect.ts";
|
|
31
29
|
import {
|
|
32
30
|
completeRecord,
|
|
33
31
|
createRecord,
|
|
34
|
-
getFullTextFrom,
|
|
35
|
-
nextRoundBaseTurnIndex,
|
|
36
32
|
project,
|
|
37
|
-
resurrectClosed,
|
|
38
33
|
snapshot,
|
|
39
34
|
tryTransition,
|
|
40
35
|
} from "./execution-record.ts";
|
|
41
36
|
import { doFinalizeRecord, doFinalizeRoundToIdle } from "./finalize-record.ts";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import { resolveJournalPath } from "./engine/paths.ts";
|
|
37
|
+
import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
|
|
38
|
+
import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
|
|
39
|
+
import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
|
|
46
40
|
import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
|
|
41
|
+
import { PiEngine } from "./engine/engines/pi/pi-engine.ts";
|
|
42
|
+
import { PI_POOL_KEY } from "./engine/engines/pi/pi-engine.ts";
|
|
43
|
+
import type { ChatRoundTicket, PiEngineService } from "./engine/engines/pi/pi-engine.ts";
|
|
47
44
|
import type { EnginePort, RunContext } from "./engine/port.ts";
|
|
48
|
-
import { DEFAULT_ENGINE_ID, getEngine } from "./engine/registry.ts";
|
|
49
|
-
import {
|
|
45
|
+
import { DEFAULT_ENGINE_ID, getEngine, listEngines } from "./engine/registry.ts";
|
|
46
|
+
import { validateModelForEngine, withCrossEngineHint } from "./engine/model-validation.ts";
|
|
47
|
+
import { type EngineRouteResult, routeEngineForHost } from "./engine/routing.ts";
|
|
50
48
|
import type { AgentOutcome } from "./engine/types.ts";
|
|
51
49
|
import { ManifestStore } from "./manifest-store.ts";
|
|
52
50
|
import type { ModelConfigService } from "./model-config-service.ts";
|
|
53
51
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
54
|
-
import type
|
|
55
|
-
|
|
52
|
+
import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts";
|
|
53
|
+
// [T4④ / PS-5] flush 被门拦时的未投递 pending 落盘账本(persistUndeliveredNotificationsForReplay 消费)
|
|
54
|
+
import type { BatchBudgetParams, BgNotifyRecord } from "./notifier.ts";
|
|
56
55
|
import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
|
|
57
56
|
import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
|
|
57
|
+
import { createRoundSettler } from "./round-settlement.ts";
|
|
58
58
|
import type { StatusFilter } from "./record-store.ts";
|
|
59
59
|
import { RecordStore } from "./record-store.ts";
|
|
60
|
+
// [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
|
|
61
|
+
//(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
|
|
62
|
+
import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
|
|
60
63
|
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
61
64
|
import {
|
|
62
|
-
getChildByRecord,
|
|
63
65
|
killAllSpawnedChildren,
|
|
64
66
|
killRecordChildWithEscalation,
|
|
65
67
|
registerSpawnedChildForRecord,
|
|
66
68
|
runSpawn,
|
|
67
|
-
spawnedChildren,
|
|
68
69
|
type SessionRunnerContext,
|
|
69
70
|
type SpawnResumeOpts,
|
|
70
|
-
} from "./session-runner.ts";
|
|
71
|
+
} from "./engine/engines/pi/session-runner.ts";
|
|
72
|
+
// [u-t2a T2②/T2③] settled watchdog:chatMode 轮 settled 等待固定硬上限(10min),
|
|
73
|
+
// 双挂载点之一在编排层热路径(deliverChatMessage,prompt 发出后),disarm 站点散布
|
|
74
|
+
// cancel/close/终态化路径(原语幂等,见 settled-watchdog.ts 头注释)。
|
|
71
75
|
import {
|
|
72
|
-
|
|
76
|
+
armMidRoundNoProgress,
|
|
73
77
|
disarmSettledWatchdog,
|
|
74
|
-
|
|
78
|
+
type SettledWatchdogFireInfo,
|
|
75
79
|
} from "./settled-watchdog.ts";
|
|
76
|
-
import { isIdle, isResumable
|
|
80
|
+
import { isIdle, isResumable } from "./lifecycle-predicates.ts";
|
|
77
81
|
import { startIdleGc } from "./idle-gc.ts";
|
|
78
|
-
import {
|
|
79
|
-
clearEpipeFailure,
|
|
80
|
-
EPIPE_FAILURE_THRESHOLD,
|
|
81
|
-
recordEpipeFailure,
|
|
82
|
-
resetAllEpipeFailures,
|
|
83
|
-
sendPromptCommand,
|
|
84
|
-
} from "./stdin-writer.ts";
|
|
82
|
+
import { resetAllEpipeFailures } from "./engine/engines/pi/stdin-writer.ts";
|
|
85
83
|
import type { StreamSink, SubagentStream } from "./stream-sink.ts";
|
|
86
84
|
import { createBackgroundStream } from "./stream-sink.ts";
|
|
87
85
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
@@ -98,19 +96,45 @@ import type {
|
|
|
98
96
|
SubagentRecord,
|
|
99
97
|
} from "./types.ts";
|
|
100
98
|
import { ForkDepthExceededError } from "./types.ts";
|
|
101
|
-
import { isReconnectableFinalReason, ResurrectDeniedError } from "./types.ts";
|
|
102
99
|
import { DEFAULT_AGENT_NAME } from "./types.ts";
|
|
103
100
|
import { registerGlobalObservability, UiRequestObservability } from "./ui-request-observability.ts";
|
|
104
101
|
import { WorktreeManager } from "./worktree-manager.ts";
|
|
102
|
+
import { toErrorMessage } from "../core/error-message.ts";
|
|
105
103
|
|
|
106
104
|
const logger = getLogger("subagents");
|
|
107
105
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
106
|
+
/** [D4 查询面聚合] 读模型轴(record 快照读取 + store 订阅)——Service 上的
|
|
107
|
+
* `service.queries` 消费面。变化轴:改查询投影 / 过滤 / 订阅语义,只动 queries 组;
|
|
108
|
+
* Service 本体保留编排核(execute/executeAndAwait/cancel)与生命周期面。 */
|
|
109
|
+
export interface SubagentQueries {
|
|
110
|
+
/** 按 id 查内存 running record 的只读快照(G3-002 修复)。不存在返回 undefined。 */
|
|
111
|
+
findRecord(id: string): RecordSnapshot | undefined;
|
|
112
|
+
/** [v8.5 A1/B] 全态查找:任意状态 × 任意归属的 record 快照(message 拒绝文案分流
|
|
113
|
+
* 与 fork-from 源解析共用)。id 在内存与磁盘均不存在返回 undefined。 */
|
|
114
|
+
lookupRecordAnyState(id: string): SubagentRecord | undefined;
|
|
115
|
+
/** 合并内存 + 磁盘 record(/subagents list + tool list 消费,按 rootSessionId 过滤)。 */
|
|
116
|
+
collectRecords(limit: number, statusFilter?: StatusFilter): SubagentRecord[];
|
|
117
|
+
/** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。 */
|
|
118
|
+
getFullRecord(id: string): SubagentRecord | undefined;
|
|
119
|
+
/** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
|
|
120
|
+
onChange(listener: () => void): () => void;
|
|
121
|
+
}
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
/** [D4 对话 action 面聚合] chat 域 message/close action 轴(M2-B3,原 Service 同节三方法)
|
|
124
|
+
* ——Service 上的 `service.chatActions` 消费面。变化轴:改对话域归属校验 / close 分流 /
|
|
125
|
+
投递编排,只动 chatActions 组。 */
|
|
126
|
+
export interface SubagentChatActions {
|
|
127
|
+
/** 按 id 查 record 并做归属校验(message/close action 的统一入口)。 */
|
|
128
|
+
getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord;
|
|
129
|
+
/** close action 的统一行为分流(running 子态 × force)。 */
|
|
130
|
+
closeSubagent(record: ExecutionRecord, force: boolean): Promise<void>;
|
|
131
|
+
/** chatMode 统一投递入口(message action,经引擎交互面执行)。 */
|
|
132
|
+
deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// [v4 A-1] EPIPE 连续失败计数器在 stdin-writer.ts(stdin 错误域,避免 session-runner
|
|
136
|
+
// 反向 import 本文件 helper 产生循环依赖)。同步路径(PiEngine 热路径投递,D2 协议知识
|
|
137
|
+
// 下沉后)与异步路径(session-runner child.stdin.on('error'))共用 stdin-writer 的同一计数器。
|
|
114
138
|
|
|
115
139
|
/** dispose 后注入的 stub UI 请求 handler。
|
|
116
140
|
*
|
|
@@ -129,48 +153,13 @@ const COLD_LOOKUP_SCAN_LIMIT = 1000;
|
|
|
129
153
|
* 没真正解决。 */
|
|
130
154
|
const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
|
|
131
155
|
|
|
132
|
-
/** Pi ExtensionAPI 的最小接口(duck-typed)。
|
|
133
|
-
* subagent-service 直接调 pi.sendMessage 发 background 完成通知(BgNotifier 滑动窗口合并),
|
|
134
|
-
* 不委托 pending-notifications EventBus 中继——后者只管 registry 不参与通知发送。 */
|
|
135
|
-
interface PiLike {
|
|
136
|
-
appendEntry(customType: string, data?: unknown): void;
|
|
137
|
-
events: { emit(channel: string, data: unknown): void };
|
|
138
|
-
sendMessage(
|
|
139
|
-
message: { customType: string; content: string; display: boolean; details?: unknown },
|
|
140
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, // g4-allow: 类型注解——PiLike 接口形状(pi.sendMessage 签面子集),非投递调用
|
|
141
|
-
): void;
|
|
142
|
-
/** 订阅 pi 事件(D8:notifier 的 settled 边沿订阅用 'agent_settled')。
|
|
143
|
-
* pi 0.84.4 的 on 返回 void 且无 off——退订语义由调用侧 disposed 标志包装兑现。
|
|
144
|
-
* 可选:旧测试 mock pi 可能未实现 on,缺省时 notifier 退化为内核退避路径。 */
|
|
145
|
-
on?(event: "agent_settled", handler: () => void): void;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
156
|
/** UI streaming sink 的最小接口(ctx.ui.setWidget 的 duck-typed 子集)。
|
|
149
157
|
* session_start 时从 ctx.ui 注入,background 执行期间用于把合并后的 text_delta
|
|
150
158
|
* 通过 setWidget 通道转发到 RPC stdout(不经 sendMessage 的持久化路径)。 */
|
|
151
159
|
export type { StreamSink } from "./stream-sink.ts";
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* 完整路径仍走 record.agent(env 注入 / 持久化)。 */
|
|
156
|
-
function emitPendingRegister(pi: PiLike | null, id: string, name?: string): void {
|
|
157
|
-
pi?.events.emit("pending:register", {
|
|
158
|
-
id,
|
|
159
|
-
type: "subagent",
|
|
160
|
-
name: name ? displayAgentName(name) : id,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function emitPendingUnregister(
|
|
165
|
-
pi: PiLike | null,
|
|
166
|
-
id: string,
|
|
167
|
-
reason: string,
|
|
168
|
-
): void {
|
|
169
|
-
pi?.events.emit("pending:unregister", {
|
|
170
|
-
id,
|
|
171
|
-
reason,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
161
|
+
// pi 依赖端口类型 re-export:测试侧 mock PiLike 历来从本模块取(与 StreamSink 同构的门面模式)
|
|
162
|
+
export type { PiLike } from "./notify-host.ts";
|
|
174
163
|
|
|
175
164
|
/**
|
|
176
165
|
* Service 构造参数(进程级)。
|
|
@@ -204,8 +193,12 @@ export interface SubagentServiceSessionInit {
|
|
|
204
193
|
* initSession 读取后存入 this.sessionMode,buildSessionRunnerContext 透传给 session-runner。 */
|
|
205
194
|
mode?: ExtensionMode;
|
|
206
195
|
/** UI 请求 handler(session 级覆盖进程级)。
|
|
207
|
-
*
|
|
208
|
-
|
|
196
|
+
* [D4-④ UI 接线外提] 本字段是 handler 的唯一注入入口(原 setUiRequestHandler 方法已删)。
|
|
197
|
+
* 三态语义:undefined = 不动(保留进程级构造/上次值,供不注入 handler 的调用方);
|
|
198
|
+
* null = 显式清空(承载原 setUiRequestHandler(undefined) 语义——headless 的
|
|
199
|
+
* createUiRequestHandlerForMode 返回 undefined 时壳侧传 null);值 = 注入并重置
|
|
200
|
+
* 缺失告警去重。 */
|
|
201
|
+
uiRequestHandler?: UiRequestHandler | null;
|
|
209
202
|
/** L2 跨子进程全局 dialog 串行队列(进程单例)。透传给 session-runner,
|
|
210
203
|
* child close 时调 rejectChildDialogs 清理 pending(SR-4 防全局死锁)。 */
|
|
211
204
|
dialogQueue?: DialogGlobalQueue;
|
|
@@ -223,14 +216,14 @@ const MS_PER_SECOND = 1000;
|
|
|
223
216
|
const SECONDS_PER_MINUTE = 60;
|
|
224
217
|
|
|
225
218
|
/** [T4① / PS-2] notify 门拦截集:disposeAllRecords 因这两类原因关闭的 record,其迟到的
|
|
226
|
-
*
|
|
219
|
+
* 轮次完成回注不得注入新 session——/new、/fork 的决策([v4 A-6])
|
|
227
220
|
* 是「被关 record 的告知改由 list 的 closedReason 表达」,不主动通知;旧门只排除
|
|
228
221
|
* cancelled,parent-new/parent-fork 放行 → 新对话被「Subagent X failed: closed due to
|
|
229
222
|
* parent-new」的僵尸回执 triggerTurn 唤醒,已废弃会话的通知注入新上下文。 */
|
|
230
223
|
const NOTIFY_BLOCKED_CLOSED_REASONS: ReadonlySet<ClosedReason> = new Set(["parent-new", "parent-fork"]);
|
|
231
224
|
|
|
232
225
|
/**
|
|
233
|
-
* [T4① / PS-2]
|
|
226
|
+
* [T4① / PS-2] 轮次完成回注的 notify 门(按 closedReason 白名单放行)。
|
|
234
227
|
*
|
|
235
228
|
* cancelled(cancelBackground 自己 notify)与 parent-new/parent-fork(编排性关闭,
|
|
236
229
|
* 告知由 list 的 closedReason 表达)不放行;其余(undefined = 本路径抢到 CAS 尚未
|
|
@@ -255,6 +248,11 @@ const ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
|
|
|
255
248
|
const ENV_DEPTH = "PI_SUBAGENT_DEPTH";
|
|
256
249
|
const ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
|
|
257
250
|
|
|
251
|
+
/** [v2 D4] E1 等待分支 settled 有界重扫上限。无上限重扫 = 泄漏(设计 §3.3 D4 被否
|
|
252
|
+
* 谱系);8 次覆盖重启后主 agent 对 resumable 成员的典型续跑轮次,达限仍有
|
|
253
|
+
* running → disposed,交下次 session_start 收敛。 */
|
|
254
|
+
const SETTLED_RESCAN_LIMIT = 8;
|
|
255
|
+
|
|
258
256
|
/** resolveIdentity 的产物——一次确定、写入 record 后不再变。 */
|
|
259
257
|
interface ResolvedIdentity {
|
|
260
258
|
agent: string;
|
|
@@ -303,16 +301,12 @@ export class SubagentService {
|
|
|
303
301
|
* (collectRecords filter 用,与 createRecordForMode 的 rootSessionId 盖章同源——子进程
|
|
304
302
|
* 因此看到整棵 ROOT 树)。设计见 recursive-subagent-visibility.md 决策 3。 */
|
|
305
303
|
private sessionRootId: string | null = null;
|
|
306
|
-
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
* 这一跨进程树形关系成立。
|
|
313
|
-
* initSession 设置:有 env PI_SUBAGENT_SELF_RECORD_ID → {recordId: env 值, depth: env DEPTH};
|
|
314
|
-
* 无 env(根进程)→ null(顶层)。 */
|
|
315
|
-
private execCtxBaseline: { recordId: string | undefined; depth: number } | null = null;
|
|
304
|
+
/**
|
|
305
|
+
* [D3-⑤ 嵌套防护合一] 进程内执行嵌套上下文(原 execCtxAls 私有字段下沉公共层
|
|
306
|
+
* common/nesting-guard.ts ExecutionNestingContext——机制注释含 ALS 断裂基线兜底)。
|
|
307
|
+
* 实例 per-Service:基线随宿主进程身份而异(initSession 从 env 建立)。
|
|
308
|
+
*/
|
|
309
|
+
private readonly execNesting = new ExecutionNestingContext();
|
|
316
310
|
/** fork 深度基线(同 ALS 断裂问题:forkDepthAls.getStore() 兜底用)。根进程=0。 */
|
|
317
311
|
private forkDepthBaseline = 0;
|
|
318
312
|
/** [MF-3] 所属根进程 cwd(sessions/records 落盘目录编码键)。
|
|
@@ -328,32 +322,109 @@ export class SubagentService {
|
|
|
328
322
|
getStreamSink(): StreamSink | null { return this.streamSink; }
|
|
329
323
|
private _disposed = false;
|
|
330
324
|
private _seq = 0;
|
|
331
|
-
/**
|
|
332
|
-
|
|
325
|
+
/** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
|
|
326
|
+
* 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
|
|
327
|
+
* deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
|
|
328
|
+
* createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
|
|
329
|
+
* [sync-collect 合并] toNotifyRecord/notify/notifyBatch 随批路由需要由 host 导出
|
|
330
|
+
* (collectCoordinator 闭包与 E9/E1 直发路径消费,见 notify-host.ts 接口注释)。 */
|
|
331
|
+
private readonly notifyHost: NotifyHost = createNotifyHost({
|
|
332
|
+
getPi: () => this.pi,
|
|
333
|
+
listRunning: () => this.store.listRunning(),
|
|
334
|
+
getIsIdle: () => this.isIdleFn,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
|
|
338
|
+
private readonly collectCoordinator: CollectCoordinator;
|
|
339
|
+
/** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
|
|
340
|
+
* 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
|
|
341
|
+
* 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
|
|
342
|
+
* sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
|
|
343
|
+
* 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
|
|
344
|
+
private readonly e9ConvertedIds = new Set<string>();
|
|
345
|
+
/** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
|
|
346
|
+
* 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
|
|
347
|
+
* 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
|
|
348
|
+
* pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
|
|
349
|
+
* 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
|
|
350
|
+
* 的文件域);dispose 的惰化处置见 dispose()。 */
|
|
351
|
+
private settledRescanState: { disposed: boolean; scans: number } | null = null;
|
|
333
352
|
/** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
|
|
334
353
|
* 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
|
|
335
354
|
* parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
|
|
336
355
|
* [MF#2] 旧实现用单实例字段跨执行链共享 → 并发下 A 还原深度后 B 读到被压低值 → 护栏失效。 */
|
|
337
356
|
private readonly forkDepthAls = new AsyncLocalStorage<number>();
|
|
338
357
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* EPIPE 兜底不持锁,同样被覆盖)。child 注册完成后 deliverMessage 走热路径,不经此守卫。 */
|
|
358
|
+
// [D3-⑤] subagent 执行上下文(record 身份 + 递归深度)的 ALS 传递已下沉公共层
|
|
359
|
+
// (execNesting 字段,common/nesting-guard.ts)——「B run() 期间挂身份,B 内创建 C
|
|
360
|
+
// 时读到 B」的机制与 ALS 断裂基线兜底注释见该文件。与 forkDepthAls 独立:后者只数
|
|
361
|
+
// fork 链(fork=true 才递增),嵌套上下文数所有 subagent 嵌套。
|
|
362
|
+
|
|
363
|
+
/** [review MF1] record 级在途 resume 守卫。冷路径续轮(resumeChatRound)全部守卫通过后
|
|
364
|
+
* add,runAndFinalize 结束(finally,覆盖轮次完成 / MF-6 失败回退 / abort / 终态化所有
|
|
365
|
+
* 分支)时 delete(幂等:execute() 新建 record 不在集合,no-op)。窗口 = resume 发起
|
|
366
|
+
* (含 pool.acquire 排队)→ 本轮 runAndFinalize 收尾。窗口内同 record 再次到达续轮
|
|
367
|
+
* (冷路径重入 / EPIPE 兜底)直接 throw——防两个 pi 子进程以 --session 同一 JSONL 双写 +
|
|
368
|
+
* 前一个脱离 kill 记账成孤儿(冷路径的 acquireActivateLock 只覆盖续轮同步段,锁释放
|
|
369
|
+
* 在子进程注册(session-runner spawnedChildren.set)之前,锁空洞由此守卫兜住;EPIPE
|
|
370
|
+
* 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
|
|
353
371
|
private readonly resumesInFlight = new Set<string>();
|
|
354
372
|
|
|
373
|
+
/** chat 域 pi 引擎实例(D2 单轨:chat 域执行/投递统一经 EnginePort)。per-service DI——
|
|
374
|
+
* getService 经适配器绑本实例;registry 全局 'pi' 单例绑进程级 getSubagentService(),
|
|
375
|
+
* 直构 Service 的测试场景解析不到本实例。不能 import registration.ts(其 import 本文件
|
|
376
|
+
* → 循环依赖),直接构造 PiEngine(pi-engine 不反向依赖本文件)。 */
|
|
377
|
+
private readonly chatPiEngine: PiEngine = new PiEngine({ getService: () => this.piEngineServiceAdapter() });
|
|
378
|
+
|
|
379
|
+
/** chat 域轮次交接包(executeViaEngine / 冷路径续轮挂载 → PiEngine.run 经 taskId 消费)。 */
|
|
380
|
+
private readonly chatRoundTickets = new Map<string, ChatRoundTicket>();
|
|
381
|
+
|
|
382
|
+
/** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
|
|
383
|
+
* 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
|
|
384
|
+
private readonly settleRound = createRoundSettler({
|
|
385
|
+
notifyComplete: (record) => this.collectCoordinator.route(record),
|
|
386
|
+
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
387
|
+
closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
|
|
391
|
+
* deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
|
|
392
|
+
private readonly coldResurrectDeps: ColdResurrectDeps = {
|
|
393
|
+
findLightById: (id) => this.store.findLightById(id),
|
|
394
|
+
collectRecords: (limit, statusFilter, rootFilter) =>
|
|
395
|
+
this.store.collectRecords(limit, statusFilter, rootFilter),
|
|
396
|
+
register: (record) => this.store.register(record),
|
|
397
|
+
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
398
|
+
getSessionRootId: () => this.sessionRootId,
|
|
399
|
+
getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
|
|
403
|
+
* 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
|
|
404
|
+
readonly queries: SubagentQueries = {
|
|
405
|
+
findRecord: (id) => this.findRecord(id),
|
|
406
|
+
lookupRecordAnyState: (id) => this.lookupRecordAnyState(id),
|
|
407
|
+
collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
|
|
408
|
+
getFullRecord: (id) => this.getFullRecord(id),
|
|
409
|
+
onChange: (listener) => this.onChange(listener),
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** [D4 对话 action 面聚合] chat 域 message/close 消费面(壳 subagent-actions 经此访问;
|
|
413
|
+
* 纯委托同上。PiEngineService 适配器不经此——引擎边界走 piEngineServiceAdapter)。 */
|
|
414
|
+
readonly chatActions: SubagentChatActions = {
|
|
415
|
+
getRecordForAction: (id, opts) => this.getRecordForAction(id, opts),
|
|
416
|
+
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
417
|
+
deliverChatMessage: (record, text, interrupt) => this.deliverChatMessage(record, text, interrupt),
|
|
418
|
+
};
|
|
419
|
+
|
|
355
420
|
private readonly manifestStore: ManifestStore;
|
|
356
421
|
|
|
422
|
+
/**
|
|
423
|
+
* [D6 #7a] records 目录(与 manifestStore 同源同一推导)——屏障失败 warn 带 manifest
|
|
424
|
+
* 文件路径用(ManifestStore.dir 私有,此处不破封装另存同源值;漂移由构造点同语句保证不发生)。
|
|
425
|
+
*/
|
|
426
|
+
private readonly recordsDir: string;
|
|
427
|
+
|
|
357
428
|
/**
|
|
358
429
|
* [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
|
|
359
430
|
* 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
|
|
@@ -378,9 +449,83 @@ export class SubagentService {
|
|
|
378
449
|
const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
379
450
|
const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
380
451
|
this.sessionsDir = sessionsDir;
|
|
452
|
+
this.recordsDir = recordsDir;
|
|
381
453
|
this.manifestStore = new ManifestStore(recordsDir);
|
|
382
454
|
this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? undefined);
|
|
383
|
-
|
|
455
|
+
// collectCoordinator(subagent-sync-collect U2):notifyComplete 唯一路由入口——
|
|
456
|
+
// async 直通(字节不变)/ sync 批缓冲 + 闭合检测。[U3 接线点] 已接线:flush =
|
|
457
|
+
// manifest 屏障(await 全部落盘)→ notifier.notifyBatch 单条批投递(幂等键
|
|
458
|
+
// sync-batch:<hash> + 闭合触发排程合批 flush:同宏任务去抖窗口收纳背靠背
|
|
459
|
+
// route——U8 拆批盲窗修复 → ledger 写账 → attemptDeliver 边沿投递)+
|
|
460
|
+
// batchFinalized 落标(出口①,见下方闭包注释)。屏障先于写账 = 「通知可达 ⇒
|
|
461
|
+
// 索引就位」的构造性保证(时序竞态修复,见 flushBatch 闭包注释)。
|
|
462
|
+
// [U8] 排程与 E9/E1 交互:dispose 经 convertPendingSyncBufferToAsync 先取消挂起
|
|
463
|
+
// 排程(取消而非同步 flush——双通道并发写账防护,见该函数注释);E1 只在
|
|
464
|
+
// session_start 编排处运行(此前 dispose 已取消排程,补发直走 notifyBatch 不经
|
|
465
|
+
// 协调器),异常时序相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
|
|
466
|
+
// [D4-① 合并] notifier 实例封装在 notifyHost 内(createNotifyHost 装配),本类
|
|
467
|
+
// 经 this.notifyHost.notify / notifyBatch / toNotifyRecord 出口消费。
|
|
468
|
+
this.collectCoordinator = new CollectCoordinator({
|
|
469
|
+
notifyAsync: (record) => {
|
|
470
|
+
const notify = this.notifyHost.toNotifyRecord(record);
|
|
471
|
+
if (notify) this.notifyHost.notify(notify);
|
|
472
|
+
},
|
|
473
|
+
toNotifyRecord: (record) => this.notifyHost.toNotifyRecord(record),
|
|
474
|
+
// 闭合判定数据源:listAllActive(原始 ExecutionRecord 内存态,携带 collectMode/
|
|
475
|
+
// batchFinalized 原始值)。[U3 修正] 原接 collectRecords——其经 recordToSubagent
|
|
476
|
+
// 投影丢 collectMode(U2 披露的投影缺口)→ 真链上闭合判定恒立即闭合、跨轮续累
|
|
477
|
+
// 失效(真链 trace 实证)。非终态 sync 成员必在内存(archive 只删终态;磁盘重建
|
|
478
|
+
// 残留属孤儿恢复域),内存视图语义完整。
|
|
479
|
+
listRecords: (limit) => this.store.listAllActive().slice(0, limit),
|
|
480
|
+
flushBatch: async (members) => {
|
|
481
|
+
// [E9 防御过滤] 排除 dispose 时已转 async 写账的成员(同进程 revive 后协调器
|
|
482
|
+
// 缓冲残留的陈旧快照,见 e9ConvertedIds 字段注释);全被排除 → 空批零副作用
|
|
483
|
+
// 返回(成员已单独写账+落标,无需再动)。
|
|
484
|
+
const live = members.filter((m) => !this.e9ConvertedIds.has(m.id));
|
|
485
|
+
if (live.length === 0) return;
|
|
486
|
+
// 成员全量快照:getFullRecord 冷路径重建(成员在 notifyComplete 前已 archive,
|
|
487
|
+
// 内存无;闭合判定 hasRunningSync 的 listRecords 扫描已建 idToFile 索引,此处
|
|
488
|
+
// 命中)。快照在屏障前一次取定,屏障写与落标共用同一份(manifest 与落标 entry
|
|
489
|
+
// 字段同源)。
|
|
490
|
+
// [S11] getFullRecord 不可达(子 session 文件缺失/已 GC 的窗口)的成员改用
|
|
491
|
+
// 缓冲快照兜底落标,不再跳过——该成员仍随 live 进批写账(成员集 hash 含它),
|
|
492
|
+
// 跳过落标的旧行为会在重启后让 E1 重新收集它(无标记候选),以异成员集 hash
|
|
493
|
+
// 重新补发 → 同成员双投递(异 hash 不触发账本 sync-batch 幂等;旧注释「该
|
|
494
|
+
// 窗口下 E1 本也收不到该成员」不成立:E1 走主 session 文件末条 entry 扫描,
|
|
495
|
+
// 与 getFullRecord 的 manifest/子文件通路相互独立,manifest 屏障 best-effort
|
|
496
|
+
// 写失败同样制造此窗口)。
|
|
497
|
+
const fulls: SubagentRecord[] = [];
|
|
498
|
+
const fullMissed: BgNotifyRecord[] = [];
|
|
499
|
+
for (const m of live) {
|
|
500
|
+
const full = this.store.getFullRecord(m.id);
|
|
501
|
+
if (full) fulls.push(full);
|
|
502
|
+
else fullMissed.push(m);
|
|
503
|
+
}
|
|
504
|
+
// [时序屏障] 成员 manifest 写先于写账并 await 全部落盘——「通知可达 ⇒ 索引
|
|
505
|
+
// 就位」的构造性保证(by construction):批通知的指针行消费依赖
|
|
506
|
+
// records/<sa-id>.json 反查索引,fire-and-forget 下「通知送达时已落盘」只是
|
|
507
|
+
// 大概率成立(探针实测 mtime 相对 notify entry ±2/3ms 方向不定)。写失败仍
|
|
508
|
+
// best-effort(debug 不阻断写账投递,语义与 doFinalizeRecord Step 4 一致)。
|
|
509
|
+
await this.writeSyncBatchManifestBarrier(fulls);
|
|
510
|
+
// 单条批投递:accepted=false(同成员集批已在账——E1 重建重发/重复 flush)或
|
|
511
|
+
// 空批/dispose → 零副作用返回,不落标(设计 §3.1.3 出口①绑「写账成功」)。
|
|
512
|
+
const accepted = this.notifyHost.notifyBatch(live, this.getCollectSyncBudget());
|
|
513
|
+
if (!accepted) return;
|
|
514
|
+
// batchFinalized 纯落标(设计 §3.1.3 两出口之一:批闭合 flush 写账成功后)。
|
|
515
|
+
// collectMode/batchFinalized 显式覆写在 appendBatchFinalizedEntry 内(防非
|
|
516
|
+
// entry 源重建丢标记);末条 entry 带标记 → E1 重建扫描(collectMode=sync 且
|
|
517
|
+
// 无标记才收)据此排除,防双重通知。源序「写账先于落标」不变(v1 幂等窗口
|
|
518
|
+
// 语义);manifest 已在屏障提前写,幂等窗口内崩溃时索引更早已就位。
|
|
519
|
+
for (const full of fulls) {
|
|
520
|
+
this.appendBatchFinalizedEntry(full);
|
|
521
|
+
}
|
|
522
|
+
// [S11] miss 成员兜底落标(缓冲快照 → 最小标记 entry,映射见 sync-rebuild.ts),
|
|
523
|
+
// 与 fulls 同出口①语义(写账成功后统一补标)。
|
|
524
|
+
for (const m of fullMissed) {
|
|
525
|
+
this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? undefined));
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
});
|
|
384
529
|
// #11:注册进程级 observability 单例——ui-request-queue.handleUiRequest 经
|
|
385
530
|
// globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
|
|
386
531
|
// warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
|
|
@@ -389,20 +534,6 @@ export class SubagentService {
|
|
|
389
534
|
|
|
390
535
|
// ── 生命周期(index.ts 调)──────────────────────────────
|
|
391
536
|
|
|
392
|
-
/** 覆盖 UI 请求 handler(W3: index.ts session_start 时按 mode 注入 handler 后调)。
|
|
393
|
-
* 委托 uiObservability 重置缺失告警去重——新 handler 就位后允许重新 warn。 */
|
|
394
|
-
setUiRequestHandler(handler: UiRequestHandler | undefined): void {
|
|
395
|
-
this.uiRequestHandler = handler;
|
|
396
|
-
this.uiObservability.resetMissingHandlerWarnings();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** session-runner handleUiRequest 在 handler 缺失时调用(FR-9 可观测性)。
|
|
400
|
-
* 委托 uiObservability:按 session 去重,同一 session 的多次 UI 请求只 warn 一次。
|
|
401
|
-
* W2: console.warn 兜底。W3 接入 pi.appendEntry("subagent:ui-request-missing-handler", ...)。 */
|
|
402
|
-
notifyMissingHandler(sessionId: string): void {
|
|
403
|
-
this.uiObservability.notifyMissingHandler(sessionId);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
537
|
/** session_start 注入 pi + revive(modelRegistry/entries 归 ModelConfigService.initModel)。 */
|
|
407
538
|
initSession(init: SubagentServiceSessionInit): void {
|
|
408
539
|
this.pi = init.pi;
|
|
@@ -415,10 +546,12 @@ export class SubagentService {
|
|
|
415
546
|
this.mainSessionFile = init.mainSessionFile;
|
|
416
547
|
this.streamSink = init.streamSink ?? null;
|
|
417
548
|
this.isIdleFn = init.isIdle;
|
|
418
|
-
// 读取 mode(W4 守卫透传给 session-runner)+ session 级 handler
|
|
549
|
+
// 读取 mode(W4 守卫透传给 session-runner)+ session 级 handler 覆盖
|
|
550
|
+
//([D4-④] initSession.uiRequestHandler 是唯一注入入口;三态语义见接口注释——
|
|
551
|
+
//null = 显式清空,承载原 setUiRequestHandler(undefined) 语义)。
|
|
419
552
|
this.uiObservability.setMode(init.mode);
|
|
420
553
|
if (init.uiRequestHandler !== undefined) {
|
|
421
|
-
this.uiRequestHandler = init.uiRequestHandler;
|
|
554
|
+
this.uiRequestHandler = init.uiRequestHandler ?? undefined;
|
|
422
555
|
this.uiObservability.resetMissingHandlerWarnings();
|
|
423
556
|
}
|
|
424
557
|
// SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
|
|
@@ -436,8 +569,15 @@ export class SubagentService {
|
|
|
436
569
|
this.initExecContextBaseline(envRoot, init.sessionId);
|
|
437
570
|
// revive(dispose 的逆操作:/resume /fork /new 后复活)
|
|
438
571
|
this._disposed = false;
|
|
572
|
+
// [v2 D4] settled 重扫状态随 revive 重置:新 session 的 E1 若再判「仍有 running」
|
|
573
|
+
// 可重新注册。旧 handler 闭包捕获旧 state:正常时序(session_shutdown →
|
|
574
|
+
// session_start)下已随 dispose() 惰化;未经 dispose 的时序残留仍会在 settled
|
|
575
|
+
// 边沿执行——其扫描 this.mainSessionFile 当前值(非注册时的旧文件),行为等价于
|
|
576
|
+
// 新 session 多注册一次扫描,由账本 sync-batch:<hash> 幂等 + batchFinalized 候选
|
|
577
|
+
// 过滤收敛,无跨 session 污染面。
|
|
578
|
+
this.settledRescanState = null;
|
|
439
579
|
this.store.revive();
|
|
440
|
-
this.
|
|
580
|
+
this.notifyHost.revive();
|
|
441
581
|
// 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
|
|
442
582
|
// sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
|
|
443
583
|
this.recoverOrphansIfRootProcess();
|
|
@@ -462,7 +602,7 @@ export class SubagentService {
|
|
|
462
602
|
|
|
463
603
|
/**
|
|
464
604
|
* [递归可见性] exec 上下文基线:子进程读 env PI_SUBAGENT_SELF_RECORD_ID / DEPTH
|
|
465
|
-
* 建立身份基线后,createRecordForMode
|
|
605
|
+
* 建立身份基线后,createRecordForMode 读嵌套上下文自动正确(孙挂到子名下)。
|
|
466
606
|
* enterWith 贯穿整个 session 生命周期(与 forkDepthAls 同构,决策 4)。
|
|
467
607
|
*/
|
|
468
608
|
private initExecContextBaseline(envRoot: string | undefined, sessionId: string): void {
|
|
@@ -470,13 +610,14 @@ export class SubagentService {
|
|
|
470
610
|
if (envSelfRecord !== undefined && envSelfRecord !== "") {
|
|
471
611
|
const envNestingDepth = Number.parseInt(process.env[ENV_DEPTH] ?? "0", 10);
|
|
472
612
|
const nestingDepth = Number.isNaN(envNestingDepth) ? 0 : envNestingDepth;
|
|
473
|
-
// [ALS 断裂修复] 基线兜底:enterWith 在 pi
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
this.
|
|
613
|
+
// [ALS 断裂修复] 基线兜底:enterWith 在 pi 事件回调模型下不可靠(机制注释见
|
|
614
|
+
// common/nesting-guard.ts ExecutionNestingContext),基线是 createRecordForMode /
|
|
615
|
+
// 护栏读 ALS store 失败时的权威回退。
|
|
616
|
+
this.execNesting.setBaseline({ recordId: envSelfRecord, depth: nestingDepth });
|
|
617
|
+
this.execNesting.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
|
|
477
618
|
if (process.env.XYZ_AGENT_DEBUG) {
|
|
478
619
|
logger.debug(
|
|
479
|
-
`[subagents]
|
|
620
|
+
`[subagents] execNesting initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? sessionId}`,
|
|
480
621
|
);
|
|
481
622
|
}
|
|
482
623
|
}
|
|
@@ -493,7 +634,7 @@ export class SubagentService {
|
|
|
493
634
|
*(hours-long wave 必然命中)→ 活记录被无关进程盖 .finalized sidecar,closed entry
|
|
494
635
|
* 写进别的进程的 session 文件(跨进程互写,无任何锁)。子进程身份判据 = env
|
|
495
636
|
* PI_SUBAGENT_SELF_RECORD_ID(父 spawn 时注入的「子进程自己的 record id」,仅子进程
|
|
496
|
-
* 非空)——与
|
|
637
|
+
* 非空)——与 execNesting 基线同源。根进程恢复语义不变。
|
|
497
638
|
*/
|
|
498
639
|
private recoverOrphansIfRootProcess(): void {
|
|
499
640
|
const isChildProcess = (process.env[ENV_SELF_RECORD_ID] ?? "") !== "";
|
|
@@ -504,23 +645,26 @@ export class SubagentService {
|
|
|
504
645
|
}
|
|
505
646
|
}
|
|
506
647
|
|
|
507
|
-
/** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords
|
|
508
|
-
* private 封装——与 recoverManifestTmpFiles
|
|
648
|
+
/** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
|
|
649
|
+
* private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
|
|
650
|
+
* 是 initSession,转 private)。判定语义见 store 侧注释。
|
|
651
|
+
* mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
|
|
652
|
+
* 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
|
|
509
653
|
* 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
|
|
510
654
|
* E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。 */
|
|
511
|
-
recoverOrphanRecords(): void {
|
|
655
|
+
private recoverOrphanRecords(): void {
|
|
512
656
|
try {
|
|
513
|
-
this.store.recoverOrphanRecords(this.sessionRootId ?? undefined);
|
|
657
|
+
this.store.recoverOrphanRecords(this.sessionRootId ?? undefined, this.mainSessionFile);
|
|
514
658
|
} catch (err) {
|
|
515
659
|
logger.warn("[subagents] orphan recovery failed", {
|
|
516
|
-
reason:
|
|
660
|
+
reason: toErrorMessage(err),
|
|
517
661
|
});
|
|
518
662
|
}
|
|
519
663
|
try {
|
|
520
664
|
this.store.recoverEntryOnlyOrphans(this.mainSessionFile, this.sessionRootId ?? undefined);
|
|
521
665
|
} catch (err) {
|
|
522
666
|
logger.warn("[subagents] entry-only orphan recovery failed", {
|
|
523
|
-
reason:
|
|
667
|
+
reason: toErrorMessage(err),
|
|
524
668
|
});
|
|
525
669
|
}
|
|
526
670
|
}
|
|
@@ -594,7 +738,7 @@ export class SubagentService {
|
|
|
594
738
|
});
|
|
595
739
|
}
|
|
596
740
|
// pending-notifications 注销
|
|
597
|
-
emitPendingUnregister(
|
|
741
|
+
this.notifyHost.emitPendingUnregister(record.id, "closed");
|
|
598
742
|
count++;
|
|
599
743
|
}
|
|
600
744
|
return count;
|
|
@@ -634,35 +778,309 @@ export class SubagentService {
|
|
|
634
778
|
* listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
|
|
635
779
|
* 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
|
|
636
780
|
* 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
|
|
781
|
+
/** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
|
|
782
|
+
* 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
|
|
783
|
+
* → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
|
|
784
|
+
* sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
|
|
785
|
+
private getCollectSyncBudget(): BatchBudgetParams | undefined {
|
|
786
|
+
const cs = this.collectSyncSection();
|
|
787
|
+
return cs !== undefined ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : undefined;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** [E9 专用] batchFinalized 落标 + manifest fire-and-forget 写(设计 §3.1.5 E9)。
|
|
791
|
+
* 批通知路径(flush/E1)已改走「manifest 屏障 → 写账 → 纯落标」序列(「通知可达
|
|
792
|
+
* ⇒ 索引就位」的构造性保证,见 flushBatch 闭包 / runSyncCollectRecoveryScan),
|
|
793
|
+
* 不再经本 helper;仅 E9 转换的成员保持原形态——其走 async 单条通知(全文注入、
|
|
794
|
+
* 无指针行消费),manifest 无时序要求,落标后 fire-and-forget 补写(list 后手动
|
|
795
|
+
* 反查的顺带索引)。
|
|
796
|
+
* 路径 = getFullRecord 冷路径重建 → appendBatchFinalizedEntry 纯落标 → fire
|
|
797
|
+
* manifest。getFullRecord 不可达(子 session 文件缺失/已 GC)→ 跳过该成员
|
|
798
|
+
* (详见 flushBatch 闭包注释)。 */
|
|
799
|
+
private markMembersBatchFinalized(memberIds: readonly string[]): void {
|
|
800
|
+
for (const id of memberIds) {
|
|
801
|
+
const full = this.store.getFullRecord(id);
|
|
802
|
+
if (!full) continue;
|
|
803
|
+
this.appendBatchFinalizedEntry(full);
|
|
804
|
+
// 反查索引缺失只影响指针行反查(session-reader 错误文案已指引绝对路径兜底),
|
|
805
|
+
// 不构成落标失败(与屏障路径的 best-effort 语义同源,仅无时序保证)。
|
|
806
|
+
void this.writeBatchMemberManifest(full).catch((err: unknown) => {
|
|
807
|
+
logger.debug(
|
|
808
|
+
`[subagents] batch-finalized manifest write failed (record=${full.id})`,
|
|
809
|
+
{ reason: err instanceof Error ? err.message : String(err) },
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** batchFinalized 落标唯一出口(appendEntry 公共末步,纯落标):显式覆写
|
|
816
|
+
* collectMode/batchFinalized → reportSubagentRecord。覆写动机:recordToSubagent
|
|
817
|
+
* 投影已含两字段(U5 修复),但 getFullRecord 冷路径含 sidecar/manifest 重建分支
|
|
818
|
+
* (非 entry 源),显式赋值防非 entry 源重建时丢标记。
|
|
819
|
+
*
|
|
820
|
+
* [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
|
|
821
|
+
* 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
|
|
822
|
+
* records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
|
|
823
|
+
* 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
|
|
824
|
+
* await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
|
|
825
|
+
* fire-and-forget(async 单条通知无指针行消费,无时序要求)。
|
|
826
|
+
* rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
|
|
827
|
+
* 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
|
|
828
|
+
* task/slug/parentRecordId 等可选 undefined 自然缺省。 */
|
|
829
|
+
private appendBatchFinalizedEntry(rec: SubagentRecord): void {
|
|
830
|
+
this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** 批成员 manifest(sa- id → sessionFile 反查索引)写的唯一投影点(D2 字段投影 +
|
|
834
|
+
* status 如实投影:成功成员此刻 record 实态 running+resumable → "running",后续
|
|
835
|
+
* message upgrade 走完整 finalize 时 Step 4 原子覆盖为 "closed")。
|
|
836
|
+
* 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
|
|
837
|
+
* writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
|
|
838
|
+
* fire-and-forget catch(debug 不阻断落标)。 */
|
|
839
|
+
private writeBatchMemberManifest(rec: SubagentRecord): Promise<void> {
|
|
840
|
+
return this.manifestStore.writeManifest({
|
|
841
|
+
id: rec.id,
|
|
842
|
+
rootSessionId: rec.rootSessionId ?? "",
|
|
843
|
+
parentRecordId: rec.parentRecordId,
|
|
844
|
+
agentName: rec.agent,
|
|
845
|
+
status: rec.status,
|
|
846
|
+
createdAt: rec.startedAt,
|
|
847
|
+
completedAt: rec.endedAt,
|
|
848
|
+
sessionFile: rec.sessionFile,
|
|
849
|
+
task: rec.task,
|
|
850
|
+
slug: rec.slug,
|
|
851
|
+
model: rec.model,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** [时序屏障] 批通知路径(flush/E1)专用:成员 manifest 并行写 + await 全部完成
|
|
856
|
+
* (allSettled)后才允许写账投递——「通知可达 ⇒ 索引就位」的构造性保证。写失败
|
|
857
|
+
* 不阻断(best-effort 语义与 doFinalizeRecord Step 4 一致:反查索引缺失只影响指针行
|
|
858
|
+
* 反查,session-reader 错误文案已指引绝对路径兜底,不构成写账失败);warn 留痕
|
|
859
|
+
* (D6 #7a / SC-1:屏障失败意味着该成员指针行反查索引缺失,debug 级在排障时不可见)。 */
|
|
860
|
+
private async writeSyncBatchManifestBarrier(recs: readonly SubagentRecord[]): Promise<void> {
|
|
861
|
+
const results = await Promise.allSettled(recs.map((rec) => this.writeBatchMemberManifest(rec)));
|
|
862
|
+
for (let i = 0; i < results.length; i++) {
|
|
863
|
+
const result = results[i]!;
|
|
864
|
+
if (result.status === "rejected") {
|
|
865
|
+
logger.warn(
|
|
866
|
+
`[subagents] batch-finalized manifest write failed (record=${recs[i]!.id}, manifest=${this.recordsDir}/${recs[i]!.id}.json)`,
|
|
867
|
+
{ reason: result.reason instanceof Error ? result.reason.message : String(result.reason) },
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** [E9] dispose 时批未闭合:缓冲中已终态未通知成员逐条转 async 语义写账(放弃攒批)
|
|
874
|
+
* + 落 batchFinalized 标记(E1 重建扫描据此排除,防双重通知),交由既有 shutdown
|
|
875
|
+
* flush / resume 重放兑底;仍在跑的成员走现有退出路径(disposeAllRecords 关闭,
|
|
876
|
+
* 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
|
|
877
|
+
* 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
|
|
878
|
+
* 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
|
|
879
|
+
private convertPendingSyncBufferToAsync(): void {
|
|
880
|
+
// [U8 拆批修复·E9 交互] 挂起的合批排程先取消——dispose 已选「放弃攒批转 async」
|
|
881
|
+
// 语义(最简语义 = 取消而非同步 flush):放任排程触发会让 flushBatch(批 hash
|
|
882
|
+
// 通道)与下方逐条 notify(async id 通道)双通道并发写账 → 同成员双投递,且触发
|
|
883
|
+
// 点可能落在 notifier/store dispose 之后。取消后缓冲原样保留,本函数既有单通路
|
|
884
|
+
// 完整接管(协调器侧幂等:无排程时 no-op)。
|
|
885
|
+
this.collectCoordinator.cancelScheduledFlush();
|
|
886
|
+
const members = this.collectCoordinator.pendingMembers();
|
|
887
|
+
if (members.length === 0) return;
|
|
888
|
+
for (const member of members) {
|
|
889
|
+
this.e9ConvertedIds.add(member.id);
|
|
890
|
+
this.notifyHost.notify(member);
|
|
891
|
+
}
|
|
892
|
+
this.markMembersBatchFinalized(members.map((m) => m.id));
|
|
893
|
+
logger.warn(
|
|
894
|
+
`[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
|
|
895
|
+
{ ids: members.map((m) => m.id) },
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* [E1] sync 批崩溃恢复钩子(设计 §3.1.5 E1,index.ts session_start 恢复编排处调用,
|
|
901
|
+
* 须晚于 initSession——孤儿终态恢复先行收敛 running 成员,「全员终态」判定才可达):
|
|
902
|
+
*
|
|
903
|
+
* - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
|
|
904
|
+
* collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
|
|
905
|
+
* 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
|
|
906
|
+
* - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
|
|
907
|
+
* 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
|
|
908
|
+
* - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
|
|
909
|
+
* 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
|
|
910
|
+
* entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
|
|
911
|
+
* 「已处理」);
|
|
912
|
+
* - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
|
|
913
|
+
* 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
|
|
914
|
+
* 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
|
|
915
|
+
* running 口径与协调器同构
|
|
916
|
+
* (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
|
|
917
|
+
* - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
|
|
918
|
+
* 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
|
|
919
|
+
* 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
|
|
920
|
+
*
|
|
921
|
+
* async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
|
|
922
|
+
* session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
|
|
923
|
+
* 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
|
|
924
|
+
*
|
|
925
|
+
* [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
|
|
926
|
+
* dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
|
|
927
|
+
* 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
|
|
928
|
+
*/
|
|
929
|
+
async recoverSyncCollectBatch(): Promise<void> {
|
|
930
|
+
try {
|
|
931
|
+
const { outcome } = await this.runSyncCollectRecoveryScan();
|
|
932
|
+
// [v2 D4] 断链 4:等待分支不再死等——挂 settled 有界重扫(幂等单注册)。
|
|
933
|
+
if (outcome === "waiting") {
|
|
934
|
+
this.armSettledRescan();
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
// 与 index.ts 调用点原 try/catch 的 warn 容错同语义(该处 catch 对 async 化后
|
|
938
|
+
// 的 promise 异常兑现不到,容错收敛到本方法内部)。
|
|
939
|
+
logger.warn("[subagents] sync collect batch recovery failed", {
|
|
940
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** [E1/D4] 单次「扫描→判定→可达则补发+落标」,E1 首扫与 settled 重扫共用同一实现
|
|
946
|
+
* (防两处复制粘贴分岔)。三态返回:idle(无 sync 候选——已全部落标/E9 转换/异根,
|
|
947
|
+
* 无事可等)/ waiting(仍有 running 成员,本次不动)/ dispatched(全员终态,已补发
|
|
948
|
+
* +统一落标)。async:补发前有 manifest 屏障 await(见函数头 E1 注释)。 */
|
|
949
|
+
/** 返回 outcome + waitingIds(达限 warn 需滞留成员 id,D6 #7b——仅 waiting 态非空)。 */
|
|
950
|
+
private async runSyncCollectRecoveryScan(): Promise<{
|
|
951
|
+
outcome: "idle" | "waiting" | "dispatched";
|
|
952
|
+
waitingIds: string[];
|
|
953
|
+
}> {
|
|
954
|
+
const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
|
|
955
|
+
if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
956
|
+
const rootFilter = this.sessionRootId;
|
|
957
|
+
const candidates = lastRecords.filter(
|
|
958
|
+
(r) =>
|
|
959
|
+
r.collectMode === "sync" &&
|
|
960
|
+
r.batchFinalized !== true &&
|
|
961
|
+
(rootFilter === undefined || r.rootSessionId === rootFilter),
|
|
962
|
+
);
|
|
963
|
+
if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
964
|
+
// [v2 D3] 与协调器 hasRunningSync 同构口径(collect-coordinator.ts):running+
|
|
965
|
+
// resumable 视为已完成、不阻止补发——成功成员崩溃时的末条 entry 恒为轮终
|
|
966
|
+
// running+resumable(SP-5 有意语义),旧口径只看 status !== "closed" 会把主场景
|
|
967
|
+
// (批内含成功成员)顶死在「等自然终态」永不补发(v2 §2.3 断链 3)。
|
|
968
|
+
const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
|
|
969
|
+
if (running.length > 0) {
|
|
970
|
+
logger.debug(
|
|
971
|
+
`[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
|
|
972
|
+
{ ids: running.map((r) => r.id) },
|
|
973
|
+
);
|
|
974
|
+
return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
|
|
975
|
+
}
|
|
976
|
+
// [时序屏障] manifest 先于写账 await 全部落盘——E1 补发同样是批通知(指针行消费
|
|
977
|
+
// 依赖反查索引),「通知可达 ⇒ 索引就位」的构造性保证与 flushBatch 同款。
|
|
978
|
+
await this.writeSyncBatchManifestBarrier(candidates);
|
|
979
|
+
// 全员终态:单条批补发(budget 热读与 flushBatch 同源);账本同 hash 幂等拒绝也算
|
|
980
|
+
// 已投递(批已在账/已销账,重放由账本承接)——两种结局统一补标。
|
|
981
|
+
const members = candidates.map((r) => syncRebuildToNotifyMember(r));
|
|
982
|
+
const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
|
|
983
|
+
for (const rec of candidates) {
|
|
984
|
+
this.appendBatchFinalizedEntry(rec);
|
|
985
|
+
}
|
|
986
|
+
logger.warn(
|
|
987
|
+
`[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
|
|
988
|
+
{ ids: members.map((m) => m.id) },
|
|
989
|
+
);
|
|
990
|
+
return { outcome: "dispatched", waitingIds: [] };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** [v2 D4] 注册 agent_settled 有界重扫(幂等:settledRescanState 非 null 不叠加注册
|
|
994
|
+
* ——E1 现仅 session_start 单调用点,守卫是防第二入口引入时的注册叠加断言面)。
|
|
995
|
+
* 每次 settled 边沿重跑同一 E1 扫描(runSyncCollectRecoveryScan):dispatched
|
|
996
|
+
* (补发+落标完成,scan 的 warn 已留痕)或 idle(候选已被其他通路落标)→ disposed;
|
|
997
|
+
* 累计 SETTLED_RESCAN_LIMIT 次仍在等 → disposed + warn 留痕(D6 #7b / SC-2:达限
|
|
998
|
+
* 放弃重扫意味着滞留成员的批通知要等下次 session_start 才收敛,含滞留 id 的 warn
|
|
999
|
+
* 是唯一线索,debug 级排障不可见;后续事件零处理,下次 session_start 再收敛)。
|
|
1000
|
+
* pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
|
|
1001
|
+
* index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
|
|
1002
|
+
* pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
|
|
1003
|
+
* extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
|
|
1004
|
+
* extension 的全部 handler 逐一 await;故本注册与 ledger host 经
|
|
1005
|
+
* piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
|
|
1006
|
+
private armSettledRescan(): void {
|
|
1007
|
+
if (this.settledRescanState !== null) return;
|
|
1008
|
+
const state = { disposed: false, scans: 0 };
|
|
1009
|
+
this.settledRescanState = state;
|
|
1010
|
+
this.pi?.on?.("agent_settled", async () => {
|
|
1011
|
+
if (state.disposed) return;
|
|
1012
|
+
state.scans += 1;
|
|
1013
|
+
// await 完整补发序列(manifest 屏障 → 写账 → 落标):pi emit 对 handler 逐一
|
|
1014
|
+
// await(P-settled 定谳,0.84.4 dist runner.js),async 化不改变分发语义。
|
|
1015
|
+
const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
|
|
1016
|
+
if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
|
|
1017
|
+
state.disposed = true;
|
|
1018
|
+
if (outcome === "waiting") {
|
|
1019
|
+
// D6 #7b / SC-2:warn + 滞留成员 id(debug 级排障不可见——达限即批通知挂起至下次 session_start)
|
|
1020
|
+
logger.warn(
|
|
1021
|
+
`[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
|
|
1022
|
+
{ ids: waitingIds },
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// [E1 语义对齐 toNotifyRecord] 补发成员映射 syncRebuildToNotifyMember 拆至
|
|
1029
|
+
// sync-rebuild.ts(变化轴:恢复批通知语义):one-shot 成功成员末条恒
|
|
1030
|
+
// running+resumable(SP-5),直通 status 会让恢复批批头「0 finished」且丢
|
|
1031
|
+
// patchFile 的 git-apply 指针——对齐后补发记录为 closed + outcome 物化 +
|
|
1032
|
+
// patchFile 透传。调用点:runSyncCollectRecoveryScan。
|
|
1033
|
+
|
|
637
1034
|
dispose(): void {
|
|
638
1035
|
if (this._disposed) return;
|
|
639
1036
|
this._disposed = true;
|
|
640
1037
|
this.stopGcTimer();
|
|
1038
|
+
// [v2 D4] settled 重扫 handler 惰化:dispose 后 trailing settled 边沿若仍触发,
|
|
1039
|
+
// 旧 handler 不得再跑扫描——notifier 随后将 dispose,notifyBatch 短路返回 false
|
|
1040
|
+
// 且不写账,而 E1 dispatched 段不判 accepted 仍统一落标 → 批被标 batchFinalized
|
|
1041
|
+
// 而通知从未写账(永久丢失,不可逆)。惰化后通知由下次重启的 E1 首扫兑现
|
|
1042
|
+
// (成员无标记,候选可达)。与 initSession revive 重置不冲突:dispose 是终态置
|
|
1043
|
+
// disposed,revive 置 null 是新 session 的重新注册,旧 state 对象随旧 handler
|
|
1044
|
+
// 闭包保持 disposed 永久惰化。
|
|
1045
|
+
if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
|
|
641
1046
|
// [dispose stub] 第一时间换 stub,防 trailing ui_request 调到 stale handler 闭包
|
|
642
1047
|
// (仍持有 disposed session 的 ctx)产生误导性 console.error。stub 干净降级为 cancelled。
|
|
643
1048
|
// 必须在 emit/abort 之前——这些步骤可能同步触发 trailing pump。
|
|
644
|
-
|
|
1049
|
+
// [D4-④] 原 setUiRequestHandler 方法已删(initSession 参数为唯一注入入口),
|
|
1050
|
+
// 此处内联其方法体(赋值 + 缺失告警去重重置)。
|
|
1051
|
+
this.uiRequestHandler = disposedUiRequestStub;
|
|
1052
|
+
this.uiObservability.resetMissingHandlerWarnings();
|
|
645
1053
|
// [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
|
|
646
1054
|
// abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
|
|
647
1055
|
this.store.abortRunningControllers();
|
|
648
1056
|
killAllSpawnedChildren();
|
|
1057
|
+
// [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
|
|
1058
|
+
// 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
|
|
1059
|
+
// running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
|
|
1060
|
+
// 冷 idToFile(无目录扫描则 miss → 跳过落标 → E1 重建误收已转换成员);先转换取
|
|
1061
|
+
// 内存命中,与 flushBatch 出口①同款通路。同样在 flushPendingNotifications 之前
|
|
1062
|
+
//(转换条目加入本次 flush)与 notifier/store dispose 之前(写账与 appendEntry
|
|
1063
|
+
// 通道仍可用)。仍在跑成员不在此处理——后续 disposeAllRecords 按现有退出路径关闭。
|
|
1064
|
+
this.convertPendingSyncBufferToAsync();
|
|
649
1065
|
// SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
|
|
650
1066
|
// 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
|
|
651
1067
|
this.disposeAllRecords("parent-shutdown");
|
|
652
1068
|
// [v4 A-1] EPIPE 连续失败计数器清零(计数器已迁移到 stdin-writer,防跨 session 泄漏)
|
|
653
1069
|
resetAllEpipeFailures();
|
|
654
|
-
// [review MF1] 在途 resume
|
|
1070
|
+
// [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
|
|
655
1071
|
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
656
1072
|
this.resumesInFlight.clear();
|
|
1073
|
+
// chat 轮次交接包清空(正常由 PiEngine.run 消费;此处兜底 kill 后仍挂着的条目)
|
|
1074
|
+
this.chatRoundTickets.clear();
|
|
657
1075
|
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
658
|
-
this.
|
|
1076
|
+
this.notifyHost.flushPendingNotifications();
|
|
659
1077
|
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
660
1078
|
// 复查失败时 pending 挂在 ledger 内存态,随后 dispose 销毁——旧注释「flush 后 dispose
|
|
661
|
-
|
|
1079
|
+
//(防丢失)」的承诺与行为不符。现把未投递 pending 经 ledger entry 通道复写落盘
|
|
662
1080
|
// (notifyId 幂等,重复 entry 由恢复扫描后写覆盖吸收),重启 recoverFromSession 按
|
|
663
1081
|
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
664
1082
|
this.persistUndeliveredNotificationsForReplay();
|
|
665
|
-
this.
|
|
1083
|
+
this.notifyHost.dispose();
|
|
666
1084
|
this.store.dispose();
|
|
667
1085
|
}
|
|
668
1086
|
|
|
@@ -701,108 +1119,9 @@ export class SubagentService {
|
|
|
701
1119
|
|
|
702
1120
|
// ── 执行(subagent-tool 调)────────────────────────────
|
|
703
1121
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
* SP-1: closed 统一终态(done/failed/crashed 合并),closedReason 携带 L2 原因。 */
|
|
708
|
-
private notifyComplete(record: ExecutionRecord): void {
|
|
709
|
-
const notify = this.toNotifyRecord(record);
|
|
710
|
-
if (notify) this.notifier.notify(notify);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/** [C-1] chatMode close 终态通知(设计 D2:正文空/本轮增量 + sessionFile 指针行)。
|
|
714
|
-
*
|
|
715
|
-
* 与 notifyComplete 的差异只在 dedup 身份与轮次统计:终态通知必须与最后一轮的轮次通知
|
|
716
|
-
* 区分(轮次通知 key=`id:round`),否则同 key 被 60s dedup 吞——close 后父 agent 永远
|
|
717
|
-
* 收不到带指针行的终态通知(审查 C-1)。故 round 置 undefined(key 回退为裸 id),
|
|
718
|
-
* 轮数改经 totalRounds 进文案 "completed after N rounds."(C-2)。
|
|
719
|
-
*
|
|
720
|
-
* 仅 chatMode close 语义调用(closeChatIdle / closeAfterRoundSettled 终态化成功后)。
|
|
721
|
-
* one-shot 显式拒绝(G4:one-shot close 路径现状无终态通知,字节不变);cancel 走
|
|
722
|
-
* cancelBackground 自己的 notifyComplete,不经本方法。幂等性:两条 close 路径均由
|
|
723
|
-
* closeSubagent 的 status 分流守卫(closed 后幂等 no-op)/ CAS 抢锁保证只执行一次,
|
|
724
|
-
* 本方法自身不重复发送;迟到的 kickOffBackground.then 通知与轮次通知同 key=`id:round`,
|
|
725
|
-
* 60s 窗内仍被吞,不构成第三条。 */
|
|
726
|
-
/** @param emptyBody true = 终态通知正文置空串(D2 路径②)。W16 P-1 修复后
|
|
727
|
-
* closeChatIdle 的 doneResult.text 改用 record.result 保真(close 终态
|
|
728
|
-
* subagent-record entry 的 result 不抹空轮终真实值),「正文空」不再由合成空
|
|
729
|
-
* text 的副作用承载,改为显式参数——持久化 result 与通知正文两个关注点解耦。 */
|
|
730
|
-
private notifyClosed(record: ExecutionRecord, emptyBody = false): void {
|
|
731
|
-
if (!record.chatMode) return;
|
|
732
|
-
const notify = this.toNotifyRecord(record);
|
|
733
|
-
if (!notify) return;
|
|
734
|
-
notify.round = undefined;
|
|
735
|
-
if (emptyBody) notify.result = "";
|
|
736
|
-
if (record.round != null) notify.totalRounds = record.round;
|
|
737
|
-
this.notifier.notify(notify);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/** notifier 的 NotifierHost 适配器(绑定到 pi.sendMessage + store 查询)。 */
|
|
741
|
-
private piAdapter(): NotifierHost {
|
|
742
|
-
return {
|
|
743
|
-
sendMessage: (message, options) => {
|
|
744
|
-
this.pi?.sendMessage(message, options);
|
|
745
|
-
},
|
|
746
|
-
hasRunningBackground: () => {
|
|
747
|
-
// [M3] 「在跑的 background 工作」= 有活进程且非等待续聊(idle timer armed)。
|
|
748
|
-
// v4 B-1 把旧 idle 折入 running 且 record 留 store:轮次完成的 chatMode record
|
|
749
|
-
// (timer armed、无在跑轮)与 one-shot 完成后等待 message 升级的 record 都不再计入。
|
|
750
|
-
// 旧判定 `mode === "background"` 对这两类恒 true → 轮次完成通知恒挂 60s 合并窗口
|
|
751
|
-
// (notifier MERGE_WINDOW_MS),主 agent 的续聊回复固定延迟 60s 送达,持续对话(G1)失效。
|
|
752
|
-
return this.store.listRunning().some(
|
|
753
|
-
(r) => r.mode === "background" && hasLiveProcessHandle(r.id) && !hasIdleTimer(r.id),
|
|
754
|
-
);
|
|
755
|
-
},
|
|
756
|
-
isIdle: () => this.isIdleFn?.() ?? true,
|
|
757
|
-
// [must-fix #4 / D8] settled 边沿订阅,与 isIdle 同源(session_start 注入的 pi)。
|
|
758
|
-
// 只注入原生订阅能力;disposed 标志包装(退订语义)在 notifier 的 port 装配完成。
|
|
759
|
-
onAgentSettled: (handler) => { this.pi?.on?.("agent_settled", handler); },
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/** record → BgNotifyRecord(notifier.notify 入参映射,内部不外露)。
|
|
764
|
-
* v4 B-1:守卫放行 closed(终态,含 cancelled)、isIdle(对话模式轮次完成,notify 主 agent G1)
|
|
765
|
-
* 或 isResumable(running + 无活进程——SP-5 one-shot 成功完成 / MF-6 失败轮回退)。
|
|
766
|
-
* 正在执行(running + 活进程 + 非 timer-armed)返回 undefined(调用方 notifyComplete 跳过)。
|
|
767
|
-
* SP-1: closed 统一终态,closedReason 由 BgNotifyRecord 携带。 */
|
|
768
|
-
private toNotifyRecord(record: ExecutionRecord): BgNotifyRecord | undefined {
|
|
769
|
-
const snap = snapshot(record);
|
|
770
|
-
const s = snap.status;
|
|
771
|
-
// [N1] isResumable 放行:SP-5 one-shot 成功完成后 finalizeRoundToIdle 把 record 回退
|
|
772
|
-
// running-resumable——进程已死且永不 arm idle timer(armIdleTimer 只在 agent_settled 的
|
|
773
|
-
// chatMode 分支调用),旧守卫(closed / isIdle only)对其恒拒绝 → 完成通知静默丢失,
|
|
774
|
-
// 而 one-shot 失败走 finalizeRecord 保持 closed 反而通知——与 tool 契约「runs once,
|
|
775
|
-
// notifies on completion」完全倒置。isResumable = running + 无活进程,恰为该完成态;
|
|
776
|
-
// 在跑轮的 record 有活进程,不会被误放行。
|
|
777
|
-
if (s !== "closed" && !isIdle(record) && !isResumable(record)) return undefined;
|
|
778
|
-
// closed → BgNotifyRecord.closed(cancelled 区分靠 closedReason);chatMode 的 isIdle/
|
|
779
|
-
// isResumable(轮次完成或 MF-6 失败轮回退,对话可续)→ running(轮次完成)。
|
|
780
|
-
// isResumable 且非 chatMode(SP-5 one-shot 成功完成)→ closed:对主 agent 的语义是
|
|
781
|
-
// completed(非对话轮次),且只有 closed 分支文案携带 worktree patchFile 的 git apply
|
|
782
|
-
// 提示——one-shot worktree 模式的改动回收依赖该提示(running 分支文案不含 patchFile)。
|
|
783
|
-
const notifyStatus: BgNotifyRecord["status"] =
|
|
784
|
-
s === "closed" || !record.chatMode ? "closed" : "running";
|
|
785
|
-
return {
|
|
786
|
-
id: snap.id,
|
|
787
|
-
status: notifyStatus,
|
|
788
|
-
agent: snap.agent,
|
|
789
|
-
model: snap.model,
|
|
790
|
-
result: snap.result,
|
|
791
|
-
error: snap.error,
|
|
792
|
-
startedAt: snap.startedAt,
|
|
793
|
-
endedAt: snap.endedAt,
|
|
794
|
-
patchFile: record.patchFile,
|
|
795
|
-
// round 透传给 notifier 的 dedup key(对话模式按轮次去重,G1 决策 9)。
|
|
796
|
-
round: record.round,
|
|
797
|
-
// SP-1: closedReason 透传给 notifier(L2 原因,供通知文案按需展示)。
|
|
798
|
-
closedReason: record.closedReason,
|
|
799
|
-
// [wave2] chatMode 条件透传 sessionFile:通知末尾追加 Full transcript 指针行
|
|
800
|
-
//(增量语义的全文恢复通道,见 notifier.buildLlmContent)。one-shot(chatMode
|
|
801
|
-
// falsy)不透传——通知输出逐字节不变(G4),该条件由 message-close 测试的
|
|
802
|
-
// 必选用例锁死(漏加条件时 notifier 单测不红——notifier 层只见最终字段)。
|
|
803
|
-
sessionFile: record.chatMode ? record.sessionFile : undefined,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
1122
|
+
// [D4-①] 通知簇四方法(notifyComplete / notifyClosed / piAdapter / toNotifyRecord)
|
|
1123
|
+
// 与 emitPendingRegister / emitPendingUnregister 模块函数已整体搬移至 notify-host.ts
|
|
1124
|
+
//(本类经 this.notifyHost 消费;行为逐字节等价,搬移 + 依赖注入)。
|
|
806
1125
|
|
|
807
1126
|
/**
|
|
808
1127
|
* 预解析 model(renderCall 标题行用,同步)。代理 modelService.resolveModel。
|
|
@@ -828,13 +1147,14 @@ export class SubagentService {
|
|
|
828
1147
|
// [T4② / PS-4] idleTimeoutMs 配置错误在首个副作用前同步 fail-fast(错误含合法范围)。
|
|
829
1148
|
this.assertIdleTimeoutMsSafe(opts);
|
|
830
1149
|
|
|
831
|
-
// 通用嵌套深度护栏(D-033
|
|
832
|
-
//
|
|
833
|
-
//
|
|
1150
|
+
// 通用嵌套深度护栏(D-033):嵌套上下文([D3-⑤] 公共层 ExecutionNestingContext)
|
|
1151
|
+
// 记录所有 subagent 嵌套层级(fork + 非 fork),每层 +1。MAX_FORK_DEPTH 同时限
|
|
1152
|
+
// fork 链与通用嵌套——非 fork 递归虽不累积 session 体积,但耗资源且 LLM 易陷入
|
|
1153
|
+
// 「委派→再委派」死循环。在所有副作用之前拦截,错误直达调用方。
|
|
834
1154
|
// 计数基准:顶层 nestingDepth=0,nestingDepth>MAX 被拒。与 fork 体积护栏(parentForkDepth 检查)
|
|
835
1155
|
// 互补:本护栏更严(计所有嵌套),混合链下先生效;两者共享 MAX_FORK_DEPTH 上限不漂移。
|
|
836
|
-
// [ALS 断裂修复]
|
|
837
|
-
const parentNesting = this.
|
|
1156
|
+
// [ALS 断裂修复] current() 内含基线兜底(pi 事件回调模型下 enterWith 不贯穿)。
|
|
1157
|
+
const parentNesting = this.execNesting.current();
|
|
838
1158
|
const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
|
|
839
1159
|
if (nestingDepth > MAX_FORK_DEPTH) {
|
|
840
1160
|
throw new ForkDepthExceededError(
|
|
@@ -844,92 +1164,41 @@ export class SubagentService {
|
|
|
844
1164
|
|
|
845
1165
|
// mode 固定 background(sync 模式已删除)
|
|
846
1166
|
const mode: ExecutionMode = "background";
|
|
847
|
-
const ctx = this.buildSessionRunnerContext(opts.cwd);
|
|
848
1167
|
|
|
849
|
-
// ── 1. IDENTITY
|
|
850
|
-
|
|
1168
|
+
// ── 1. IDENTITY 前置解析:agentConfig(agent .md 加载)保持在最前 ──
|
|
1169
|
+
// [u-h2 D2-1] 路由先行:model 解析从「路由之前」移到「路由之后、按目标引擎分支」
|
|
1170
|
+
// (修 F2-A/B 时序根因——曾 :850 先解析 model 再 :867 路由)。agentConfig 是路由
|
|
1171
|
+
// 第二层输入(frontmatter engine)必须先解析;显式 agent ref 校验语义不变。
|
|
1172
|
+
const agent = opts.agent ?? DEFAULT_AGENT_NAME;
|
|
1173
|
+
const agentConfig = opts.agent
|
|
1174
|
+
? this.modelService.getRequiredAgentConfig(opts.agent)
|
|
1175
|
+
: undefined;
|
|
851
1176
|
|
|
852
|
-
// ── 1.5 引擎路由(
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
1177
|
+
// ── 1.5 引擎路由(D2 单轨 + D3-② 路由单点:统一经 routeEngineForHost)──
|
|
1178
|
+
// 唯一实现在 engine/routing.ts(pi 同步短路 + registry 注入 + 兜底回本地 pi 实例
|
|
1179
|
+
// 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
|
|
1180
|
+
// 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
|
|
1181
|
+
// 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
|
|
1182
|
+
// record。pi 请求路径同步短路(routed 非 Promise,零微任务——缺省路径时序不变)。
|
|
1183
|
+
// [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
|
|
1184
|
+
// engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
|
|
1185
|
+
// 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
|
|
859
1186
|
const routingInput = {
|
|
860
1187
|
callEngine: opts.engine,
|
|
861
|
-
agentEngine:
|
|
1188
|
+
agentEngine: agentConfig?.engine,
|
|
862
1189
|
globalDefaultEngine: this.modelService.getGlobalConfig().defaultEngine,
|
|
863
1190
|
};
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (route.engineId !== DEFAULT_ENGINE_ID) {
|
|
876
|
-
return this.executeViaEngine(opts, identity, route);
|
|
877
|
-
}
|
|
878
|
-
// 兜底成功(典型:默认路由 + probe 失败 + 无守卫命中)→ 落回下方 pi 主路径,
|
|
879
|
-
// record 创建时按 pi 语义 + engine/engineFallback 留痕(engine = 实际执行引擎)
|
|
880
|
-
}
|
|
881
|
-
// D5 字节级守护:无 fallback 的 pi 路由剥掉 opts.engine——createRecordForMode
|
|
882
|
-
// 不盖章(pi record entry 序列化产物不得新增 engine 键,undefined 经 JSON 省略)。
|
|
883
|
-
// 兜底路径显式盖 engine='pi' + engineFallback(见上方时机注释)。
|
|
884
|
-
const piOpts =
|
|
885
|
-
route?.engineFallback !== undefined
|
|
886
|
-
? { ...opts, engine: DEFAULT_ENGINE_ID, engineFallback: route.engineFallback }
|
|
887
|
-
: opts.engine === undefined
|
|
888
|
-
? opts
|
|
889
|
-
: { ...opts, engine: undefined };
|
|
890
|
-
|
|
891
|
-
// ── 2. RECORD 创建 + 注册 ──
|
|
892
|
-
const record = this.createRecordForMode(identity, piOpts, mode);
|
|
893
|
-
emitPendingRegister(this.pi, record.id, record.agent);
|
|
894
|
-
|
|
895
|
-
// ── 2.5 worktree 创建(仅 worktree===true 或已传入 handle 时)──
|
|
896
|
-
// record 先创建,worktree 失败时可 finalizeFailed(record 已在 store 中)。
|
|
897
|
-
// worktree 必须显式开启:worktree===true 创建新 worktree;worktree===undefined/false 不创建。
|
|
898
|
-
// fork 不隐含 worktree(UC-1 fork 可独立使用,fork 仅继承上下文,在 parent cwd 跑)。
|
|
899
|
-
let worktreeHandle: WorktreeHandle | undefined;
|
|
900
|
-
if (typeof opts.worktree === "object") {
|
|
901
|
-
// 传入的是已创建的 WorktreeHandle
|
|
902
|
-
worktreeHandle = opts.worktree;
|
|
903
|
-
} else if (opts.worktree === true) {
|
|
904
|
-
// worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
|
|
905
|
-
try {
|
|
906
|
-
worktreeHandle = await this.worktreeManager.create(this.cwd, record.id);
|
|
907
|
-
record.worktreeHandle = worktreeHandle;
|
|
908
|
-
// [create-await 竞态守卫] create 的 await 窗口内 cancel/dispose 可 CAS 把 record
|
|
909
|
-
// 转成 closed 终态——cancelBackground 当时读到的 worktreeHandle 可能仍是 undefined
|
|
910
|
-
// (cleanup 被跳过)。赋值后同同步段检查终态:closed 则主动 cleanup(幂等,抢先的
|
|
911
|
-
// fire-and-forget 清理无害)+ early-failed 返回,不进 kickOffBackground(避免子进程白跑)。
|
|
912
|
-
// 实现约束:赋值 → 终态检查 → kickOffBackground 必须在同一同步段,中间禁止插入 await。
|
|
913
|
-
if (record.status === "closed") {
|
|
914
|
-
await this.worktreeManager.cleanup(worktreeHandle);
|
|
915
|
-
return this.buildEarlyFailedHandle(record);
|
|
916
|
-
}
|
|
917
|
-
} catch (err) {
|
|
918
|
-
// create 失败→不进入 run,finalizeFailed 统一收尾(含 emitPendingUnregister failed)
|
|
919
|
-
const _result = await this.finalizeFailed(record, err);
|
|
920
|
-
return this.buildEarlyFailedHandle(record);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// ── 3. MODE 固定 background:signal/controller、priority 固定 ──
|
|
925
|
-
const signal = record.controller!.signal;
|
|
926
|
-
const priority = PRIORITY_BACKGROUND;
|
|
927
|
-
|
|
928
|
-
// ── 4-7. background 包 detached 立即返回 id ──
|
|
929
|
-
// background detached 运行对 tool 层不可见,完成由 notify 驱动新 turn。
|
|
930
|
-
const bgDetails = project(record);
|
|
931
|
-
this.kickOffBackground(record, { ...piOpts, worktree: worktreeHandle }, ctx, identity, signal, priority);
|
|
932
|
-
return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: bgDetails };
|
|
1191
|
+
const routed = routeEngineForHost({
|
|
1192
|
+
routing: routingInput,
|
|
1193
|
+
// 守卫 c 判据只看调用方显式指定的 model(resolved model 含 ctxModel 兼底,
|
|
1194
|
+
// 恒非空会把一切兜底误判为 model 绑定命中)
|
|
1195
|
+
taskModel: opts.model,
|
|
1196
|
+
strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
|
|
1197
|
+
probe: (engineId) => getEngine(engineId).probe(),
|
|
1198
|
+
piEngine: this.chatPiEngine,
|
|
1199
|
+
});
|
|
1200
|
+
const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
|
|
1201
|
+
return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
|
|
933
1202
|
}
|
|
934
1203
|
|
|
935
1204
|
/**
|
|
@@ -938,7 +1207,7 @@ export class SubagentService {
|
|
|
938
1207
|
* 供 tool 层 cancelHandler 翻译 throw 用(id 不存在 / mode / 终态三种错误)。
|
|
939
1208
|
* 不存在返回 undefined。
|
|
940
1209
|
*/
|
|
941
|
-
findRecord(id: string): RecordSnapshot | undefined {
|
|
1210
|
+
private findRecord(id: string): RecordSnapshot | undefined {
|
|
942
1211
|
this.assertReady();
|
|
943
1212
|
const record = this.store.getMutable(id);
|
|
944
1213
|
return record ? snapshot(record) : undefined;
|
|
@@ -963,7 +1232,7 @@ export class SubagentService {
|
|
|
963
1232
|
*
|
|
964
1233
|
* 返回 undefined:id 在内存与磁盘均不存在。
|
|
965
1234
|
*/
|
|
966
|
-
lookupRecordAnyState(id: string): SubagentRecord | undefined {
|
|
1235
|
+
private lookupRecordAnyState(id: string): SubagentRecord | undefined {
|
|
967
1236
|
try {
|
|
968
1237
|
this.assertReady();
|
|
969
1238
|
} catch {
|
|
@@ -977,33 +1246,81 @@ export class SubagentService {
|
|
|
977
1246
|
// ── 对话模式投递(M2-B3 message action 调用)──────────────
|
|
978
1247
|
|
|
979
1248
|
// [review 修复] 已删除 deliverToRunning(busy follow_up/steer 投递 + pendingMessages
|
|
980
|
-
// 消费确认制):SP-5 upgrade 后所有 running record 走 chatMode 分支 →
|
|
981
|
-
|
|
1249
|
+
// 消费确认制):SP-5 upgrade 后所有 running record 走 chatMode 分支 → 统一投递
|
|
1250
|
+
//(热路径 prompt+streamingBehavior / 冷路径 resume),该方法无生产调用方,
|
|
982
1251
|
// 其配套三段消费链(push / message_start shift / redeliverPending 补投)全部不可达,
|
|
983
1252
|
// 一并移除(详见各文件同步删除)。
|
|
1253
|
+
// [D2 单轨] 投递的 pi RPC stdin 协议知识(stdin prompt 命令直调 + streamingBehavior
|
|
1254
|
+
// 映射 + EPIPE 兜底 + 冷路径分流)已下沉 PiEngine.deliverPrompt——本层经
|
|
1255
|
+
// deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* [V2 决策 3] chatMode 统一投递入口(message action 的 Service 面)——经引擎交互面
|
|
1259
|
+
* 执行(D2:PiEngine.interactRecord——port face interact 的 record 锚定形态,协议知识
|
|
1260
|
+
* 在引擎边界,编排层不做 stdin 写入)。分流语义(按**进程死活**,
|
|
1261
|
+
* 不按 record.status)与热/冷路径细节见 PiEngine.deliverPrompt:
|
|
1262
|
+
*
|
|
1263
|
+
* 热路径(进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle(F3/F4)。
|
|
1264
|
+
* 冷路径(进程死):冷路径续轮(resumeChatRound)重开 session + prompt(仅崩溃/
|
|
1265
|
+
* timeout kill/跨重启命中)。
|
|
1266
|
+
*
|
|
1267
|
+
* 失败语义与直调形态一致:业务拒绝(not ready / EPIPE 兜底耗尽等,文案自带行动语言)
|
|
1268
|
+
* 经 interact 结构化结果回传后原样 throw(错误文本逐字节保持)。
|
|
1269
|
+
*
|
|
1270
|
+
* @param record 目标 record(chatMode,running 或 idle)
|
|
1271
|
+
* @param text 消息正文
|
|
1272
|
+
* @param interrupt true=steer(抢占)/ false=followUp(排队),仅热路径 streamingBehavior 用
|
|
1273
|
+
*/
|
|
1274
|
+
private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
|
|
1275
|
+
this.assertReady();
|
|
1276
|
+
// interactRecord:interact 的 record 锚定形态(调用方已持归属校验过的同一 record
|
|
1277
|
+
// 对象——port face interact 的 handle 解析在此冗余且会做二次 store 查找)
|
|
1278
|
+
const result = await this.chatPiEngine.interactRecord(
|
|
1279
|
+
record,
|
|
1280
|
+
{ kind: "message", payload: text, interrupt },
|
|
1281
|
+
);
|
|
1282
|
+
if (!result.ok) {
|
|
1283
|
+
throw new Error(result.message);
|
|
1284
|
+
}
|
|
1285
|
+
// [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点,首轮
|
|
1286
|
+
// 调用点在 session-runner runSpawn)。原挂载位在 deliverMessage prompt 写入成功后
|
|
1287
|
+
// 的同步段——D2 协议知识下沉 PiEngine.deliverPrompt 后编排层在 interact 返回点
|
|
1288
|
+
// arm(deliverPrompt 热路径段无 await,返回点距 prompt 发出仅差微任务链;窗口口径
|
|
1289
|
+
// 「整轮含 turn 执行与收尾」不受影响)。冷路径(EPIPE 兜底/resume)下 runSpawn 的
|
|
1290
|
+
// 首轮 arm 与此处重复挂载由原语幂等(先清旧 timer)吸收 = 窗口重置,无害。
|
|
1291
|
+
// prompt 发出后挂**中段**无进展检测(有效协议事件行刷新,连续静默判 wedged)+
|
|
1292
|
+
// agent_end 交棒收尾段固定上界;settled 到达(PiEngine.handleSdkEvent 的 disarm)/
|
|
1293
|
+
// 进程 close / cancel/close 终态化处置后即清(disarm 站点散布上述路径,幂等)。
|
|
1294
|
+
armMidRoundNoProgress(record.id, {
|
|
1295
|
+
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
1296
|
+
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
984
1299
|
|
|
985
1300
|
/**
|
|
986
|
-
*
|
|
1301
|
+
* 冷路径续轮(PiEngine.deliverPrompt 的编排回调,D2 下沉后的归属):resume spawn
|
|
1302
|
+
* 开启新一轮对话(设计决策 6 idle 分支)。仅进程死(idle timer reap / 崩溃 / 跨重启
|
|
1303
|
+
* / EPIPE 兜底)时经引擎到达。
|
|
987
1304
|
*
|
|
988
|
-
* record 必须 idle(轮次完成、进程已回收、record 留内存)。手动把 status
|
|
989
|
-
* (M2-A 边界:idle→running 是恢复非终态,绕过 tryTransition——
|
|
990
|
-
* running 才 CAS
|
|
1305
|
+
* record 必须 idle-resumable(轮次完成、进程已回收、record 留内存)。手动把 status
|
|
1306
|
+
* 设回 "running"(M2-A 边界:idle→running 是恢复非终态,绕过 tryTransition——
|
|
1307
|
+
* tryTransition 要求当前态 running 才 CAS)。
|
|
991
1308
|
*
|
|
992
1309
|
* resume 参数从 record identity 读(防多轮对话模型漂移,探针 P-10):sessionFile、
|
|
993
1310
|
* model、thinkingLevel 均为 record 身份字段(创建时确定、不可变)。maxTurns/schema 等
|
|
994
1311
|
* 执行约束第一版不恢复(设计 §5 拆分 1 待验证检查点),agentConfig 用 undefined
|
|
995
|
-
|
|
1312
|
+
*(pi --session 续写保留上下文,agent 行为由 session 内 messages 决定;M2-B3 messageHandler 可完善)。
|
|
996
1313
|
*
|
|
997
|
-
* detached
|
|
998
|
-
* chatMode + done
|
|
999
|
-
*
|
|
1314
|
+
* detached 编排(kickOffChatRound,经 EnginePort 交接):不 await,轮次在 background 跑。
|
|
1315
|
+
* chatMode + done 时轮次收尾的 M2-A 分流自动把 record 回退 idle-resumable。并发槽在
|
|
1316
|
+
* 轮次执行内重新 acquire(轮次间 idle 已 release);pool.acquire 是排队模型,
|
|
1000
1317
|
* 池满时排队等待槽位而非 throw(与 execute 一致)。
|
|
1001
1318
|
*
|
|
1002
|
-
* @param record 目标 record(必须 idle)
|
|
1319
|
+
* @param record 目标 record(必须 idle-resumable)
|
|
1003
1320
|
* @param text 新一轮消息正文
|
|
1004
|
-
* @throws Error record 非
|
|
1321
|
+
* @throws Error record 非 running / 无 sessionFile / 无 controller / worktree 绑定丢失 / 续轮在途
|
|
1005
1322
|
*/
|
|
1006
|
-
|
|
1323
|
+
private resumeColdRound(record: ExecutionRecord, text: string): void {
|
|
1007
1324
|
this.assertReady();
|
|
1008
1325
|
// [CL-b1-cas-coupling / v4 B-1] v4 把旧 idle 折入 running 后此守卫对 idle-resumable
|
|
1009
1326
|
// record 恒放行(idle 本来就是 running),`status = "running"`(下方)是幂等写——
|
|
@@ -1017,10 +1334,10 @@ export class SubagentService {
|
|
|
1017
1334
|
);
|
|
1018
1335
|
}
|
|
1019
1336
|
// [review MF1] 在途 resume 守卫:上一条消息发起的 resume 仍在途(spawn 尚未注册 /
|
|
1020
|
-
//
|
|
1337
|
+
// 本轮轮次未收尾)时,再次到达(冷路径重入 / EPIPE 兜底)直接拒绝。
|
|
1021
1338
|
// 触发链:pi 对同一 assistant message 的 tool calls 顺序执行(sequential),tool1 的
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1339
|
+
// 投递在冷路径续轮返回即 resolve(早于 spawn 注册完成),tool2 立即执行 →
|
|
1340
|
+
// getChildByRecord 仍 undefined → 再次冷路径。无此守卫 → 两次 kickOff →
|
|
1024
1341
|
// runSpawn 2 次 → 两 pi 子进程双写同一 session JSONL + 第一个脱离 kill 记账成孤儿。
|
|
1025
1342
|
if (this.resumesInFlight.has(record.id)) {
|
|
1026
1343
|
// MF-4:行动语言。
|
|
@@ -1033,7 +1350,7 @@ export class SubagentService {
|
|
|
1033
1350
|
if (!record.sessionFile) {
|
|
1034
1351
|
// MF-4:session 损坏 → canonical 文案(spec §3.1 失败表)。
|
|
1035
1352
|
throw new Error(
|
|
1036
|
-
`
|
|
1353
|
+
`subagent ${record.id} session unavailable (session file missing or unreadable). ` +
|
|
1037
1354
|
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
1038
1355
|
);
|
|
1039
1356
|
}
|
|
@@ -1097,144 +1414,21 @@ export class SubagentService {
|
|
|
1097
1414
|
};
|
|
1098
1415
|
const ctx = this.buildSessionRunnerContext();
|
|
1099
1416
|
|
|
1100
|
-
// detached
|
|
1417
|
+
// detached 编排:轮次在 background 跑,pool 重新 acquire(轮次间 idle 已 release)。
|
|
1101
1418
|
// chatMode + done 时 M2-A 分流自动 finalizeRoundToIdle(record 回 idle、round+1)。
|
|
1102
|
-
// [review MF1] 在途标记在 kickOff
|
|
1103
|
-
// (冷路径 / EPIPE
|
|
1419
|
+
// [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
|
|
1420
|
+
// (冷路径 / EPIPE 兜底)在守卫处被拒;轮次收尾 finally 统一清除。
|
|
1104
1421
|
this.resumesInFlight.add(record.id);
|
|
1105
|
-
this.
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* [V2 决策 3] chatMode 统一投递:按**进程死活**分流,不按 record.status。
|
|
1110
|
-
*
|
|
1111
|
-
* V2 进程长驻——chatMode record 首轮 agent_settled 后进轻量 idle(Step 4a:进程保活、
|
|
1112
|
-
* idle timer armed),续聊时进程仍在内存,不该重开 session。故续聊投递不按 status
|
|
1113
|
-
*(running/idle 都可能是热路径),而是判进程死活:
|
|
1114
|
-
*
|
|
1115
|
-
* 热路径(进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle(F3/F4)。
|
|
1116
|
-
* busy(isStreaming)时 followUp 入队/steer 抢占;idle 时 streamingBehavior 被忽略、
|
|
1117
|
-
* 直接开新 turn。不用 steer/followUp 命令、不依赖 clearQueue(F8),结构上消除残留。
|
|
1118
|
-
* 冷路径(进程死):复用 resumeRound 重开 session + prompt(仅崩溃/timeout kill/跨重启命中)。
|
|
1119
|
-
*
|
|
1120
|
-
* disarm idle timer:新 turn 开始必须 disarm(V2 决策 4),防 turn 期间 idle timer 误杀活进程。
|
|
1121
|
-
*
|
|
1122
|
-
* status 处理:判活分流后**各自**设 running——热路径手动设 running(新 turn 开始);
|
|
1123
|
-
* 冷路径由 resumeRound 校验 idle 并自行设 running + spawn(故不在此预设 running,否则
|
|
1124
|
-
* resumeRound 的 idle 检查会 throw)。resume spawn 后 session-runner 回填 record.pid,
|
|
1125
|
-
* 热路径拿到 child 时也顺便刷新 pid(resume 重开进程后 pid 已变)。
|
|
1126
|
-
*
|
|
1127
|
-
* [review 修复] 曾对比的 deliverToRunning(非 chatMode busy 投递 + pendingMessages
|
|
1128
|
-
* 消费确认制)已删除——SP-5 upgrade 后无生产调用方(V2 决策 3 已删消费确认制)。
|
|
1129
|
-
*
|
|
1130
|
-
* @param record 目标 record(chatMode,running 或 idle)
|
|
1131
|
-
* @param text 消息正文
|
|
1132
|
-
* @param interrupt true=steer(抢占)/ false=followUp(排队),仅热路径 prompt streamingBehavior 用
|
|
1133
|
-
*/
|
|
1134
|
-
async deliverMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
|
|
1135
|
-
this.assertReady();
|
|
1136
|
-
// 新 turn,disarm idle timer(防 turn 期间误杀活进程,V2 决策 4)
|
|
1137
|
-
disarmIdleTimer(record.id);
|
|
1138
|
-
const child = getChildByRecord(record.id);
|
|
1139
|
-
if (child && !child.killed) {
|
|
1140
|
-
// 热路径:进程活,prompt + streamingBehavior(V2 决策 3,pi 权威裁决 busy/idle)
|
|
1141
|
-
record.status = "running";
|
|
1142
|
-
// 刷新 pid 内存记账(resume spawn 后 child.pid 已变,顺便更新)
|
|
1143
|
-
if (child.pid !== undefined) record.pid = child.pid;
|
|
1144
|
-
try {
|
|
1145
|
-
sendPromptCommand(child, text, { streamingBehavior: interrupt ? "steer" : "followUp" });
|
|
1146
|
-
// 热路径成功,清零 EPIPE 连续失败计数([v4 A-1] 计数器已迁移到 stdin-writer)
|
|
1147
|
-
clearEpipeFailure(record.id);
|
|
1148
|
-
// [race-F5] 写后死进程检测:write 同步成功只代表数据进了内核 pipe 缓冲,子进程
|
|
1149
|
-
// 可能在读取前死亡(gate/idle kill 竞速)。exitCode/signalCode 已非 null = 进程已死
|
|
1150
|
-
//(close 事件可能尚未到达),缓冲中的消息将被静默丢弃。只 warn 留证(含 runId 与
|
|
1151
|
-
// 消息类型),不抛错不重试:终态回收已由 kill 路径保证,对死进程重试反而可能二次写。
|
|
1152
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1153
|
-
logger.warn(
|
|
1154
|
-
`[subagents] deliverMessage: child ${record.id} died around stdin write, message may be lost`,
|
|
1155
|
-
{
|
|
1156
|
-
msgType: interrupt ? "steer" : "followUp",
|
|
1157
|
-
exitCode: child.exitCode,
|
|
1158
|
-
signalCode: child.signalCode,
|
|
1159
|
-
},
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
// 轮始执行态信号清除 + 迁移上报(residual-fixes U3 补全,与冷路径 resumeRound
|
|
1163
|
-
// 对称):新一轮开跑 = 无轮终信号——清上一轮 result(§5.4 isStreaming 公式要求
|
|
1164
|
-
// result undefined 才显示 streaming)与 resumable,appendEntry 让 runtime/W18
|
|
1165
|
-
// 派生缓存失效、GUI 侧从 waiting 切回 spinner。仅在投递成功后清(失败保留
|
|
1166
|
-
// 上一轮信号,EPIPE 兜底走 resumeRound 时由其再清)。
|
|
1167
|
-
record.result = undefined;
|
|
1168
|
-
record.resumable = undefined;
|
|
1169
|
-
this.store.reportRecordTransition(record);
|
|
1170
|
-
// [T2③ / LC-1] 热路径轮 settled 等待固定硬上限(双挂载原语之热路径调用点,
|
|
1171
|
-
// 首轮调用点在 session-runner runSpawn)。prompt 发出即起算(整轮含 turn 执行与
|
|
1172
|
-
// 收尾都在窗口内,impl-plan §5 窗口口径裁决),settled 到达(session-runner
|
|
1173
|
-
// handleSdkEvent 的 disarm,幂等生效于热路径轮)/ 进程 close(waitForChildExit
|
|
1174
|
-
// 的 disarm)/ 本方终态化处置后即清——settled 永不到达(事件行丢失 / 子进程
|
|
1175
|
-
// wedged)时本 timer 是唯一独立回收通道:现状「settled 不 arm 则 idle timer 不挂、
|
|
1176
|
-
// runSpawn 已在首轮返回、spawn watchdog 默认关」的三无窗口由此收敛。
|
|
1177
|
-
armSettledWatchdog(record.id, () => this.onHotPathSettledWatchdogTimeout(record));
|
|
1178
|
-
} catch (err) {
|
|
1179
|
-
// EPIPE 兜底:stdin 管道已断,进程实际已死但 close 事件尚未到达。
|
|
1180
|
-
// 检测 EPIPE 关键词 → 进程按 dead 处理 → 自动转冷路径 resume + 消息重放。
|
|
1181
|
-
// [review MF1] 本兜底不持 activateLock,但与冷路径共用 resumeRound 的在途守卫
|
|
1182
|
-
//(resumesInFlight):resume 已在途时兜底的 resumeRound 调用被拒(throw 行动语言),
|
|
1183
|
-
// 不会二次 spawn。
|
|
1184
|
-
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
1185
|
-
logger.warn(`[subagents] EPIPE on hot path for ${record.id}, falling back to cold path resume`, {
|
|
1186
|
-
detail: err.message,
|
|
1187
|
-
});
|
|
1188
|
-
// 清理 spawnedChildren 中的死进程条目(让 resumeRound 能重新 spawn)。
|
|
1189
|
-
// [M4] 按值守卫:仅当 Map 当前值仍是本次写 EPIPE 的 child 才删——若已被 resume
|
|
1190
|
-
// spawn 覆盖为新 child(close 事件先于本 catch 到达的极端时序),不误删新注册
|
|
1191
|
-
//(与 session-runner removeChildRegistration 同语义)。
|
|
1192
|
-
if (spawnedChildren.get(record.id) === child) {
|
|
1193
|
-
spawnedChildren.delete(record.id);
|
|
1194
|
-
}
|
|
1195
|
-
// 递增连续 EPIPE 计数([v4 A-1] helper 合并同步/异步路径计数)
|
|
1196
|
-
const count = recordEpipeFailure(record.id);
|
|
1197
|
-
if (count >= EPIPE_FAILURE_THRESHOLD) {
|
|
1198
|
-
// 连续达阈值 EPIPE → 不再尝试 resume,throw 含恢复指引
|
|
1199
|
-
clearEpipeFailure(record.id);
|
|
1200
|
-
throw new Error(
|
|
1201
|
-
`[subagents] EPIPE fallback exhausted for ${record.id}: ${count} consecutive EPIPE failures. ` +
|
|
1202
|
-
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
// 冷路径 resume + 原消息重放(v4 B-1: status 已 running,resumeRound CAS 直接放行)
|
|
1206
|
-
this.resumeRound(record, text);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
// [T2⑧ / PS-3] 非 EPIPE 写失败(如 ERR_STREAM_DESTROYED——错误分类只认 "EPIPE"
|
|
1210
|
-
// 子串,过窄):deliverMessage 入口已无条件 disarmIdleTimer,旧实现直接 rethrow
|
|
1211
|
-
// 不 re-arm——「timer armed」这个防泄漏前提在失败路径永久丢失:进程无人回收
|
|
1212
|
-
//(无 timer、无轮、record=running)泄漏至宿主退出,hasRunningBackground 恒真
|
|
1213
|
-
// 还会顺延其他完成通知。修复:rethrow 前再武装 idle timer(含防御性降级),恢复
|
|
1214
|
-
// 进程回收通道 + 通知放行门;调用方错误语义不变(仍 rethrow)。
|
|
1215
|
-
this.rearmIdleTimerAfterHotPathFailure(record, err);
|
|
1216
|
-
// 非 EPIPE 错误——不应发生,重新抛出让调用方处理
|
|
1217
|
-
throw err;
|
|
1218
|
-
}
|
|
1219
|
-
} else {
|
|
1220
|
-
// 冷路径:进程死(idle timer reap / 崩溃 / 跨重启),record 应为 idle → resume spawn。
|
|
1221
|
-
// D3:acquireActivateLock 双保险——注意锁只覆盖 resumeRound 同步段,释放在子进程注册
|
|
1222
|
-
//(session-runner spawnedChildren.set)之前(中间隔 pool.acquire await + tempFile 等异步点)。
|
|
1223
|
-
// 真正的单写者守卫是 resumeRound 的 resumesInFlight([review MF1]):锁释放后、child
|
|
1224
|
-
// 注册前到达的第二次冷路径 message 在 resumeRound 处被拒,不会二次 spawn。
|
|
1225
|
-
const releaseLock = await acquireActivateLock(record.id);
|
|
1226
|
-
try {
|
|
1227
|
-
this.resumeRound(record, text);
|
|
1228
|
-
} finally {
|
|
1229
|
-
releaseLock();
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1422
|
+
this.kickOffChatRound(record, opts, identity, ctx, record.controller.signal, PRIORITY_BACKGROUND, resume);
|
|
1232
1423
|
}
|
|
1233
1424
|
|
|
1234
1425
|
/**
|
|
1235
1426
|
* [T2③] 热路径轮 settled watchdog 到期处置(对齐 u-t2a 首轮形态:kill + 该轮失败
|
|
1236
1427
|
* 终态化 + 失败通知,error 含 'settled watchdog' 标记与恢复指引)。
|
|
1237
1428
|
*
|
|
1429
|
+
* [D9 两段式] 两段(mid-round 无进展 / settled 收尾段上界)共用本处置,fire 信息
|
|
1430
|
+
*(段 + 窗长)由原语注入,失败文案按段分叉窗长语义。
|
|
1431
|
+
*
|
|
1238
1432
|
* 与首轮的差异:runSpawn 已返回(无收尾链路承接 settledWatchdogFired 标记),失败
|
|
1239
1433
|
* 终态化在本回调内完成。chatMode 按 MF-6 语义回退 running-resumable(与首轮 watchdog
|
|
1240
1434
|
* 经 runAndFinalize 失败分支的最终形态一致——对话可冷路径复活);非 chatMode 终态
|
|
@@ -1243,10 +1437,14 @@ export class SubagentService {
|
|
|
1243
1437
|
* 回调在 timer 触发的同步上下文执行:同步段只做 kill + CAS(不抛),异步收尾
|
|
1244
1438
|
* fire-and-forget 且 catch 归 bestEffort——错误逃出回调 = uncaughtException 崩宿主。
|
|
1245
1439
|
*/
|
|
1246
|
-
private onHotPathSettledWatchdogTimeout(record: ExecutionRecord): void {
|
|
1440
|
+
private onHotPathSettledWatchdogTimeout(record: ExecutionRecord, fire: SettledWatchdogFireInfo): void {
|
|
1441
|
+
const windowDesc =
|
|
1442
|
+
fire.phase === "mid-round"
|
|
1443
|
+
? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min after prompt (mid-round no-progress)`
|
|
1444
|
+
: `no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end (settled phase)`;
|
|
1247
1445
|
logger.warn(
|
|
1248
|
-
`[subagents] settled watchdog fired for ${record.id}:
|
|
1249
|
-
|
|
1446
|
+
`[subagents] settled watchdog (${fire.phase}) fired for ${record.id}: ${windowDesc}, ` +
|
|
1447
|
+
`terminating (LC-1 wedge recovery)`,
|
|
1250
1448
|
);
|
|
1251
1449
|
killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
|
|
1252
1450
|
const failedResult: AgentResult = {
|
|
@@ -1255,7 +1453,7 @@ export class SubagentService {
|
|
|
1255
1453
|
durationMs: Date.now() - record.startedAt,
|
|
1256
1454
|
success: false,
|
|
1257
1455
|
error:
|
|
1258
|
-
`subagent did not reach agent_settled
|
|
1456
|
+
`subagent did not reach agent_settled (${windowDesc}; settled watchdog); ` +
|
|
1259
1457
|
`the process was terminated to bound the wait. ` +
|
|
1260
1458
|
`Recovery: check state with subagents action:'list', then re-send your message to continue.`,
|
|
1261
1459
|
sessionId: record.id,
|
|
@@ -1268,38 +1466,17 @@ export class SubagentService {
|
|
|
1268
1466
|
? this.finalizeRoundToIdle(record, failedResult)
|
|
1269
1467
|
: this.finalizeRecord(record, failedResult, "closed", "gc");
|
|
1270
1468
|
void finalize
|
|
1271
|
-
|
|
1469
|
+
// [D4-①] 通知簇搬移 notify-host 后的遗留调用点修正:this.notifyComplete 方法已
|
|
1470
|
+
// 不存在(其余三处调用点均 this.notifyHost.notifyComplete),旧引用 throw
|
|
1471
|
+
// TypeError 被本 .catch 吞掉 → settled watchdog 失败通知静默丢失
|
|
1472
|
+
.then(() => this.collectCoordinator.route(record))
|
|
1272
1473
|
.catch((err: unknown) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
|
|
1273
1474
|
}
|
|
1274
1475
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
* armIdleTimer 理论不 throw;降级链仍保底:非法 → 挂 DEFAULT_IDLE_TIMEOUT_MS + warn
|
|
1280
|
-
* (兜底可见,对齐 session-runner agent_settled 侧的 T4② 降级形态),双重失败退回
|
|
1281
|
-
* 「不挂」但留 error 痕。
|
|
1282
|
-
*/
|
|
1283
|
-
private rearmIdleTimerAfterHotPathFailure(record: ExecutionRecord, cause: unknown): void {
|
|
1284
|
-
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1285
|
-
const onTimeout = (): void => {
|
|
1286
|
-
killRecordChildWithEscalation(record.id, "idle timer (hot-path failure fallback)");
|
|
1287
|
-
};
|
|
1288
|
-
try {
|
|
1289
|
-
armIdleTimer(record.id, onTimeout, record.idleTimeoutMs);
|
|
1290
|
-
} catch {
|
|
1291
|
-
try {
|
|
1292
|
-
armIdleTimer(record.id, onTimeout, DEFAULT_IDLE_TIMEOUT_MS);
|
|
1293
|
-
} catch (fallbackErr) {
|
|
1294
|
-
bestEffort(fallbackErr, "rearmIdleTimer fallback (deliverMessage non-EPIPE failure)", "error");
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
logger.warn(
|
|
1299
|
-
`[subagents] deliverMessage hot path failed for ${record.id}; idle timer re-armed to keep process recovery bounded`,
|
|
1300
|
-
{ detail },
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1476
|
+
// [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
|
|
1477
|
+
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/engines/pi/pi-engine.ts 的
|
|
1478
|
+
// rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
|
|
1479
|
+
// 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
|
|
1303
1480
|
|
|
1304
1481
|
// ── 对话模式 message/close action 支持(M2-B3)──────────────
|
|
1305
1482
|
|
|
@@ -1323,14 +1500,15 @@ export class SubagentService {
|
|
|
1323
1500
|
* @throws Error record 不存在 / 非本 session 所有(含恢复指引)
|
|
1324
1501
|
* @throws ResurrectDeniedError 命中可重连集但被 worktree/异进程活实例守卫拦截(自带完整行动语言)
|
|
1325
1502
|
*/
|
|
1326
|
-
getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord {
|
|
1503
|
+
private getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord {
|
|
1327
1504
|
this.assertReady();
|
|
1328
1505
|
let record = this.store.getMutable(id);
|
|
1329
1506
|
// SP-2 跨重启恢复:内存未命中时,从磁盘 collectRecords 重建 idle record。
|
|
1330
1507
|
// reconstructAll 已将跨重启 record(无 sidecar + pid 死)标记为 running(v4 B-1 可续聊语义,非 crashed),
|
|
1331
1508
|
// 直接转为可变 ExecutionRecord register 进内存,供 message/close action 续操作。
|
|
1332
1509
|
if (!record) {
|
|
1333
|
-
|
|
1510
|
+
// [D4-③] 冷查/复活链整体搬移至 cold-resurrect.ts(行为逐字节等价)。
|
|
1511
|
+
record = coldLookupForAction(this.coldResurrectDeps, id, opts?.allowReconnect === true);
|
|
1334
1512
|
}
|
|
1335
1513
|
if (!record || record.rootSessionId !== this.sessionRootId) {
|
|
1336
1514
|
throw new Error(
|
|
@@ -1340,12 +1518,12 @@ export class SubagentService {
|
|
|
1340
1518
|
}
|
|
1341
1519
|
// [v4 A-5 / P7] 直接父校验:rootSessionId 已确认 record 属于本 session 树,但递归场景下
|
|
1342
1520
|
// 孙级 record(parentRecordId = 某子进程的 self recordId)的子进程句柄只存在于其直接父
|
|
1343
|
-
//
|
|
1521
|
+
// 进程内存。主进程(基线 null)若仅凭 rootSessionId 通过就 message 孙级,会走
|
|
1344
1522
|
// 冷路径重新 spawn → 双写同一 session 文件(P7 双写者窗口)。统一用 baseline recordId 校验:
|
|
1345
1523
|
// - 主进程 baseline=undefined → 只能操作 parentRecordId=undefined 的根层 record
|
|
1346
1524
|
// - 子进程 baseline="sa-X" → 只能操作 parentRecordId="sa-X" 的直接孩子
|
|
1347
1525
|
// record.parentRecordId===undefined 视作根层,仅主进程可操作(身份缺省的旧/异常 record 归此)。
|
|
1348
|
-
const baselineRecordId = this.
|
|
1526
|
+
const baselineRecordId = this.execNesting.baseline()?.recordId ?? undefined;
|
|
1349
1527
|
if (record.parentRecordId !== baselineRecordId) {
|
|
1350
1528
|
throw new Error(
|
|
1351
1529
|
`subagent ${id} is owned by its direct parent; message it through that parent ` +
|
|
@@ -1357,156 +1535,13 @@ export class SubagentService {
|
|
|
1357
1535
|
return record;
|
|
1358
1536
|
}
|
|
1359
1537
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
* 未命中再全目录 collectRecords 兜底(running,或 allowReconnect 且可重连 closed)。
|
|
1368
|
-
*
|
|
1369
|
-
* [T5③ / PS-7b] running 候选异进程活实例守卫:冷查 running 候选(跨重启 / 内存重建)
|
|
1370
|
-
* 此前不经任何探针直接 resurrect + resume spawn——若其 .alive marker 仍指向活着的
|
|
1371
|
-
* 异进程实例(父进程重启后旧子进程尚存的窗口),resume 会 spawn 第二个 pi 子进程
|
|
1372
|
-
* 写同一 session JSONL(本代码最忌惮的双写者形态,v4 A-5/P7 事故模式)。closed 候选
|
|
1373
|
-
* 的同款守卫已在 assertReconnectAllowed(v8.5 D);本守卫闭合 running 候选的防御
|
|
1374
|
-
* 不对称。marker 的 pid 是子进程 pi 的 pid(非父进程),本进程持有的 running record
|
|
1375
|
-
* 恒在内存(archive 才移出),可达本冷查分支的 running 候选必然来自磁盘重建——
|
|
1376
|
-
* 探针命中即拒绝(ResurrectDeniedError,与 closed 候选守卫同异常类型,错误含 pid
|
|
1377
|
-
* 与恢复指引)。 */
|
|
1378
|
-
private findColdLookupCandidate(id: string, allowReconnect: boolean): SubagentRecord | undefined {
|
|
1379
|
-
const direct = this.store.findLightById(id);
|
|
1380
|
-
const found =
|
|
1381
|
-
(direct?.status === "running" ? direct : undefined) ??
|
|
1382
|
-
this.store
|
|
1383
|
-
.collectRecords(COLD_LOOKUP_SCAN_LIMIT, "all", undefined)
|
|
1384
|
-
.find((r) => r.id === id && (r.status === "running" || (allowReconnect && this.isReconnectableClosed(r))));
|
|
1385
|
-
if (found?.status === "running" && found.sessionFile) {
|
|
1386
|
-
const foreign = findForeignLiveInstance(found.sessionFile);
|
|
1387
|
-
if (foreign) {
|
|
1388
|
-
throw new ResurrectDeniedError(
|
|
1389
|
-
`subagent ${id} is currently running in another process instance (pid ${foreign.pid}, ` +
|
|
1390
|
-
`startedAt=${new Date(foreign.startedAt).toISOString()}); resuming here would double-write ${found.sessionFile}. ` +
|
|
1391
|
-
`Recovery: retry once that process exits; if it never exits, action:'close' this subagent, then action:'start' a fresh one.`,
|
|
1392
|
-
);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return found;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
/** 可重连守卫(coldLookupForAction 步骤 2,[v8.5 D]):先于任何状态突变与注册。
|
|
1399
|
-
* worktree 绑定丢失 / 异进程活实例以 ResurrectDeniedError 抛出(endedMessageGuard
|
|
1400
|
-
* 必须原样透传,不得改写为 fork-from 指引误导 agent 走已被判死的通道);拒绝时
|
|
1401
|
-
* 内存不得残留该记录(findRecord 契约)。 */
|
|
1402
|
-
private assertReconnectAllowed(found: SubagentRecord, id: string): void {
|
|
1403
|
-
if (found.status !== "closed") return;
|
|
1404
|
-
if (found.worktree === true) {
|
|
1405
|
-
throw new ResurrectDeniedError(
|
|
1406
|
-
`subagent ${id} cannot be transparently resumed: it was created with worktree isolation, ` +
|
|
1407
|
-
`and its worktree checkout no longer exists after restart (resuming in place would make spawn cwd fall back to the main repo). ` +
|
|
1408
|
-
`Recovery: action:'start' a fresh subagent (with a new worktree if isolation is still needed); ` +
|
|
1409
|
-
`its conversation history remains intact at ${found.sessionFile}.`,
|
|
1410
|
-
);
|
|
1411
|
-
}
|
|
1412
|
-
const foreign = found.sessionFile ? findForeignLiveInstance(found.sessionFile) : undefined;
|
|
1413
|
-
if (foreign) {
|
|
1414
|
-
throw new ResurrectDeniedError(
|
|
1415
|
-
`subagent ${id} is not transparently resumable: its previous instance still finishing in another process ` +
|
|
1416
|
-
`(pid ${foreign.pid}, startedAt=${new Date(foreign.startedAt).toISOString()}). ` +
|
|
1417
|
-
`Resuming in place would double-write ${found.sessionFile}. ` +
|
|
1418
|
-
`Recovery: retry once that process exits; if it never exits, action:'start' a fresh subagent and treat the history at ${found.sessionFile} as read-only reference.`,
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
/** 磁盘候选重建为可变 record 并 register + 上报(coldLookupForAction 步骤 4)。 */
|
|
1424
|
-
private resurrectColdRecord(found: SubagentRecord, id: string): ExecutionRecord {
|
|
1425
|
-
const record = createRecord(id, {
|
|
1426
|
-
agent: found.agent,
|
|
1427
|
-
model: found.model,
|
|
1428
|
-
thinkingLevel: found.thinkingLevel,
|
|
1429
|
-
mode: found.mode,
|
|
1430
|
-
task: found.task,
|
|
1431
|
-
slug: found.slug,
|
|
1432
|
-
startedAt: found.startedAt,
|
|
1433
|
-
rootSessionId: found.rootSessionId,
|
|
1434
|
-
parentRecordId: found.parentRecordId,
|
|
1435
|
-
depth: found.depth,
|
|
1436
|
-
// [v4 A-3] 跨重启恢复入口——message 路径磁盘重建无条件置 chatMode=true(现状机制,
|
|
1437
|
-
// V3 方案 A 方向兑现)。改动此处必须带 S3 回归场景(跨重启 message 续聊验证)。
|
|
1438
|
-
// V3 SP-5 探针定案:机制已存在,本注释即定案,不再悬置。
|
|
1439
|
-
chatMode: true,
|
|
1440
|
-
controller: new AbortController(),
|
|
1441
|
-
});
|
|
1442
|
-
record.sessionFile = found.sessionFile;
|
|
1443
|
-
record.round = found.round;
|
|
1444
|
-
// [review round2] 跨重启 worktree 绑定丢失防护:原 record 创建时启用了 worktree 隔离
|
|
1445
|
-
//(session entry 的 worktree 标志),但 WorktreeHandle 不可序列化、重建后恒缺失。
|
|
1446
|
-
// 标记 hadWorktree,resumeRound 守卫据此拒绝续聊(防 spawn cwd 静默回落主 repo 破坏
|
|
1447
|
-
// 隔离——正是 worktree 要防的并发写冲突场景)。close 不受影响(closeChatIdle 走
|
|
1448
|
-
// doFinalizeRecord,泄漏的 worktree 由 reaper 兜底回收)。
|
|
1449
|
-
record.hadWorktree = found.worktree === true;
|
|
1450
|
-
// [v8.5 D] 透明重生回边:独立函数不走 tryTransition 单向语义(closed 单向性对正常
|
|
1451
|
-
// 执行流完整保留);准入唯一依据 = A 档 sidecar 真实死因 ∈ 可重连集。死亡语义位由
|
|
1452
|
-
// resurrectClosed 清除;register 后立刻上报 transition entry,live/reload 视图同步
|
|
1453
|
-
// 翻回 running(等价性由 applyEntry reducer 保证,对齐 SP-2 重建即报告先例)。
|
|
1454
|
-
if (found.status !== "running") {
|
|
1455
|
-
// [review MF-8] 磁盘终态位同步翻转:record-store buildRecord 分支 2(.finalized
|
|
1456
|
-
// 存在 → closed)优先级高于 .alive 活态分支 3,重生若不删 sidecar,任何磁盘扫描
|
|
1457
|
-
// (异进程 / reload / session-reader)都会把本进程内存里 running 的 record 报成
|
|
1458
|
-
// closed/disconnected——破坏 live ≡ reload,且为跨进程二次 resurrect 开门。
|
|
1459
|
-
// best-effort 对齐 BC-4 语义;.alive 刷新为当前进程(后续 resume spawn 会覆盖写)。
|
|
1460
|
-
if (record.sessionFile) {
|
|
1461
|
-
try {
|
|
1462
|
-
fs.rmSync(`${record.sessionFile}.finalized`, { force: true });
|
|
1463
|
-
writeAliveMarker(record.sessionFile, { pid: process.pid, id, startedAt: Date.now() });
|
|
1464
|
-
} catch (_e) {
|
|
1465
|
-
void _e; // best-effort:sidecar 翻转失败不阻断重生主流程
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
resurrectClosed(record);
|
|
1469
|
-
}
|
|
1470
|
-
this.store.register(record);
|
|
1471
|
-
if (found.status !== "running") {
|
|
1472
|
-
this.store.reportRecordTransition(record);
|
|
1473
|
-
}
|
|
1474
|
-
return record;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
private coldLookupForAction(id: string, allowReconnect: boolean): ExecutionRecord | undefined {
|
|
1478
|
-
const found = this.findColdLookupCandidate(id, allowReconnect);
|
|
1479
|
-
if (!found) return undefined;
|
|
1480
|
-
// [v8.5 D] 可重连候选的守卫先于任何状态突变与注册(细节见 assertReconnectAllowed)
|
|
1481
|
-
this.assertReconnectAllowed(found, id);
|
|
1482
|
-
// [review MF-9] 归属校验先于任何持久化副作用:coldLookup 是 getRecordForAction 的
|
|
1483
|
-
// 内存未命中分支,若先 resurrect/register/report 再由调用方抛归属错误,会在磁盘/
|
|
1484
|
-
// 内存留下幽灵 running record + running transition entry(跨进程双 resurrect 窗口)。
|
|
1485
|
-
// rootSessionId 不匹配 → 返回 undefined,由 getRecordForAction 抛统一「not found or
|
|
1486
|
-
// not owned」(不区分失败形态,防跨 session 探测);parentRecordId 跨层不匹配 →
|
|
1487
|
-
// 原样抛 direct parent 错误(与外层校验同文案,保留跨层导航指引)。
|
|
1488
|
-
if (found.rootSessionId !== this.sessionRootId) {
|
|
1489
|
-
return undefined;
|
|
1490
|
-
}
|
|
1491
|
-
const baselineRecordId = this.execCtxBaseline?.recordId ?? undefined;
|
|
1492
|
-
if (found.parentRecordId !== baselineRecordId) {
|
|
1493
|
-
throw new Error(
|
|
1494
|
-
`subagent ${id} is owned by its direct parent; message it through that parent ` +
|
|
1495
|
-
`(see /subagents list, parent=${found.parentRecordId ?? "(root layer)"}). [v4 A-5] cross-layer ` +
|
|
1496
|
-
`ownership guard: this process's baseline=${baselineRecordId ?? "(root)"} is not the direct parent of ${id}; ` +
|
|
1497
|
-
`operating here would race the owning child process's handle and double-write the session file.`,
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
return this.resurrectColdRecord(found, id);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
/** [v8.5 D] 冷查候选过滤:closed 且死因落在可重连集。判定源 = closedReason(buildRecord
|
|
1504
|
-
* 归一化后的对外字段:A 档真实死因直通、旧空 sidecar 兑底 disconnected——SubagentRecord
|
|
1505
|
-
* 不暴露 raw finalizedReason);cancelled/user-close/gc 等主动关闭与自然完成死因天然不在集合内。
|
|
1506
|
-
* 防线在集合本身而非调用点。 */
|
|
1507
|
-
private isReconnectableClosed(r: SubagentRecord): boolean {
|
|
1508
|
-
return isReconnectableFinalReason(r.closedReason);
|
|
1509
|
-
}
|
|
1538
|
+
// [D4-③] 冷路径复活链(findColdLookupCandidate / assertReconnectAllowed /
|
|
1539
|
+
// resurrectColdRecord / coldLookupForAction + isReconnectableClosed 判定)已整体
|
|
1540
|
+
// 搬移至 cold-resurrect.ts(本类经 coldResurrectDeps 注入,行为逐字节等价)。
|
|
1541
|
+
// SP-2 冷路径 [perf] 语义不变:idToFile 索引直查 → collectRecords 全扫兜底。
|
|
1542
|
+
// [T5③ / PS-7b] running 候选异进程活实例守卫(findForeignLiveInstance 探针,
|
|
1543
|
+
// ResurrectDeniedError)已随迁 cold-resurrect.ts 的 findColdLookupCandidate
|
|
1544
|
+
//(与 closed 候选守卫 assertReconnectAllowed 对称)。
|
|
1510
1545
|
|
|
1511
1546
|
/**
|
|
1512
1547
|
* close action 的统一行为分流(running 子态 × force)。
|
|
@@ -1523,7 +1558,7 @@ export class SubagentService {
|
|
|
1523
1558
|
* @param record 目标 record(getRecordForAction 已校验归属)
|
|
1524
1559
|
* @param force true=立即终止(running 时 SIGTERM)/ false=优雅关闭(running 时等轮完)
|
|
1525
1560
|
*/
|
|
1526
|
-
async closeSubagent(record: ExecutionRecord, force: boolean): Promise<void> {
|
|
1561
|
+
private async closeSubagent(record: ExecutionRecord, force: boolean): Promise<void> {
|
|
1527
1562
|
this.assertReady();
|
|
1528
1563
|
if (record.status === "running") {
|
|
1529
1564
|
if (force) {
|
|
@@ -1590,7 +1625,7 @@ export class SubagentService {
|
|
|
1590
1625
|
store: this.store,
|
|
1591
1626
|
modelService: this.modelService,
|
|
1592
1627
|
pi: this.pi,
|
|
1593
|
-
emitUnregister: (id, st) => emitPendingUnregister(
|
|
1628
|
+
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
1594
1629
|
sessionDir: this.sessionsDir,
|
|
1595
1630
|
},
|
|
1596
1631
|
record,
|
|
@@ -1603,7 +1638,7 @@ export class SubagentService {
|
|
|
1603
1638
|
// (P-1 修复),正文空由 notifyClosed 的 emptyBody 参数显式表达。
|
|
1604
1639
|
// dedup 身份独立于轮次通知(notifyClosed 置 round=undefined),60s 窗内不被吞。
|
|
1605
1640
|
// 防重入:closeSubagent 对 closed record 幂等 no-op,本路径不会被二次进入。
|
|
1606
|
-
this.notifyClosed(record, true);
|
|
1641
|
+
this.notifyHost.notifyClosed(record, true);
|
|
1607
1642
|
}
|
|
1608
1643
|
|
|
1609
1644
|
/**
|
|
@@ -1641,7 +1676,7 @@ export class SubagentService {
|
|
|
1641
1676
|
await this.finalizeRecord(record, doneResult, "closed", "user-close");
|
|
1642
1677
|
// [C-1] 终态通知(设计 D2 路径①):与前置轮次通知(key=`id:round`)dedup 身份区分,
|
|
1643
1678
|
// 「最后一轮轮次通知 + 终态通知」两条都送达父 agent。
|
|
1644
|
-
this.notifyClosed(record);
|
|
1679
|
+
this.notifyHost.notifyClosed(record);
|
|
1645
1680
|
}
|
|
1646
1681
|
|
|
1647
1682
|
// ── 编排层专用接口(workflow 消费)──────────────────────
|
|
@@ -1651,7 +1686,7 @@ export class SubagentService {
|
|
|
1651
1686
|
*
|
|
1652
1687
|
* 与 execute() 的区别(D-A1):
|
|
1653
1688
|
* 1. 返回 workflow AgentResult(content 字段),非 ExecutionHandle
|
|
1654
|
-
* 2.
|
|
1689
|
+
* 2. 不经 chat 轮次 kick-off(detached 回注)→ 不注入 followUp 完成通知(BC-11,结果直接返回 workflow)
|
|
1655
1690
|
* 3. T2 删 sync 时 executeAndAwait 不受牵连(独立方法)
|
|
1656
1691
|
*
|
|
1657
1692
|
* 共享:runSpawn + ConcurrencyPool + record + pending emit(D-A4)。
|
|
@@ -1666,9 +1701,9 @@ export class SubagentService {
|
|
|
1666
1701
|
// [T4② / PS-4] 与 execute() 同款入口校验(两入口共享 runAndFinalize → armIdleTimer 链)。
|
|
1667
1702
|
this.assertIdleTimeoutMsSafe(opts);
|
|
1668
1703
|
|
|
1669
|
-
// ── BC-12 嵌套护栏:复用 execute()
|
|
1670
|
-
// [ALS 断裂修复]
|
|
1671
|
-
const parentNesting = this.
|
|
1704
|
+
// ── BC-12 嵌套护栏:复用 execute() 的嵌套上下文深度检查 ──
|
|
1705
|
+
// [ALS 断裂修复] current() 内含基线兜底(与 execute 同)。
|
|
1706
|
+
const parentNesting = this.execNesting.current();
|
|
1672
1707
|
const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
|
|
1673
1708
|
if (nestingDepth > MAX_FORK_DEPTH) {
|
|
1674
1709
|
throw new ForkDepthExceededError(
|
|
@@ -1681,7 +1716,7 @@ export class SubagentService {
|
|
|
1681
1716
|
|
|
1682
1717
|
// ── 步骤 2: RECORD 创建(mode="background" 进池)──
|
|
1683
1718
|
const record = this.createRecordForMode(identity, opts, "background");
|
|
1684
|
-
emitPendingRegister(
|
|
1719
|
+
this.notifyHost.emitPendingRegister(record.id, record.agent);
|
|
1685
1720
|
|
|
1686
1721
|
// ── 步骤 2.5: worktree creation (only worktree===true; handle injection is execute()'s path) ──
|
|
1687
1722
|
// Workflow path receives boolean only (AgentCallOpts.worktree: boolean) — WorktreeHandle is a
|
|
@@ -1764,59 +1799,138 @@ export class SubagentService {
|
|
|
1764
1799
|
// ── 状态查询(TUI 调)──────────────────────────────────
|
|
1765
1800
|
|
|
1766
1801
|
/** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
|
|
1767
|
-
onChange(listener: () => void): () => void {
|
|
1802
|
+
private onChange(listener: () => void): () => void {
|
|
1768
1803
|
return this.store.onChange(listener);
|
|
1769
1804
|
}
|
|
1770
1805
|
|
|
1771
|
-
|
|
1772
|
-
listRunning
|
|
1773
|
-
return this.store.listRunning();
|
|
1774
|
-
}
|
|
1806
|
+
// [D4] listRunning 已删除:零生产调用方(TUI 计数经 collectRecords / notify-host 的
|
|
1807
|
+
// piAdapter 直调 store.listRunning 覆盖),唯一消费是初始空态单测——保留 store 层方法。
|
|
1775
1808
|
|
|
1776
1809
|
/** 合并内存(running) + 磁盘(session.jsonl 重建) record(/subagents list + tool list 消费)。
|
|
1777
1810
|
* 按 rootSessionId 过滤:根进程=本 session(sessionRootId===sessionId);
|
|
1778
1811
|
* 子进程=env 贯穿的真 ROOT(sessionRootId≠sessionId)→ 看到整棵 ROOT 树(决策 3)。
|
|
1779
1812
|
* [perf] 磁盘源为 light(头部 identity + 状态,无 eventLog/result/turns 等重数据)
|
|
1780
1813
|
* ——列表/补全/hasRunning 够用;详情场景调 getFullRecord(id) 懒加载补齐。 */
|
|
1781
|
-
collectRecords(limit: number, statusFilter: StatusFilter = "all"): SubagentRecord[] {
|
|
1814
|
+
private collectRecords(limit: number, statusFilter: StatusFilter = "all"): SubagentRecord[] {
|
|
1782
1815
|
return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? undefined);
|
|
1783
1816
|
}
|
|
1784
1817
|
|
|
1818
|
+
/**
|
|
1819
|
+
* collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
|
|
1820
|
+
* startHandler 缺省 collect 解析用)。
|
|
1821
|
+
* config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
|
|
1822
|
+
* 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
|
|
1823
|
+
* reloadGlobalConfig 刷新)。
|
|
1824
|
+
*/
|
|
1825
|
+
/** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
|
|
1826
|
+
* budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
|
|
1827
|
+
private collectSyncSection() {
|
|
1828
|
+
return this.modelService.getGlobalConfig().collectSync;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
getCollectSyncDefault(): "async" | "sync" {
|
|
1832
|
+
return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1785
1835
|
/** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
|
|
1786
1836
|
* 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
|
|
1787
1837
|
* 返回 undefined:id 不存在于内存与磁盘。 */
|
|
1788
|
-
getFullRecord(id: string): SubagentRecord | undefined {
|
|
1838
|
+
private getFullRecord(id: string): SubagentRecord | undefined {
|
|
1789
1839
|
return this.store.getFullRecord(id);
|
|
1790
1840
|
}
|
|
1791
1841
|
|
|
1792
1842
|
// ── 执行内部:身份解析 + record 创建 ──────────
|
|
1793
1843
|
|
|
1794
|
-
/** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。
|
|
1795
|
-
|
|
1844
|
+
/** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。
|
|
1845
|
+
*
|
|
1846
|
+
* [u-h2] pi 未命中跨引擎候选(D2-4):resolveModel 抛 notFoundError(pi registry
|
|
1847
|
+
* 全等裁决未命中)时反查其他已注册引擎清单,唯一命中则追加「该 id 属于引擎 X」
|
|
1848
|
+
* 候选段(场景 3);其余裁决失败(孪生歧义/auth)与命中路径原样返回(零回归)。
|
|
1849
|
+
* execute() 与 executeAndAwait() 两个派发路径共享本方法,故 chat 与 workflow 域的
|
|
1850
|
+
* pi 校验同享场景 3 文案。
|
|
1851
|
+
*/
|
|
1852
|
+
private async resolveIdentity(
|
|
1853
|
+
opts: ExecuteOptions,
|
|
1854
|
+
pre?: { agent: string; agentConfig: AgentConfig | undefined },
|
|
1855
|
+
): Promise<ResolvedIdentity> {
|
|
1796
1856
|
// agentRef 语义(S2):agent 参数 = .md 绝对路径;不传 = 不加载 agentConfig,
|
|
1797
1857
|
// 直接用 override → 主 agent model。DEFAULT_AGENT_NAME 仅作 record 显示名
|
|
1798
1858
|
// (TUI 层 extractAgentName 共用,保证显示一致)。
|
|
1799
|
-
const agent = opts.agent ?? DEFAULT_AGENT_NAME;
|
|
1859
|
+
const agent = pre?.agent ?? opts.agent ?? DEFAULT_AGENT_NAME;
|
|
1800
1860
|
// 显式 agent ref(用户点名)失败必须报错,不静默降级:无 require 的 loadByPath
|
|
1801
1861
|
// 对相对路径/裸名/文件缺失都返回 undefined → agentConfig undefined → resolveModel
|
|
1802
1862
|
// 静默回落 override→主 agent model,用户拿到的 subagent 无 systemPrompt/工具白名单
|
|
1803
1863
|
// 且零反馈。require:true 让失败抛出带 <available_subagents> 指引的错误(对齐
|
|
1804
1864
|
// workflow name not found 反馈风格);不传 agent = 默认 general-purpose 语义,
|
|
1805
1865
|
// agentConfig 保持 undefined(合法缺省,走 override → ctxModel 兑底)。
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1866
|
+
// [u-h2 D2-1] execute() 已在路由前解析 agentConfig(pre 通道),此处复用不二次加载。
|
|
1867
|
+
const agentConfig = pre
|
|
1868
|
+
? pre.agentConfig
|
|
1869
|
+
: opts.agent
|
|
1870
|
+
? this.modelService.getRequiredAgentConfig(opts.agent)
|
|
1871
|
+
: undefined;
|
|
1872
|
+
|
|
1873
|
+
let resolved: ResolvedModel;
|
|
1874
|
+
try {
|
|
1875
|
+
resolved = this.modelService.resolveModel(
|
|
1876
|
+
opts.agent ?? "",
|
|
1877
|
+
{ model: opts.model, thinkingLevel: opts.thinkingLevel },
|
|
1878
|
+
opts.ctxModel,
|
|
1879
|
+
agentConfig,
|
|
1880
|
+
);
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
throw withCrossEngineHint(err, listEngines(), (id) => {
|
|
1883
|
+
try {
|
|
1884
|
+
return getEngine(id);
|
|
1885
|
+
} catch {
|
|
1886
|
+
return undefined; // 清单快照与注册表并发变化的防御:取不到引擎按未注册处理
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1816
1890
|
|
|
1817
1891
|
return { agent, agentConfig, resolved };
|
|
1818
1892
|
}
|
|
1819
1893
|
|
|
1894
|
+
/**
|
|
1895
|
+
* [u-h2 D2-1③] 非 pi 引擎的 identity 解析:跳过 pi registry 三层解析(ctxModel 主
|
|
1896
|
+
* agent model 不透传——主 agent 的 pi id 对目标引擎大概率无效,缺省语义归引擎)。
|
|
1897
|
+
*
|
|
1898
|
+
* 逐层语义(设计 D2-1 归趋表):
|
|
1899
|
+
* - model 源 = engineModel(调用参数 opts.model > agentConfig.model frontmatter,
|
|
1900
|
+
* agent 作者声明不忽略——配错在 validateModel 同步报错,不落引擎缺省静默续跑);
|
|
1901
|
+
* - 无显式 model → 校验/留痕走引擎缺省(validateModel(undefined) 的 canonicalRef);
|
|
1902
|
+
* - thinkingLevel 直接透传(引擎中立参数,不涉 registry)。
|
|
1903
|
+
*
|
|
1904
|
+
* 引擎未实现 validateModel 时 modelRef 原样透传(其 prepare 期校验兜底,现状语义)。
|
|
1905
|
+
*/
|
|
1906
|
+
private resolveIdentityForEngine(
|
|
1907
|
+
engine: EnginePort,
|
|
1908
|
+
engineModel: string | undefined,
|
|
1909
|
+
agent: string,
|
|
1910
|
+
agentConfig: AgentConfig | undefined,
|
|
1911
|
+
opts: ExecuteOptions,
|
|
1912
|
+
): ResolvedIdentity {
|
|
1913
|
+
const canonical = validateModelForEngine(engine, engineModel);
|
|
1914
|
+
// record.model 留痕:canonical(引擎裁决全名,含短名→缺省 provider 归一化);
|
|
1915
|
+
// 引擎未实现校验面且无显式 model 时为空串(记录形态退化,生产不可达——注册表
|
|
1916
|
+
// 内非 pi 引擎均实现 validateModel;防御性拼接避免 throw 打断兜底语义)。
|
|
1917
|
+
const modelStr = canonical ?? engineModel ?? "";
|
|
1918
|
+
const slashIdx = modelStr.indexOf("/");
|
|
1919
|
+
return {
|
|
1920
|
+
agent,
|
|
1921
|
+
agentConfig,
|
|
1922
|
+
resolved: {
|
|
1923
|
+
model: {
|
|
1924
|
+
id: slashIdx > 0 ? modelStr.slice(slashIdx + 1) : "",
|
|
1925
|
+
name: modelStr,
|
|
1926
|
+
provider: slashIdx > 0 ? modelStr.slice(0, slashIdx) : modelStr,
|
|
1927
|
+
reasoning: false,
|
|
1928
|
+
},
|
|
1929
|
+
thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
1930
|
+
},
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1820
1934
|
/** 步骤 2:按 mode 生成 id + controller,创建 record 并注册。
|
|
1821
1935
|
* [L-1] ExecutionMode 类型固定 "background"(sync 已删除),id/controller 分支简化。 */
|
|
1822
1936
|
private createRecordForMode(
|
|
@@ -1829,11 +1943,11 @@ export class SubagentService {
|
|
|
1829
1943
|
const controller = new AbortController();
|
|
1830
1944
|
|
|
1831
1945
|
// 从 async 调用链读父执行上下文:主 session 链上无 store → 顶层 record;
|
|
1832
|
-
// B run()
|
|
1946
|
+
// B run() 期间包了嵌套上下文,B 内创建 C 时读到 B → C.parentRecordId=B.id, C.depth=B.depth+1。
|
|
1833
1947
|
// depth 语义:顶层(无父)=0;有父=父 depth+1。靠 recordId 是否存在区分,不用负数魔数。
|
|
1834
|
-
// [ALS 断裂修复]
|
|
1835
|
-
//
|
|
1836
|
-
const parentCtx = this.
|
|
1948
|
+
// [ALS 断裂修复] current() 内含基线兜底——本进程的身份在 initSession 已确定(env 注入),
|
|
1949
|
+
// 任何上下文下都能正确挂父链。
|
|
1950
|
+
const parentCtx = this.execNesting.current();
|
|
1837
1951
|
const parentRecordId = parentCtx?.recordId;
|
|
1838
1952
|
const depth = parentCtx ? parentCtx.depth + 1 : 0;
|
|
1839
1953
|
|
|
@@ -1854,6 +1968,10 @@ export class SubagentService {
|
|
|
1854
1968
|
// 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
|
|
1855
1969
|
engine: opts.engine,
|
|
1856
1970
|
engineFallback: opts.engineFallback,
|
|
1971
|
+
// subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
|
|
1972
|
+
// 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
|
|
1973
|
+
// undefined = async(缺省语义,旧记录零迁移)。
|
|
1974
|
+
collectMode: opts.collect === "sync" ? "sync" : undefined,
|
|
1857
1975
|
controller,
|
|
1858
1976
|
});
|
|
1859
1977
|
|
|
@@ -1871,70 +1989,113 @@ export class SubagentService {
|
|
|
1871
1989
|
// ── 引擎分支(D4/D10:非 pi 引擎的 chat 域执行骨架,U0)──────────
|
|
1872
1990
|
|
|
1873
1991
|
/**
|
|
1874
|
-
*
|
|
1875
|
-
|
|
1992
|
+
* chat 域统一执行入口(D2 单轨:全引擎——含 pi——经此进入 EnginePort)。路由
|
|
1993
|
+
*(routeEngineForHost:三层 + pi 同步短路 + probe/守卫)已由 execute 完成——这里
|
|
1994
|
+
* 只剩 unsupported 预检 → identity(pi 链解析 / 非 pi 按目标引擎校验,
|
|
1995
|
+
* [u-h2 D2-1/D2-2])→ record 创建+盖章 → worktree → detached 引擎 run。
|
|
1876
1996
|
* 全部同步拒绝发生在 record 创建前(不产生孤儿 record)。
|
|
1877
1997
|
*/
|
|
1878
|
-
private executeViaEngine(
|
|
1998
|
+
private async executeViaEngine(
|
|
1879
1999
|
opts: ExecuteOptions,
|
|
1880
|
-
|
|
2000
|
+
preIdentity: { agent: string; agentConfig: AgentConfig | undefined },
|
|
1881
2001
|
route: EngineRouteResult,
|
|
1882
|
-
|
|
2002
|
+
mode: ExecutionMode,
|
|
2003
|
+
): Promise<ExecutionHandle> {
|
|
1883
2004
|
const engine = route.engine;
|
|
1884
|
-
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
//
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2005
|
+
// [D3-④ 预检 capabilities 化] 唯一实现 = common/capability-gate(capabilities
|
|
2006
|
+
// 驱动,含 maxTurns 扩位)。检查点钉死:execute/executeViaEngine 同步段、record
|
|
2007
|
+
// 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
|
|
2008
|
+
// 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
|
|
2009
|
+
// 检查若只落在 engine.run 内则拒绝异步化为「派发成功 + 静默失败 record」)。
|
|
2010
|
+
assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
|
|
2011
|
+
|
|
2012
|
+
// identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
|
|
2013
|
+
// - pi:pi 链三层解析在路由后执行(现状三层解析链行为零变化;含 resolveModel
|
|
2014
|
+
// 失败的跨引擎候选提示,D2-4);
|
|
2015
|
+
// - 非 pi:跳过 pi registry,model 按目标引擎校验(同步期 throw,record 创建前
|
|
2016
|
+
// ——场景 2 错误;ctxModel 不透传,缺省语义归引擎,D2-1③)。
|
|
2017
|
+
const isPiRoute = route.engineId === DEFAULT_ENGINE_ID;
|
|
2018
|
+
// [u-h2 D2-1③] 非 pi 的 model 源 = 调用参数 > agent .md frontmatter(作者声明不
|
|
2019
|
+
// 忽略)——frontmatter 声明须真正透传给引擎(taskSpec.model 消费 opts.model),
|
|
2020
|
+
// 不能只进 record 留痕;无显式 model 时引擎落自身缺省(validateModel(undefined) 裁决)。
|
|
2021
|
+
const engineModel = isPiRoute ? undefined : (opts.model ?? preIdentity.agentConfig?.model);
|
|
2022
|
+
const identity = isPiRoute
|
|
2023
|
+
? await this.resolveIdentity(opts, preIdentity)
|
|
2024
|
+
: this.resolveIdentityForEngine(engine, engineModel, preIdentity.agent, preIdentity.agentConfig, opts);
|
|
2025
|
+
|
|
2026
|
+
// record 盖章路由结果(D5 字节级守护的执行侧落点):
|
|
2027
|
+
// - pi 纯缺省/显式 pi:不盖 engine 键(pi record entry 序列化产物不得新增 engine
|
|
2028
|
+
// 键,undefined 经 JSON 省略)——与旧 pi 主路径 piOpts 剥离语义逐字节一致;
|
|
2029
|
+
// - pi 兜底:engine='pi' + engineFallback 留痕(engine = 实际执行引擎,from=请求
|
|
2030
|
+
// 引擎留痕);
|
|
2031
|
+
// - 非 pi:engine=route.engineId 显式留痕(+engineFallback 如有)+ model 覆写
|
|
2032
|
+
// (frontmatter 声明透传,u-h2 D2-1③)。
|
|
2033
|
+
const recordOpts: ExecuteOptions = isPiRoute
|
|
2034
|
+
? route.engineFallback !== undefined
|
|
2035
|
+
? { ...opts, engine: DEFAULT_ENGINE_ID, engineFallback: route.engineFallback }
|
|
2036
|
+
: opts.engine === undefined
|
|
2037
|
+
? opts
|
|
2038
|
+
: { ...opts, engine: undefined }
|
|
2039
|
+
: {
|
|
1891
2040
|
...opts,
|
|
2041
|
+
...(engineModel !== undefined ? { model: engineModel } : {}),
|
|
1892
2042
|
engine: route.engineId,
|
|
1893
2043
|
...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
);
|
|
1897
|
-
emitPendingRegister(this.pi, record.id, record.agent);
|
|
1898
|
-
this.kickOffEngineRun(record, opts, engine);
|
|
1899
|
-
return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: project(record) };
|
|
1900
|
-
}
|
|
2044
|
+
};
|
|
2045
|
+
const record = this.createRecordForMode(identity, recordOpts, mode);
|
|
2046
|
+
this.notifyHost.emitPendingRegister(record.id, record.agent);
|
|
1901
2047
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2048
|
+
// ── worktree 创建(仅 worktree===true 或已传入 handle 时)──
|
|
2049
|
+
// record 先创建,worktree 失败时可 finalizeFailed(record 已在 store 中)。
|
|
2050
|
+
// worktree 必须显式开启:worktree===true 创建新 worktree;worktree===undefined/false 不创建。
|
|
2051
|
+
// fork 不隐含 worktree(UC-1 fork 可独立使用,fork 仅继承上下文,在 parent cwd 跑)。
|
|
2052
|
+
// 非 pi 引擎带 worktree 已被上方预检同步拒绝(caps.sandbox='none'),此段实际仅
|
|
2053
|
+
// sandbox 能力引擎(pi:caps.sandbox='emulated')可达。
|
|
2054
|
+
let worktreeHandle: WorktreeHandle | undefined;
|
|
2055
|
+
if (typeof opts.worktree === "object") {
|
|
2056
|
+
// 传入的是已创建的 WorktreeHandle
|
|
2057
|
+
worktreeHandle = opts.worktree;
|
|
2058
|
+
} else if (opts.worktree === true) {
|
|
2059
|
+
// worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
|
|
2060
|
+
try {
|
|
2061
|
+
worktreeHandle = await this.worktreeManager.create(this.cwd, record.id);
|
|
2062
|
+
record.worktreeHandle = worktreeHandle;
|
|
2063
|
+
// [create-await 竞态守卫] create 的 await 窗口内 cancel/dispose 可 CAS 把 record
|
|
2064
|
+
// 转成 closed 终态——cancelBackground 当时读到的 worktreeHandle 可能仍是 undefined
|
|
2065
|
+
// (cleanup 被跳过)。赋值后同同步段检查终态:closed 则主动 cleanup(幂等,抢先的
|
|
2066
|
+
// fire-and-forget 清理无害)+ early-failed 返回,不进轮次 kick-off(避免子进程白跑)。
|
|
2067
|
+
// 实现约束:赋值 → 终态检查 → kick-off 必须在同一同步段,中间禁止插入 await。
|
|
2068
|
+
if (record.status === "closed") {
|
|
2069
|
+
await this.worktreeManager.cleanup(worktreeHandle);
|
|
2070
|
+
return this.buildEarlyFailedHandle(record);
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
// create 失败→不进入 run,finalizeFailed 统一收尾(含 emitPendingUnregister failed)
|
|
2074
|
+
const _result = await this.finalizeFailed(record, err);
|
|
2075
|
+
return this.buildEarlyFailedHandle(record);
|
|
2076
|
+
}
|
|
1925
2077
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2078
|
+
|
|
2079
|
+
if (isPiRoute) {
|
|
2080
|
+
// pi:record 耦合执行(runSpawn 直驱 record 记账)——预备轮次经 EnginePort 交接
|
|
2081
|
+
//(kickOffChatRound),编排收尾(notify/终态迁移)与旧 pi 主路径语义一致。
|
|
2082
|
+
this.kickOffChatRound(
|
|
2083
|
+
record,
|
|
2084
|
+
{ ...recordOpts, worktree: worktreeHandle },
|
|
2085
|
+
identity,
|
|
2086
|
+
this.buildSessionRunnerContext(opts.cwd),
|
|
2087
|
+
record.controller!.signal,
|
|
2088
|
+
PRIORITY_BACKGROUND,
|
|
1932
2089
|
);
|
|
2090
|
+
} else {
|
|
2091
|
+
// 非 pi 引擎:engine.run 自足执行(handle+outcome),编排侧 journal 接线 + 终态迁移
|
|
2092
|
+
this.kickOffEngineRun(record, recordOpts, engine);
|
|
1933
2093
|
}
|
|
2094
|
+
return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: project(record) };
|
|
1934
2095
|
}
|
|
1935
2096
|
|
|
1936
2097
|
/**
|
|
1937
|
-
* 非 pi 引擎的 detached 执行编排(与
|
|
2098
|
+
* 非 pi 引擎的 detached 执行编排(与 pi 轮次 kick-off 同构的 background 语义):
|
|
1938
2099
|
* pool 并发槽(maxConcurrent 对非 pi 引擎同样生效)→ journal 接线(D6 第②级:
|
|
1939
2100
|
* taskId=record.id,初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际
|
|
1940
2101
|
* 池 key——路径与 paths.ts 同源推导)→ engine.run(signal 接 record controller,
|
|
@@ -1962,7 +2123,7 @@ export class SubagentService {
|
|
|
1962
2123
|
await this.runEngineTask(record, opts, engine, signal);
|
|
1963
2124
|
// cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
|
|
1964
2125
|
if (record.closedReason !== "cancelled") {
|
|
1965
|
-
this.
|
|
2126
|
+
this.collectCoordinator.route(record);
|
|
1966
2127
|
}
|
|
1967
2128
|
} finally {
|
|
1968
2129
|
this.pool.release();
|
|
@@ -1974,7 +2135,7 @@ export class SubagentService {
|
|
|
1974
2135
|
* kickOffEngineRun 的 acquire 后主体:journal 接线(D6 第②级:taskId=record.id,
|
|
1975
2136
|
* 初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际池 key)→ engine.run
|
|
1976
2137
|
* (signal 接 record controller,kill-chain 两级生效)→ engineHandle 回填(终态迁移
|
|
1977
|
-
* 落 entry 前)→ 终态迁移。bg notify 归编排侧(与
|
|
2138
|
+
* 落 entry 前)→ 终态迁移。bg notify 归编排侧(与 pi 轮次收尾通知归编排对称)。
|
|
1978
2139
|
*/
|
|
1979
2140
|
private async runEngineTask(
|
|
1980
2141
|
record: ExecutionRecord,
|
|
@@ -1982,20 +2143,16 @@ export class SubagentService {
|
|
|
1982
2143
|
engine: EnginePort,
|
|
1983
2144
|
signal: AbortSignal | undefined,
|
|
1984
2145
|
): Promise<void> {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
});
|
|
1990
|
-
const retargetJournal = (poolKey: string): void => {
|
|
1991
|
-
journal.retarget(resolveJournalPath(getEngineDataDir(), engine.id, poolKey, record.id));
|
|
1992
|
-
};
|
|
2146
|
+
// [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
|
|
2147
|
+
//(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
|
|
2148
|
+
// 不传 forwardEvents。
|
|
2149
|
+
const journal = wireEventJournal({ engineId: engine.id, taskId: record.id });
|
|
1993
2150
|
// [R4 §3.4 不变量 3] 运行中句柄回填:create 应答后(远早于 run resolve)回填
|
|
1994
2151
|
// record.engineHandle 并经 reportRecordTransition 落 entry——运行中的 GUI 经 entry
|
|
1995
2152
|
// 重建 record 即拿到 ①②级读取钥匙(sessionRef/dbPath/poolKey/journalPath),
|
|
1996
2153
|
// 不再等终态回填(详情页中途打开可见当时进度快照)。journalPath 此时已是
|
|
1997
|
-
// retarget 后的最终路径(onPoolResolved 在 prepare
|
|
1998
|
-
//
|
|
2154
|
+
// retarget 后的最终路径(onPoolResolved 在 prepare 期先行触发,writer 是路径权威)。
|
|
2155
|
+
// 仅 chat 域接线——workflow 域 SAR 无运行中 record 读取方,刻意不做同类回填(防误扩展)。
|
|
1999
2156
|
const backfillEngineHandle = (partial: { sessionRef: Record<string, string>; poolKey: string }): void => {
|
|
2000
2157
|
if (record.engineHandle !== undefined && record.engineHandle.sessionRef["sessionId"] !== undefined) {
|
|
2001
2158
|
return; // 幂等守卫:终态回填已落(迟到重复回调不覆盖)
|
|
@@ -2008,14 +2165,15 @@ export class SubagentService {
|
|
|
2008
2165
|
this.store.reportRecordTransition(record);
|
|
2009
2166
|
};
|
|
2010
2167
|
// 对齐点③:journal 路径权威 = 引擎声明的池 key(writer 初始用占位,retarget 后
|
|
2011
|
-
// 与 handle.poolKey
|
|
2168
|
+
// 与 handle.poolKey 同源)。
|
|
2012
2169
|
const runCtx: RunContext = {
|
|
2013
2170
|
taskId: record.id,
|
|
2014
|
-
poolKey:
|
|
2171
|
+
poolKey: JOURNAL_INITIAL_POOL_KEY,
|
|
2015
2172
|
signal,
|
|
2016
2173
|
ctxModel: opts.ctxModel,
|
|
2017
|
-
onEvent:
|
|
2018
|
-
onPoolResolved:
|
|
2174
|
+
onEvent: journal.onEvent,
|
|
2175
|
+
onPoolResolved: journal.onPoolResolved,
|
|
2176
|
+
// [R4 §3.4 不变量 3] 运行中句柄回填通道(engine/port.ts RunContext.onHandleReady)
|
|
2019
2177
|
onHandleReady: backfillEngineHandle,
|
|
2020
2178
|
// D9①:路由层 fallback 留痕投影进 outcome(zcode 无独立 record 通路)
|
|
2021
2179
|
...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
|
|
@@ -2090,17 +2248,14 @@ export class SubagentService {
|
|
|
2090
2248
|
/** resume 选项(M2-B1):透传 runSpawn,重开已 idle 的 session 续聊。undefined = 新 session。 */
|
|
2091
2249
|
resume?: SpawnResumeOpts,
|
|
2092
2250
|
): Promise<AgentResult> {
|
|
2251
|
+
// [U04 阶段化提取] 主函数只留编排,按「装配(池槽/worktree/深度)→ 执行(ALS 包装
|
|
2252
|
+
// spawn)→ 回收(finally)→ 错误收口 → 终态收口」分段;行为逐字节不变。
|
|
2093
2253
|
const pooled = record.mode === "background";
|
|
2094
2254
|
let acquired = false;
|
|
2095
2255
|
if (pooled) {
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
} catch {
|
|
2100
|
-
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
2101
|
-
if (signal?.aborted) return this.finalizeAborted(record);
|
|
2102
|
-
return this.finalizeFailed(record, new Error("aborted"));
|
|
2103
|
-
}
|
|
2256
|
+
const acquireFailure = await this.acquirePoolOrFinalize(record, signal, priority);
|
|
2257
|
+
if (acquireFailure !== undefined) return acquireFailure;
|
|
2258
|
+
acquired = true;
|
|
2104
2259
|
}
|
|
2105
2260
|
// onEvent 直通(原此处曾有 onUpdate(project(record)) 节流回流包装——生产死路径,
|
|
2106
2261
|
// 三调用点恒 onUpdate: undefined、仅测试触达,已按 swf-perf-impl ledger #22 删除
|
|
@@ -2108,78 +2263,24 @@ export class SubagentService {
|
|
|
2108
2263
|
// ExecuteOptions.onUpdate 字段一并删除,未来误用将编译期失败而非静默无效。
|
|
2109
2264
|
const onEvent = rawOnEvent;
|
|
2110
2265
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
if (typeof opts.worktree === "object") {
|
|
2114
|
-
worktreeHandle = opts.worktree;
|
|
2115
|
-
}
|
|
2116
|
-
// [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。
|
|
2117
|
-
const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
|
|
2118
|
-
const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
|
|
2266
|
+
const worktreeHandle = this.resolveWorktreeHandle(opts);
|
|
2267
|
+
const { parentDepth, effectiveDepth } = this.resolveForkDepths(opts);
|
|
2119
2268
|
|
|
2120
2269
|
let result: AgentResult;
|
|
2121
2270
|
try {
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
this.execCtxAls.run(
|
|
2126
|
-
{ recordId: record.id, depth: record.depth },
|
|
2127
|
-
() => runSpawn(record, opts.task, {
|
|
2128
|
-
resolved: identity.resolved,
|
|
2129
|
-
agentConfig: identity.agentConfig,
|
|
2130
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
2131
|
-
skillPath: opts.skillPath,
|
|
2132
|
-
schema: opts.schema,
|
|
2133
|
-
schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
|
|
2134
|
-
maxTurns: opts.maxTurns,
|
|
2135
|
-
graceTurns: opts.graceTurns,
|
|
2136
|
-
signal,
|
|
2137
|
-
onEvent,
|
|
2138
|
-
stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
|
|
2139
|
-
fork: opts.fork,
|
|
2140
|
-
// [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
|
|
2141
|
-
// opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
|
|
2142
|
-
forkSource: opts.forkFromSessionFile,
|
|
2143
|
-
worktree: worktreeHandle,
|
|
2144
|
-
parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
|
|
2145
|
-
}, ctx, resume),
|
|
2146
|
-
),
|
|
2271
|
+
result = await this.runSpawnNested(
|
|
2272
|
+
record, opts, ctx, identity, signal, onEvent, stream, resume,
|
|
2273
|
+
worktreeHandle, parentDepth, effectiveDepth,
|
|
2147
2274
|
);
|
|
2148
2275
|
} catch (err) {
|
|
2149
2276
|
// run() 正常路径不抛错,但创建期异常(createAndConfigureSession 失败)
|
|
2150
2277
|
// 会逃逸出 run() —— 合成 failed result + 收尾。
|
|
2151
2278
|
// swallow(不 re-throw):sync 调用方拿到合成 failed result,background 的
|
|
2152
2279
|
// .then 正常跑 notify。避免异常逃逸到 tool 层 + record 卡 running。
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
//(可恢复),让 agent 可重试 message 或 close。与一次性模式(finalizeFailed 终态销毁)区分。
|
|
2156
|
-
if (record.chatMode) {
|
|
2157
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2158
|
-
const failedResult: AgentResult = {
|
|
2159
|
-
text: "",
|
|
2160
|
-
turns: record.turnCount,
|
|
2161
|
-
durationMs: Date.now() - record.startedAt,
|
|
2162
|
-
success: false,
|
|
2163
|
-
error: errMsg,
|
|
2164
|
-
sessionId: record.id,
|
|
2165
|
-
toolCalls: [],
|
|
2166
|
-
};
|
|
2167
|
-
if (tryTransition(record, "closed", "gc")) {
|
|
2168
|
-
// 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
|
|
2169
|
-
await this.finalizeRoundToIdle(record, failedResult);
|
|
2170
|
-
}
|
|
2171
|
-
return failedResult;
|
|
2172
|
-
}
|
|
2173
|
-
result = await this.finalizeFailed(record, err);
|
|
2174
|
-
return result;
|
|
2280
|
+
if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
|
|
2281
|
+
return this.finalizeFailed(record, err);
|
|
2175
2282
|
} finally {
|
|
2176
|
-
|
|
2177
|
-
// 清除 streaming widget(subagent 终态,幂等)
|
|
2178
|
-
stream?.dispose();
|
|
2179
|
-
// [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
|
|
2180
|
-
// MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
|
|
2181
|
-
// execute() 新建 record 不在集合,delete 是 no-op。
|
|
2182
|
-
this.resumesInFlight.delete(record.id);
|
|
2283
|
+
this.releaseRoundResources(record, pooled && acquired, stream);
|
|
2183
2284
|
}
|
|
2184
2285
|
|
|
2185
2286
|
// [V2 决策 2/3] chatMode 首轮闭环:runSpawn 因 agent_settled 提前 resolve(onRoundSettled
|
|
@@ -2187,97 +2288,320 @@ export class SubagentService {
|
|
|
2187
2288
|
// 不进下方 chatMode 分流(那是 close 后 done/failed/cancelled 终态化的,走 finalizeRoundToIdle
|
|
2188
2289
|
// / finalizeRecord)。tryTransition(idle→done) 天然失败(要求 status==="running"),此处显式
|
|
2189
2290
|
// early return 让语义清晰 + 防状态机未来改动。防 double-notify 由 notifier dedup 兜底
|
|
2190
|
-
//(同 id:round 60s 内吞,
|
|
2291
|
+
//(同 id:round 60s 内吞,chat 轮次收尾 .then 的 notify 是 no-op,见 notifier.ts L122)。
|
|
2191
2292
|
if (record.chatMode && isIdle(record)) {
|
|
2192
2293
|
return result;
|
|
2193
2294
|
}
|
|
2194
2295
|
|
|
2195
2296
|
// v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
|
|
2196
2297
|
const aborted = signal?.aborted === true;
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2298
|
+
await this.settleFinalOutcome(record, result, aborted, this.deriveClosedReason(aborted, result.success));
|
|
2299
|
+
return result;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
/** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
|
|
2303
|
+
* 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
|
|
2304
|
+
* 不触发轮次资源回收——与原控制流逐字节一致)。 */
|
|
2305
|
+
private async acquirePoolOrFinalize(
|
|
2306
|
+
record: ExecutionRecord,
|
|
2307
|
+
signal: AbortSignal | undefined,
|
|
2308
|
+
priority: number,
|
|
2309
|
+
): Promise<AgentResult | undefined> {
|
|
2310
|
+
try {
|
|
2311
|
+
await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
|
|
2312
|
+
} catch {
|
|
2313
|
+
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
2314
|
+
if (signal?.aborted) return this.finalizeAborted(record);
|
|
2315
|
+
return this.finalizeFailed(record, new Error("aborted"));
|
|
2316
|
+
}
|
|
2317
|
+
return undefined;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/** [U04 提取·装配] 解析 worktree 参数:boolean → WorktreeHandle | undefined(true/undefined 由 run 内部处理)。 */
|
|
2321
|
+
private resolveWorktreeHandle(opts: ExecuteOptions): WorktreeHandle | undefined {
|
|
2322
|
+
return typeof opts.worktree === "object" ? opts.worktree : undefined;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
/** [U04 提取·装配] [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。 */
|
|
2326
|
+
private resolveForkDepths(opts: ExecuteOptions): { parentDepth: number; effectiveDepth: number } {
|
|
2327
|
+
const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
|
|
2328
|
+
const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
|
|
2329
|
+
return { parentDepth, effectiveDepth };
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** [U04 提取·执行] runSpawn 的两层 ALS 包装(forkDepthAls 外层 + execNesting 内层)。 */
|
|
2333
|
+
private runSpawnNested(
|
|
2334
|
+
record: ExecutionRecord,
|
|
2335
|
+
opts: ExecuteOptions,
|
|
2336
|
+
ctx: SessionRunnerContext,
|
|
2337
|
+
identity: ResolvedIdentity,
|
|
2338
|
+
signal: AbortSignal | undefined,
|
|
2339
|
+
onEvent: ((event: AgentEvent) => void) | undefined,
|
|
2340
|
+
stream: SubagentStream | undefined,
|
|
2341
|
+
resume: SpawnResumeOpts | undefined,
|
|
2342
|
+
worktreeHandle: WorktreeHandle | undefined,
|
|
2343
|
+
parentDepth: number,
|
|
2344
|
+
effectiveDepth: number,
|
|
2345
|
+
): Promise<AgentResult> {
|
|
2346
|
+
// 嵌套上下文包在 forkDepthAls 内层:B run() 期间挂 {recordId:B.id,depth:B.depth},
|
|
2347
|
+
// B 内创建 C 时 createRecordForMode 读到 B → C 挂到 B 名下。两层 ALS 独立但同生命周期。
|
|
2348
|
+
return this.forkDepthAls.run(effectiveDepth, () =>
|
|
2349
|
+
this.execNesting.run(
|
|
2350
|
+
{ recordId: record.id, depth: record.depth },
|
|
2351
|
+
() => runSpawn(record, opts.task, {
|
|
2352
|
+
resolved: identity.resolved,
|
|
2353
|
+
agentConfig: identity.agentConfig,
|
|
2354
|
+
appendSystemPrompt: opts.appendSystemPrompt,
|
|
2355
|
+
skillPath: opts.skillPath,
|
|
2356
|
+
schema: opts.schema,
|
|
2357
|
+
schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
|
|
2358
|
+
maxTurns: opts.maxTurns,
|
|
2359
|
+
graceTurns: opts.graceTurns,
|
|
2360
|
+
signal,
|
|
2361
|
+
onEvent,
|
|
2362
|
+
stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
|
|
2363
|
+
fork: opts.fork,
|
|
2364
|
+
// [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
|
|
2365
|
+
// opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
|
|
2366
|
+
forkSource: opts.forkFromSessionFile,
|
|
2367
|
+
worktree: worktreeHandle,
|
|
2368
|
+
parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
|
|
2369
|
+
}, ctx, resume),
|
|
2370
|
+
),
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
|
|
2375
|
+
* 成功)、streaming widget 清除、在途 resume 守卫清除。 */
|
|
2376
|
+
private releaseRoundResources(
|
|
2377
|
+
record: ExecutionRecord,
|
|
2378
|
+
holdSlot: boolean,
|
|
2379
|
+
stream: SubagentStream | undefined,
|
|
2380
|
+
): void {
|
|
2381
|
+
if (holdSlot) this.pool.release();
|
|
2382
|
+
// 清除 streaming widget(subagent 终态,幂等)
|
|
2383
|
+
stream?.dispose();
|
|
2384
|
+
// [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
|
|
2385
|
+
// MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
|
|
2386
|
+
// execute() 新建 record 不在集合,delete 是 no-op。
|
|
2387
|
+
this.resumesInFlight.delete(record.id);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
/** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
|
|
2391
|
+
* 不销毁对话——回退 idle(可恢复),让 agent 可重试 message 或 close。与一次性模式
|
|
2392
|
+
* (finalizeFailed 终态销毁)区分。返回合成 failed result(swallow,不 re-throw)。 */
|
|
2393
|
+
private async finalizeChatSpawnFailure(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
2394
|
+
const errMsg = toErrorMessage(err);
|
|
2395
|
+
const failedResult: AgentResult = {
|
|
2396
|
+
text: "",
|
|
2397
|
+
turns: record.turnCount,
|
|
2398
|
+
durationMs: Date.now() - record.startedAt,
|
|
2399
|
+
success: false,
|
|
2400
|
+
error: errMsg,
|
|
2401
|
+
sessionId: record.id,
|
|
2402
|
+
toolCalls: [],
|
|
2403
|
+
};
|
|
2404
|
+
if (tryTransition(record, "closed", "gc")) {
|
|
2405
|
+
// 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
|
|
2406
|
+
await this.finalizeRoundToIdle(record, failedResult);
|
|
2407
|
+
}
|
|
2408
|
+
return failedResult;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/** [U04 提取·收口派生] closedReason 派生:aborted → cancelled;否则 success → user-close,!success → gc。 */
|
|
2412
|
+
private deriveClosedReason(aborted: boolean, success: boolean): ClosedReason {
|
|
2413
|
+
return aborted ? "cancelled" : success ? "user-close" : "gc";
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/** [U04 提取·终态收口] CAS 抢锁:抢到则按 chatMode 分流完整收尾;没抢到(cancel 已先设
|
|
2417
|
+
* closed+cancelled)则跳过。 */
|
|
2418
|
+
private async settleFinalOutcome(
|
|
2419
|
+
record: ExecutionRecord,
|
|
2420
|
+
result: AgentResult,
|
|
2421
|
+
aborted: boolean,
|
|
2422
|
+
closedReason: ClosedReason,
|
|
2423
|
+
): Promise<void> {
|
|
2424
|
+
if (!tryTransition(record, "closed", closedReason)) return;
|
|
2425
|
+
if (record.chatMode) {
|
|
2426
|
+
await this.settleChatRoundOutcome(record, result, aborted, closedReason);
|
|
2427
|
+
} else {
|
|
2428
|
+
await this.settleOneShotOutcome(record, result, aborted, closedReason);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/** [U04 提取·终态收口] chatMode 轮次分流(原 A/B 分支,De Morgan 等价拆分):
|
|
2433
|
+
* 成功轮(!aborted && success)与失败/取消轮(其补集 !success || aborted)各自处理
|
|
2434
|
+
* closeAfterRound 挂起语义。 */
|
|
2435
|
+
private async settleChatRoundOutcome(
|
|
2436
|
+
record: ExecutionRecord,
|
|
2437
|
+
result: AgentResult,
|
|
2438
|
+
aborted: boolean,
|
|
2439
|
+
closedReason: ClosedReason,
|
|
2440
|
+
): Promise<void> {
|
|
2441
|
+
if (!aborted && result.success) {
|
|
2442
|
+
if (record.closeAfterRound) {
|
|
2443
|
+
// close 优雅关闭(force:false):当前轮完成后终态化为 closed。
|
|
2444
|
+
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2233
2445
|
} else {
|
|
2234
|
-
//
|
|
2235
|
-
await this.
|
|
2446
|
+
// 对话模式轮次成功完成 → 保持 running(旧 idle 折入 running,finalizeRoundToIdle 设回 running)。
|
|
2447
|
+
await this.finalizeRoundToIdle(record, result);
|
|
2448
|
+
}
|
|
2449
|
+
} else {
|
|
2450
|
+
// MF-6:chatMode 轮次失败/取消不销毁对话——回退 running-resumable(旧 idle,可恢复)。
|
|
2451
|
+
if (record.closeAfterRound) {
|
|
2452
|
+
// [M5] 优雅关闭挂起的失败/取消轮:轮已完成即兑现 close 意图终态化(含本轮 result),
|
|
2453
|
+
// 不再回退 resumable——否则标志残留到下一轮,record 已被 tool 谎报 closed。
|
|
2454
|
+
await this.consumeCloseAfterRound(record, result, closedReason);
|
|
2455
|
+
} else {
|
|
2456
|
+
await this.finalizeRoundToIdle(record, result);
|
|
2236
2457
|
}
|
|
2237
2458
|
}
|
|
2238
|
-
return result;
|
|
2239
2459
|
}
|
|
2240
2460
|
|
|
2241
|
-
/**
|
|
2242
|
-
|
|
2461
|
+
/** [U04 提取·终态收口] one-shot(非 chatMode)分流(原 C/D 分支):成功轮消费
|
|
2462
|
+
* closeAfterRound 挂起标志;失败/取消一次性销毁。 */
|
|
2463
|
+
private async settleOneShotOutcome(
|
|
2464
|
+
record: ExecutionRecord,
|
|
2465
|
+
result: AgentResult,
|
|
2466
|
+
aborted: boolean,
|
|
2467
|
+
closedReason: ClosedReason,
|
|
2468
|
+
): Promise<void> {
|
|
2469
|
+
if (!aborted && result.success) {
|
|
2470
|
+
if (record.closeAfterRound) {
|
|
2471
|
+
// [M5] 非 chatMode(one-shot)busy 时 close(force:false) 置的标志在本轮完成时消费
|
|
2472
|
+
// 终态化(对齐 close schema 文案 "release its resources")。旧代码走
|
|
2473
|
+
// finalizeRoundToIdle 不消费——tool 返回 {closed:true} 谎报,record 永久
|
|
2474
|
+
// running-resumable、5min idle timer 杀进程、期间还能继续收 message。
|
|
2475
|
+
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2476
|
+
} else {
|
|
2477
|
+
// [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
|
|
2478
|
+
await this.finalizeRoundToIdle(record, result);
|
|
2479
|
+
}
|
|
2480
|
+
} else {
|
|
2481
|
+
// 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
|
|
2482
|
+
await this.finalizeRecord(record, result, "closed", closedReason);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
|
|
2487
|
+
* 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
|
|
2488
|
+
private async consumeCloseAfterRound(
|
|
2489
|
+
record: ExecutionRecord,
|
|
2490
|
+
result: AgentResult,
|
|
2491
|
+
reason: ClosedReason,
|
|
2492
|
+
): Promise<void> {
|
|
2493
|
+
record.closeAfterRound = undefined;
|
|
2494
|
+
await this.finalizeRecord(record, result, "closed", reason);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/**
|
|
2498
|
+
* pi chat 域轮次的 detached 编排(D2 单轨——旧 pi 主路径绕过 EnginePort 的 detached
|
|
2499
|
+
* 编排删除后的 EnginePort 化形态):
|
|
2500
|
+
* 预备轮次(record/opts/identity/host ctx/stream/resume)挂载交接 Map → engine.run 经
|
|
2501
|
+
* ctx.taskId 消费(PiEngine chat 分支回调 runChatRound——即 runAndFinalize 链,含 pool
|
|
2502
|
+
* 并发槽 acquire/release 与杀链,行为零变化)→ 完成回注 notify(cancel 抢先时跳过,
|
|
2503
|
+
* 与旧 .then 语义一致)。
|
|
2504
|
+
* stream 在挂载前同步创建(spawn 前置不变量,stream-sink 退役步骤 2 守护)。
|
|
2505
|
+
* chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
|
|
2506
|
+
* 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
|
|
2507
|
+
*/
|
|
2508
|
+
private kickOffChatRound(
|
|
2243
2509
|
record: ExecutionRecord,
|
|
2244
2510
|
opts: ExecuteOptions,
|
|
2245
|
-
ctx: SessionRunnerContext,
|
|
2246
2511
|
identity: ResolvedIdentity,
|
|
2512
|
+
ctx: SessionRunnerContext,
|
|
2247
2513
|
signal: AbortSignal | undefined,
|
|
2248
2514
|
priority: number,
|
|
2249
|
-
/** resume 选项(M2-B1
|
|
2515
|
+
/** resume 选项(M2-B1):透传轮次执行→runSpawn。undefined = 新 session。 */
|
|
2250
2516
|
resume?: SpawnResumeOpts,
|
|
2251
2517
|
): void {
|
|
2252
2518
|
// 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
|
|
2253
2519
|
// 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
|
|
2254
2520
|
const stream = createBackgroundStream(record.id, this.streamSink, ctx.mode, process.env);
|
|
2255
2521
|
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2522
|
+
this.chatRoundTickets.set(record.id, { record, opts, identity, ctx, signal, priority, stream, resume });
|
|
2523
|
+
void this.chatPiEngine
|
|
2524
|
+
.run(
|
|
2525
|
+
// task 形参仅满足 port 签名——chat 轮次由 ticket lossless 携带
|
|
2526
|
+
//(identity/forkFromSessionFile/host ctx 是 host 编排件,不入任务声明
|
|
2527
|
+
// AgentCallOpts——[u-3b D6] 双形态保留的结构性裁决见 ChatRoundTicket 注释)
|
|
2528
|
+
executeOptionsToEngineTaskSpec(opts),
|
|
2529
|
+
{ taskId: record.id, poolKey: PI_POOL_KEY, signal, stream },
|
|
2530
|
+
)
|
|
2260
2531
|
.then(() => {
|
|
2261
2532
|
// background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
|
|
2262
2533
|
// 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
|
|
2263
2534
|
// 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
|
|
2264
2535
|
// 表达)——迟到的完成回注不注入(可能已切换的)新 session。
|
|
2265
2536
|
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2266
|
-
this.
|
|
2537
|
+
this.collectCoordinator.route(record);
|
|
2267
2538
|
}
|
|
2268
2539
|
})
|
|
2269
2540
|
.catch((err: unknown) => {
|
|
2270
|
-
// detached
|
|
2541
|
+
// detached 吞错:轮次执行内部已 finalize record(含 emitPendingUnregister),
|
|
2271
2542
|
// 且 finalizeRecord 的 manifest 写入已降级为 best-effort(失败仅 logger.error + appendEntry,
|
|
2272
2543
|
// 不外抛)。因此此处不应走到——但作为最后一道兼底,记录调试日志后吞下,不外抛。
|
|
2273
2544
|
// 完成通知由 finalizeRecord 内的 emitPendingUnregister 承担(pending-notifications 消费)。
|
|
2274
2545
|
// cancel 抢先时 status=cancelled,cancelBackground 自己 emit,此处无需重复。
|
|
2546
|
+
// 交接包未被消费(engine.run 前置 throw 的极端形态)时的泄漏兜底。
|
|
2547
|
+
this.chatRoundTickets.delete(record.id);
|
|
2275
2548
|
if (err instanceof Error) {
|
|
2276
2549
|
logger.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
|
|
2277
2550
|
}
|
|
2278
2551
|
});
|
|
2279
2552
|
}
|
|
2280
2553
|
|
|
2554
|
+
/** PiEngine 的编排服务适配器(chat 绑定):闭包持有本实例的编排面——chat 轮次交接
|
|
2555
|
+
* 由此成为 PiEngineService 的可选面(SAR 直绑 Service 的 workflow 实例不提供,
|
|
2556
|
+
* Service 公共接口不为引擎内部交接扩面)。 */
|
|
2557
|
+
private piEngineServiceAdapter(): PiEngineService {
|
|
2558
|
+
return {
|
|
2559
|
+
executeAndAwait: (opts, signal, onEvent, stream) => this.executeAndAwait(opts, signal, onEvent, stream),
|
|
2560
|
+
getRecordForAction: (id) => this.getRecordForAction(id),
|
|
2561
|
+
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
2562
|
+
cancel: (id) => this.cancel(id),
|
|
2563
|
+
collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
|
|
2564
|
+
takeChatRound: (taskId) => this.takeChatTicket(taskId),
|
|
2565
|
+
runChatRound: (ticket) => this.runTicketRound(ticket),
|
|
2566
|
+
resumeChatRound: (record, text) => this.resumeColdRound(record, text),
|
|
2567
|
+
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
/** [D4 聚合连带] PiEngineService 的显式结构视图(registration / SAR 直绑消费)。
|
|
2572
|
+
* 原两绑定点把 SubagentService 整体结构化兼容为 PiEngineService——依赖查询/交互
|
|
2573
|
+
* 方法 public;聚合收窄后改经本视图显式适配(成员集合与原直绑等价,行为零差异)。
|
|
2574
|
+
* getter 形态:face 视图(惰性构造的适配对象)而非动作方法。 */
|
|
2575
|
+
get asEngineService(): PiEngineService {
|
|
2576
|
+
return this.piEngineServiceAdapter();
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/** chat 域轮次交接的消费侧(PiEngine.run 回调):一次性取走,防重复消费。私有名与
|
|
2580
|
+
* PiEngineService 可选面成员不同名(历史约束:D4 聚合前的结构化直绑时代,私有同名
|
|
2581
|
+
* 成员会阻断 SubagentService 直绑;聚合后经 asEngineService 显式视图,约束已消失,
|
|
2582
|
+
* 不同名保留为与 adapter 显式映射一致的命名纪律)。 */
|
|
2583
|
+
private takeChatTicket(taskId: string): ChatRoundTicket | undefined {
|
|
2584
|
+
const ticket = this.chatRoundTickets.get(taskId);
|
|
2585
|
+
if (ticket !== undefined) this.chatRoundTickets.delete(taskId);
|
|
2586
|
+
return ticket;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/** PiEngine chat 分支的执行回调:原 runAndFinalize 直调链的 EnginePort 化落点
|
|
2590
|
+
* (编排归 Service——pool 槽 + runSpawn + 终态迁移,行为零变化)。 */
|
|
2591
|
+
private async runTicketRound(ticket: ChatRoundTicket): Promise<AgentResult> {
|
|
2592
|
+
return this.runAndFinalize(
|
|
2593
|
+
ticket.record,
|
|
2594
|
+
ticket.opts,
|
|
2595
|
+
ticket.ctx,
|
|
2596
|
+
ticket.identity,
|
|
2597
|
+
ticket.signal,
|
|
2598
|
+
ticket.priority,
|
|
2599
|
+
undefined,
|
|
2600
|
+
ticket.stream,
|
|
2601
|
+
ticket.resume,
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2281
2605
|
/**
|
|
2282
2606
|
* 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
|
|
2283
2607
|
* 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
|
|
@@ -2291,7 +2615,7 @@ export class SubagentService {
|
|
|
2291
2615
|
record.controller?.abort();
|
|
2292
2616
|
// [M6] 显式 kill + disarm:chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0)
|
|
2293
2617
|
// 返回,`opts.signal.removeEventListener("abort", onAbort)`(session-runner runSpawn 尾部)
|
|
2294
|
-
// 已移除 abort→kill listener;热路径续聊轮(
|
|
2618
|
+
// 已移除 abort→kill listener;热路径续聊轮(PiEngine 直接 stdin 写入)不再
|
|
2295
2619
|
// 进 runSpawn。此后 cancel 只有 controller.abort() 无人响应——record 已终态化 cancelled
|
|
2296
2620
|
// 但子进程继续跑完当前 turn(工具副作用继续发生),之后 agent_settled 还对已 archived
|
|
2297
2621
|
// record 触发脏通知(round+1 → 新 dedup key → "finished a round"),最终靠 5min idle
|
|
@@ -2341,9 +2665,9 @@ export class SubagentService {
|
|
|
2341
2665
|
}
|
|
2342
2666
|
}
|
|
2343
2667
|
// pending-notifications:cancel 注销(只记 registry 状态)
|
|
2344
|
-
emitPendingUnregister(
|
|
2345
|
-
// cancel
|
|
2346
|
-
this.
|
|
2668
|
+
this.notifyHost.emitPendingUnregister(record.id, "closed");
|
|
2669
|
+
// cancel 完成通知(与轮次收尾 .then 对称——cancel 抢先时 .then 跳过 notify)
|
|
2670
|
+
this.collectCoordinator.route(record);
|
|
2347
2671
|
return true;
|
|
2348
2672
|
}
|
|
2349
2673
|
|
|
@@ -2363,7 +2687,7 @@ export class SubagentService {
|
|
|
2363
2687
|
store: this.store,
|
|
2364
2688
|
modelService: this.modelService,
|
|
2365
2689
|
pi: this.pi,
|
|
2366
|
-
emitUnregister: (id, st) => emitPendingUnregister(
|
|
2690
|
+
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
2367
2691
|
sessionDir: this.sessionsDir,
|
|
2368
2692
|
},
|
|
2369
2693
|
record,
|
|
@@ -2388,7 +2712,7 @@ export class SubagentService {
|
|
|
2388
2712
|
store: this.store,
|
|
2389
2713
|
modelService: this.modelService,
|
|
2390
2714
|
pi: this.pi,
|
|
2391
|
-
emitUnregister: (id, st) => emitPendingUnregister(
|
|
2715
|
+
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
2392
2716
|
},
|
|
2393
2717
|
record,
|
|
2394
2718
|
result,
|
|
@@ -2399,7 +2723,7 @@ export class SubagentService {
|
|
|
2399
2723
|
* AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
|
|
2400
2724
|
* 继续返回(不 re-throw,swallow 策略)。 */
|
|
2401
2725
|
private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
2402
|
-
const errMsg =
|
|
2726
|
+
const errMsg = toErrorMessage(err);
|
|
2403
2727
|
// durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
|
|
2404
2728
|
const failedResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: errMsg, sessionId: record.id, toolCalls: [] };
|
|
2405
2729
|
// CAS 抢锁:抢到(status 仍 running)则完整收尾;没抢到(cancel 已先设 cancelled)跳过。
|
|
@@ -2502,74 +2826,11 @@ export class SubagentService {
|
|
|
2502
2826
|
// 落盘统一用 rootCwd 编码,ROOT 磁盘重建才扫得到深层 record(与 sessionRootId 同构)。
|
|
2503
2827
|
rootCwd: this.rootCwd,
|
|
2504
2828
|
// [V2 决策 2] chatMode 首轮闭环:agent_settled 时 session-runner 调本回调。
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2508
|
-
//
|
|
2509
|
-
|
|
2510
|
-
onRoundSettled: (record) => {
|
|
2511
|
-
// v4 B-1:status 保持 running(旧 idle 折入 running);session-runner 已 arm idle timer,
|
|
2512
|
-
// isIdle=true 让 notify 守卫放行(时序:armIdleTimer → onRoundSettled,见 session-runner.ts:670)。
|
|
2513
|
-
// round 可能初始 undefined(与 notifier.ts `record.round ?? 0` 兜底一致),
|
|
2514
|
-
// 首轮 0+1=1。round 是 notifier dedup key 的组成部分,递增后同 id 下一轮不被 60s dedup 吞。
|
|
2515
|
-
record.round = (record.round ?? 0) + 1;
|
|
2516
|
-
// [N2][增量] 轮次回复写点(增量语义):roundText 自 roundBaseTurnIndex 起派生本轮增量
|
|
2517
|
-
//(undefined 视为 0——首轮增量 = 全量,与改造前首轮逐字节一致)。成功轮次的 MF-2 原写点
|
|
2518
|
-
//(doFinalizeRoundToIdle)不可达——agent_settled 恒 arm idle timer → runAndFinalize 恒
|
|
2519
|
-
// early return。写入 record.result 后再 notify;本轮无非空增量且无 lastError(纯工具轮 /
|
|
2520
|
-
// interrupt 抢占轮 / 模型空回复)时固定占位 "(no output this round)"(D5:增量语义下沿用
|
|
2521
|
-
// 旧 record.result = 上一轮增量 → 本轮通知正文 = 上一轮内容,父 agent 误读为原样重复回复;
|
|
2522
|
-
// lastError 兜底保留让失败轮通知可读)。后续 closeAfterRoundSettled 的合成 result 读
|
|
2523
|
-
// record.result,同样携带本轮增量。
|
|
2524
|
-
const roundText = getFullTextFrom(record, record.roundBaseTurnIndex ?? 0);
|
|
2525
|
-
record.result = roundText ||
|
|
2526
|
-
(record.lastError ? `round did not complete: ${record.lastError}` : "(no output this round)");
|
|
2527
|
-
// 先送达本轮增量(notify),再推进 base / 消费 closeAfterRound——终态通知由
|
|
2528
|
-
// closeAfterRoundSettled / closeChatIdle 的 notifyClosed 显式发出(dedup 身份为裸 id,
|
|
2529
|
-
// 与本次 round notify 的 id:round key 区分),保证「本轮增量 + 终态通知」都送达;
|
|
2530
|
-
// kickOffBackground.then 的冷路径 notifyComplete 仍与本次 round notify 同 key 被 60s
|
|
2531
|
-
// dedup 吞(不构成第三条)。
|
|
2532
|
-
//
|
|
2533
|
-
// 幂等性(覆盖面如实限定):同步路径 at-least-once——notifyComplete(同步 void)抛错时
|
|
2534
|
-
// 推进/消费被跳过 → base 不推进 → 增量未消费,下轮 roundText 必含本轮文本(重发载体为
|
|
2535
|
-
// 后续轮次增量拼接)。kickOffBackground.then 的冷路径 notifyComplete 不构成重发通道
|
|
2536
|
-
// (notifier dedup.set 与 pending.splice 均先于 sendMessage,同 key `${id}:${round}`——
|
|
2537
|
-
// round 已递增——60s 窗内重入被吞)。异步 flush 窗口不保证:合并 timer armed(其他 busy
|
|
2538
|
-
// background 在场)或 isIdle 退避期间 notify 的『成功』只是入队,实际 sendMessage 发生在
|
|
2539
|
-
// base 推进之后的异步时机;该窗口进程崩溃或 sendMessage 失败 → 丢失不可重发(现状全量
|
|
2540
|
-
// 重发的次轮自愈在增量语义下消失),wave1 期恢复通道仅父 agent 经 /subagents 详情读取
|
|
2541
|
-
// record.result,wave2 指针行落地后补全。反序(先推进后 notify)在同步路径 notify 失败时
|
|
2542
|
-
// 静默丢增量且无任何重算机会,故 notify 后推进是定案。重复发送由 notifier dedup key
|
|
2543
|
-
// 60s 窗界定。
|
|
2544
|
-
this.notifyComplete(record);
|
|
2545
|
-
// R1 观测哨(不变式违反形态):推进前检查末 turn 未闭合且 text 非空——pi 现序下不可达
|
|
2546
|
-
//(带 usage 的 message_end 恒先于 turn_end,settle 时 turn 全闭合,见 types.ts
|
|
2547
|
-
// roundBaseTurnIndex 注释的行号锚定),ES1 单测自造事件序列锁不住 pi 层变化;pi 升级若
|
|
2548
|
-
// 改变 turn_end/agent_end 时序,此哨兵留痕(该形态下公式仍把文本计入本轮,不丢数据)。
|
|
2549
|
-
const lastTurn = record.turns[record.turns.length - 1];
|
|
2550
|
-
if (lastTurn !== undefined && !lastTurn.closed && lastTurn.text.length > 0) {
|
|
2551
|
-
logger.warn(
|
|
2552
|
-
`[subagents] round settle with unclosed non-empty turn (record=${record.id}, turnIndex=${record.turns.length - 1}) — pi turn_end/agent_end ordering may have changed`,
|
|
2553
|
-
);
|
|
2554
|
-
}
|
|
2555
|
-
// [增量] base 推进(notify 之后):下一轮增量从本轮边界起。滞后空 turn 不计入边界
|
|
2556
|
-
//(防御分支,留在下一轮增量内防丢文本——nextRoundBaseTurnIndex 注释)。
|
|
2557
|
-
record.roundBaseTurnIndex = nextRoundBaseTurnIndex(record);
|
|
2558
|
-
// 轮终迁移持久化(residual-fixes U3 补全):热路径轮终不经 doFinalizeRoundToIdle
|
|
2559
|
-
//(agent_settled 恒 arm idle timer → runAndFinalize 恒 early return,MF-2 原写点
|
|
2560
|
-
// 不可达)——不 appendEntry 则 runtime/W18 派生缓存不失效,renderer 停留在
|
|
2561
|
-
// register 快照(无 result),chat 等续聊的 waiting 形态显示不出来、spinner
|
|
2562
|
-
// 卡死。显式上报:entry 携带本轮 result/round/chatMode(§5.4:result 有值 +
|
|
2563
|
-
// chatMode=true → waiting)。closeAfterRound 的终态 entry 在此后追加,序不变。
|
|
2564
|
-
this.store.reportRecordTransition(record);
|
|
2565
|
-
// [M5] closeAfterRound 消费点:chatMode 每轮完成的统一汇聚点(热路径轮不经
|
|
2566
|
-
// runAndFinalize CAS 分支——agent_settled 恒 arm idle timer → runAndFinalize 恒
|
|
2567
|
-
// early return,旧消费点对 chatMode 不可达,标志置了无人消费、tool 谎报 closed:true)。
|
|
2568
|
-
if (record.closeAfterRound) {
|
|
2569
|
-
record.closeAfterRound = undefined;
|
|
2570
|
-
void this.closeAfterRoundSettled(record);
|
|
2571
|
-
}
|
|
2572
|
-
},
|
|
2829
|
+
// [D4-②] 业务闭包已拆至 round-settlement.ts(this.settleRound 字段工厂构造,
|
|
2830
|
+
// deps 回调注入 notifyComplete / reportRecordTransition / closeAfterRoundSettled,
|
|
2831
|
+
// 行为逐字节等价)——轻量 idle 化语义(round+=1 + notify 主 agent,不调
|
|
2832
|
+
// doFinalizeRoundToIdle)与增量派生/base 推进/哨兵的机制注释见该文件。
|
|
2833
|
+
onRoundSettled: this.settleRound,
|
|
2573
2834
|
};
|
|
2574
2835
|
}
|
|
2575
2836
|
}
|