@zhushanwen/subagent-core 0.5.1 → 0.7.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/README.md +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-Dv4QhSJ_.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +835 -668
- package/dist/{types-Dv4QhSJ_.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +835 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14123 -15467
- package/dist/index.d.cts +1179 -1540
- package/dist/index.d.ts +1179 -1540
- package/dist/index.js +7694 -10986
- package/dist.bundle/index.cjs +33485 -0
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/agent-registry.test.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +1 -1
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-budget.test.ts +291 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +316 -0
- package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +229 -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 +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +21 -34
- package/src/execution/__tests__/finalized-marker.test.ts +1 -1
- package/src/execution/__tests__/gc-timer.test.ts +8 -23
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/notify-batch.test.ts +500 -0
- package/src/execution/__tests__/notify-ledger.test.ts +27 -0
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/relay-env.test.ts +12 -1
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-file-gc.test.ts +1 -1
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +14 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +1102 -0
- package/src/execution/__tests__/tombstone-store.test.ts +1 -1
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
- 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 +6 -2
- package/src/execution/collect-coordinator.ts +200 -0
- package/src/execution/config.ts +77 -5
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +82 -55
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +8 -3
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/execution-record.ts +7 -0
- package/src/execution/finalize-record.ts +40 -11
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/notifier.ts +234 -25
- package/src/execution/notify-host.ts +21 -1
- package/src/execution/record-entry.ts +16 -0
- package/src/execution/record-store.ts +189 -31
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-actions-core.ts +62 -6
- package/src/execution/subagent-service.ts +1161 -406
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/sync-rebuild.ts +87 -0
- package/src/execution/types.ts +104 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/config-loader.test.ts +1 -1
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- 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__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
- package/src/orchestration/__tests__/script-generate.test.ts +5 -5
- package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
- package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/__tests__/atomic-write.test.ts +1 -1
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -134
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 执行编排 + 记录领域 Service(D4 按变化轴拆分后的编排核:execute/executeAndAwait 入口、
|
|
2
|
-
// record 生命周期、cancel)。通知簇 → notify-host.ts
|
|
3
|
-
//
|
|
2
|
+
// record 生命周期、cancel)。通知簇 → notify-host.ts;轮次结算闭包内联于
|
|
3
|
+
// settleChatRoundFromResponse(原 round-settlement.ts 已随 W3 inproc 删除收敛于此);冷路径复活 → cold-resurrect.ts。
|
|
4
4
|
// 上游:subagent-tool(execute/query/cancel)、TUI(onChange/collectRecords)。
|
|
5
5
|
// session_start 时经 initSession 注入 pi;modelRegistry/entries 归 ModelConfigService.initModel。
|
|
6
6
|
|
|
@@ -16,11 +16,14 @@ import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
|
|
|
16
16
|
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
17
17
|
import { removeAliveMarker } from "./alive-store.ts";
|
|
18
18
|
import { bestEffort } from "./best-effort.ts";
|
|
19
|
-
|
|
19
|
+
import { CollectCoordinator } from "./collect-coordinator.ts";
|
|
20
|
+
import { DEFAULT_COLLECT_SYNC } from "./config.ts";
|
|
21
|
+
// [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程);
|
|
22
|
+
// idle 相位帧(协议 host/roundLifecycle)= arm 锚点(W3 v1.x——引擎不实现 idle 定时器)。
|
|
20
23
|
// [T4②] DEFAULT_IDLE_TIMEOUT_MS:assertIdleTimeoutMsSafe 错误文案的缺省时长基准。
|
|
21
24
|
// ([M3] hasIdleTimer / hasLiveProcessHandle 的 piAdapter.hasRunningBackground 过滤随通知簇
|
|
22
25
|
// 在 notify-host.ts 消费,不经本文件。)
|
|
23
|
-
import { disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
|
|
26
|
+
import { armIdleTimer, disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
|
|
24
27
|
import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
25
28
|
import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
|
|
26
29
|
import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "./cold-resurrect.ts";
|
|
@@ -36,44 +39,72 @@ import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
|
|
|
36
39
|
import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
|
|
37
40
|
import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
|
|
38
41
|
import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
+
import type { AgentCallOpts } from "../orchestration/models/types.ts";
|
|
43
|
+
import { setHostUiRequestEndpoint } from "./engine/host/host-ui-endpoint.ts";
|
|
44
|
+
// [W3 chat 域收口] chat 轮次与 run 域同路:经 pi-host-binding 解析 registry 'pi' 的
|
|
45
|
+
// cli 形态 port(RemoteEngine),协议 run(interact) 发往 pi-subagent-cli 引擎进程。
|
|
46
|
+
// inproc PiEngine(inproc pi 引擎目录)已删——本文件不再持有任何本地引擎实例。
|
|
47
|
+
import { PI_POOL_KEY, resolveHostPiEnginePort } from "./engine/host/pi-host-binding.ts";
|
|
48
|
+
import type { HostBridgeServiceFace } from "./engine/host/host-bridge.ts";
|
|
42
49
|
import type { EnginePort, RunContext } from "./engine/port.ts";
|
|
43
50
|
import { DEFAULT_ENGINE_ID, getEngine, listEngines } from "./engine/registry.ts";
|
|
44
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
joinEngineModelRef,
|
|
53
|
+
splitEngineModelRef,
|
|
54
|
+
validateModelForEngine,
|
|
55
|
+
withCrossEngineHint,
|
|
56
|
+
} from "./engine/model-validation.ts";
|
|
45
57
|
import { type EngineRouteResult, routeEngineForHost } from "./engine/routing.ts";
|
|
46
|
-
import type { AgentOutcome } from "./engine/types.ts";
|
|
58
|
+
import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
|
|
47
59
|
import { ManifestStore } from "./manifest-store.ts";
|
|
48
60
|
import type { ModelConfigService } from "./model-config-service.ts";
|
|
49
61
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
50
62
|
import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts";
|
|
51
63
|
// [T4④ / PS-5] flush 被门拦时的未投递 pending 落盘账本(persistUndeliveredNotificationsForReplay 消费)
|
|
64
|
+
import type { BatchBudgetParams, BgNotifyRecord } from "./notifier.ts";
|
|
52
65
|
import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
|
|
53
66
|
import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
|
|
54
|
-
import { createRoundSettler } from "./round-settlement.ts";
|
|
55
67
|
import type { StatusFilter } from "./record-store.ts";
|
|
56
68
|
import { RecordStore } from "./record-store.ts";
|
|
69
|
+
// [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
|
|
70
|
+
//(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
|
|
71
|
+
import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
|
|
57
72
|
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
73
|
+
// [W6 宿主面下沉 → W3 纯镜像] killAll / killRecord / register 三函数经
|
|
74
|
+
// spawnedChildren 状态镜像公共面(engine/host/spawned-children.ts)——子进程活在
|
|
75
|
+
// 引擎进程内,本模块只做镜像记账(终止意图位),实际终止经协议 interact cancel/close。
|
|
58
76
|
import {
|
|
59
77
|
killAllSpawnedChildren,
|
|
60
78
|
killRecordChildWithEscalation,
|
|
61
79
|
registerSpawnedChildForRecord,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// [u-t2a T2②/T2③] settled watchdog:chatMode 轮 settled 等待固定硬上限(10min),
|
|
67
|
-
// 双挂载点之一在编排层热路径(deliverChatMessage,prompt 发出后),disarm 站点散布
|
|
68
|
-
// cancel/close/终态化路径(原语幂等,见 settled-watchdog.ts 头注释)。
|
|
80
|
+
} from "./engine/host/spawned-children.ts";
|
|
81
|
+
// [u-t2a T2②/T2③ + W4 协议事件面] settled watchdog:chatMode 轮 settled 等待两段守护。
|
|
82
|
+
// W3 起事件接线 = 协议事件流(arm 点 = 轮开始;refresh 源 = streamDelta/轮次生命周期
|
|
83
|
+
// 帧;W4 三入口与旧原语逐一同构,见 settled-watchdog.ts 尾段)。
|
|
69
84
|
import {
|
|
70
85
|
armMidRoundNoProgress,
|
|
86
|
+
disarmRoundFromProtocol,
|
|
71
87
|
disarmSettledWatchdog,
|
|
88
|
+
noteRoundSettledFromProtocol,
|
|
89
|
+
refreshFromProtocolEvent,
|
|
72
90
|
type SettledWatchdogFireInfo,
|
|
73
91
|
} from "./settled-watchdog.ts";
|
|
74
92
|
import { isIdle, isResumable } from "./lifecycle-predicates.ts";
|
|
75
93
|
import { startIdleGc } from "./idle-gc.ts";
|
|
76
|
-
|
|
94
|
+
// [W4] 轮次活性监督器(D2「等待有主」权威层;机制与注释见 round-supervisor/,
|
|
95
|
+
// 装配绑定面在 service-binding.ts——变化轴独立)
|
|
96
|
+
import type { RoundSupervisor } from "./round-supervisor/index.ts";
|
|
97
|
+
import {
|
|
98
|
+
createRoundSupervisorForService,
|
|
99
|
+
runPendingReconcileSweepForService,
|
|
100
|
+
} from "./round-supervisor/service-binding.ts";
|
|
101
|
+
// [W4] WorkflowRun store 纳入 idle-gc(startedAt 锚归档,宿主无关实现,见 file-run-store.ts)
|
|
102
|
+
import { FileRunStore } from "../orchestration/file-run-store.ts";
|
|
103
|
+
// [F-1 修复] idle-gc 的 WorkflowRun store 读根 = pi 壳 JsonlRunStore 同源布局
|
|
104
|
+
import { resolvePiWorkflowStateDir } from "./workflow-state-root.ts";
|
|
105
|
+
// [W4] 引擎进程死亡分诊(表 3 行 1 判据:EngineSdkError engine_crashed)
|
|
106
|
+
import { EngineSdkError } from "@zhushanwen/subagent-engine-sdk";
|
|
107
|
+
import type { HostRoundLifecycleParams, ProtocolError, ResumeAnchor } from "@zhushanwen/subagent-engine-sdk";
|
|
77
108
|
import type { StreamSink, SubagentStream } from "./stream-sink.ts";
|
|
78
109
|
import { createBackgroundStream } from "./stream-sink.ts";
|
|
79
110
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
@@ -136,13 +167,13 @@ export interface SubagentChatActions {
|
|
|
136
167
|
* 子进程彻底 close 前(pi 子进程 trap SIGTERM 做 graceful shutdown,窗口几十~几百 ms),
|
|
137
168
|
* 子进程的 trailing extension_ui_request 仍可能被父进程 pump 解析,调到 A 的 handler 闭包。
|
|
138
169
|
* 若 dispose 不清 uiRequestHandler,旧 handler 闭包仍持有 A 的 ctx,触发
|
|
139
|
-
*
|
|
170
|
+
* inproc UI 请求队列(已删) 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
|
|
140
171
|
* logger.error(看起来像 bug,实际是预期竞态;三层兜底已确保功能正确)。
|
|
141
172
|
*
|
|
142
173
|
* stub 始终返回 {cancelled:true},不调 ctx.ui、不捕获任何 ctx,让 trailing ui_request
|
|
143
174
|
* 干净降级为 cancelled(等价于子进程主动取消)。
|
|
144
175
|
*
|
|
145
|
-
* 不置 undefined —— 那会让 trailing ui_request 走
|
|
176
|
+
* 不置 undefined —— 那会让 trailing ui_request 走 inproc UI 请求队列(已删) 的 handler-missing
|
|
146
177
|
* 分支触发 notifyMissingHandlerGlobal warn,噪声性质从 threw-error 变 missing-handler,
|
|
147
178
|
* 没真正解决。 */
|
|
148
179
|
const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
|
|
@@ -242,6 +273,11 @@ const ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
|
|
|
242
273
|
const ENV_DEPTH = "PI_SUBAGENT_DEPTH";
|
|
243
274
|
const ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
|
|
244
275
|
|
|
276
|
+
/** [v2 D4] E1 等待分支 settled 有界重扫上限。无上限重扫 = 泄漏(设计 §3.3 D4 被否
|
|
277
|
+
* 谱系);8 次覆盖重启后主 agent 对 resumable 成员的典型续跑轮次,达限仍有
|
|
278
|
+
* running → disposed,交下次 session_start 收敛。 */
|
|
279
|
+
const SETTLED_RESCAN_LIMIT = 8;
|
|
280
|
+
|
|
245
281
|
/** resolveIdentity 的产物——一次确定、写入 record 后不再变。 */
|
|
246
282
|
interface ResolvedIdentity {
|
|
247
283
|
agent: string;
|
|
@@ -314,12 +350,30 @@ export class SubagentService {
|
|
|
314
350
|
/** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
|
|
315
351
|
* 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
|
|
316
352
|
* deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
|
|
317
|
-
* createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
|
|
353
|
+
* createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
|
|
354
|
+
* [sync-collect 合并] toNotifyRecord/notify/notifyBatch 随批路由需要由 host 导出
|
|
355
|
+
* (collectCoordinator 闭包与 E9/E1 直发路径消费,见 notify-host.ts 接口注释)。 */
|
|
318
356
|
private readonly notifyHost: NotifyHost = createNotifyHost({
|
|
319
357
|
getPi: () => this.pi,
|
|
320
358
|
listRunning: () => this.store.listRunning(),
|
|
321
359
|
getIsIdle: () => this.isIdleFn,
|
|
322
360
|
});
|
|
361
|
+
|
|
362
|
+
/** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
|
|
363
|
+
private readonly collectCoordinator: CollectCoordinator;
|
|
364
|
+
/** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
|
|
365
|
+
* 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
|
|
366
|
+
* 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
|
|
367
|
+
* sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
|
|
368
|
+
* 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
|
|
369
|
+
private readonly e9ConvertedIds = new Set<string>();
|
|
370
|
+
/** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
|
|
371
|
+
* 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
|
|
372
|
+
* 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
|
|
373
|
+
* pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
|
|
374
|
+
* 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
|
|
375
|
+
* 的文件域);dispose 的惰化处置见 dispose()。 */
|
|
376
|
+
private settledRescanState: { disposed: boolean; scans: number } | null = null;
|
|
323
377
|
/** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
|
|
324
378
|
* 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
|
|
325
379
|
* parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
|
|
@@ -341,22 +395,15 @@ export class SubagentService {
|
|
|
341
395
|
* 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
|
|
342
396
|
private readonly resumesInFlight = new Set<string>();
|
|
343
397
|
|
|
344
|
-
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
/** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
|
|
354
|
-
* 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
|
|
355
|
-
private readonly settleRound = createRoundSettler({
|
|
356
|
-
notifyComplete: (record) => this.notifyHost.notifyComplete(record),
|
|
357
|
-
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
358
|
-
closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record),
|
|
359
|
-
});
|
|
398
|
+
/**
|
|
399
|
+
* [W3 chat 域收口] chat 轮次反向通道路由表(recordId 键):首轮 run chat 注册、
|
|
400
|
+
* interact 续聊轮复用——host/streamDelta(recordId 分支)与 host/roundLifecycle
|
|
401
|
+
* (recordId 键)的分发目标(EngineClient.recordRoutes 经
|
|
402
|
+
* EnginePort.registerChatRoundRoute 注册)。record 终态化路径注销([F-5 修复]
|
|
403
|
+
* 汇聚点 = doFinalizeRecord 的 onFinalized 钩子 + cancelBackground /
|
|
404
|
+
* disposeAllRecords 两处直连终态化路径显式注销)。
|
|
405
|
+
*/
|
|
406
|
+
private readonly chatRoundRoutes = new Map<string, () => void>();
|
|
360
407
|
|
|
361
408
|
/** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
|
|
362
409
|
* deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
|
|
@@ -370,6 +417,21 @@ export class SubagentService {
|
|
|
370
417
|
getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
|
|
371
418
|
};
|
|
372
419
|
|
|
420
|
+
/**
|
|
421
|
+
* [W4] 轮次活性监督器(D2「等待有主」权威层):死亡事件纳管(run failed / 引擎
|
|
422
|
+
* exited → adoptOnProcessDeath)+ boot 分区重认领(initSession)+ 三态判定
|
|
423
|
+
* (record 级视图)+ 该放弃(终态化 failed + 注销 + 终止通知)。装配绑定面
|
|
424
|
+
* (deps/giveUp 编排/sweep 挂点)在 round-supervisor/service-binding.ts——变化轴
|
|
425
|
+
* 独立(通知文案 / 终态化编排 / store 读侧判据只动该文件),本字段只持实例。
|
|
426
|
+
*/
|
|
427
|
+
private readonly roundSupervisor: RoundSupervisor = createRoundSupervisorForService({
|
|
428
|
+
getStore: () => this.store,
|
|
429
|
+
getPi: () => this.pi,
|
|
430
|
+
getSessionRootId: () => this.sessionRootId,
|
|
431
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
432
|
+
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
433
|
+
});
|
|
434
|
+
|
|
373
435
|
/** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
|
|
374
436
|
* 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
|
|
375
437
|
readonly queries: SubagentQueries = {
|
|
@@ -390,6 +452,12 @@ export class SubagentService {
|
|
|
390
452
|
|
|
391
453
|
private readonly manifestStore: ManifestStore;
|
|
392
454
|
|
|
455
|
+
/**
|
|
456
|
+
* [D6 #7a] records 目录(与 manifestStore 同源同一推导)——屏障失败 warn 带 manifest
|
|
457
|
+
* 文件路径用(ManifestStore.dir 私有,此处不破封装另存同源值;漂移由构造点同语句保证不发生)。
|
|
458
|
+
*/
|
|
459
|
+
private readonly recordsDir: string;
|
|
460
|
+
|
|
393
461
|
/**
|
|
394
462
|
* [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
|
|
395
463
|
* 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
|
|
@@ -402,6 +470,9 @@ export class SubagentService {
|
|
|
402
470
|
this.modelService = init.modelService;
|
|
403
471
|
this.getMainSessionFile = init.getMainSessionFile;
|
|
404
472
|
this.uiRequestHandler = init.uiRequestHandler;
|
|
473
|
+
// [W6 R3 MF-A] 壳侧应答端登记:cli 形态引擎经 host/askUser 反向请求消费(discovery
|
|
474
|
+
// portFactory 构造 EngineClient 时读取该登记)。
|
|
475
|
+
setHostUiRequestEndpoint(init.uiRequestHandler);
|
|
405
476
|
this.pool = new DefaultConcurrencyPool(this.modelService.getGlobalConfig().maxConcurrent);
|
|
406
477
|
this.worktreeManager = new WorktreeManager(this.modelService.getAgentDir());
|
|
407
478
|
// [MF-3] worktree 隔离下全树落盘目录统一到 ROOT cwd:子进程(spawn cwd = worktree checkout 路径)
|
|
@@ -414,9 +485,84 @@ export class SubagentService {
|
|
|
414
485
|
const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
415
486
|
const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
416
487
|
this.sessionsDir = sessionsDir;
|
|
488
|
+
this.recordsDir = recordsDir;
|
|
417
489
|
this.manifestStore = new ManifestStore(recordsDir);
|
|
418
490
|
this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? undefined);
|
|
419
|
-
//
|
|
491
|
+
// collectCoordinator(subagent-sync-collect U2):notifyComplete 唯一路由入口——
|
|
492
|
+
// async 直通(字节不变)/ sync 批缓冲 + 闭合检测。[U3 接线点] 已接线:flush =
|
|
493
|
+
// manifest 屏障(await 全部落盘)→ notifier.notifyBatch 单条批投递(幂等键
|
|
494
|
+
// sync-batch:<hash> + 闭合触发排程合批 flush:同宏任务去抖窗口收纳背靠背
|
|
495
|
+
// route——U8 拆批盲窗修复 → ledger 写账 → attemptDeliver 边沿投递)+
|
|
496
|
+
// batchFinalized 落标(出口①,见下方闭包注释)。屏障先于写账 = 「通知可达 ⇒
|
|
497
|
+
// 索引就位」的构造性保证(时序竞态修复,见 flushBatch 闭包注释)。
|
|
498
|
+
// [U8] 排程与 E9/E1 交互:dispose 经 convertPendingSyncBufferToAsync 先取消挂起
|
|
499
|
+
// 排程(取消而非同步 flush——双通道并发写账防护,见该函数注释);E1 只在
|
|
500
|
+
// session_start 编排处运行(此前 dispose 已取消排程,补发直走 notifyBatch 不经
|
|
501
|
+
// 协调器),异常时序相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
|
|
502
|
+
// [D4-① 合并] notifier 实例封装在 notifyHost 内(createNotifyHost 装配),本类
|
|
503
|
+
// 经 this.notifyHost.notify / notifyBatch / toNotifyRecord 出口消费。
|
|
504
|
+
this.collectCoordinator = new CollectCoordinator({
|
|
505
|
+
notifyAsync: (record) => {
|
|
506
|
+
const notify = this.notifyHost.toNotifyRecord(record);
|
|
507
|
+
if (notify) this.notifyHost.notify(notify);
|
|
508
|
+
},
|
|
509
|
+
toNotifyRecord: (record) => this.notifyHost.toNotifyRecord(record),
|
|
510
|
+
// 闭合判定数据源:listAllActive(原始 ExecutionRecord 内存态,携带 collectMode/
|
|
511
|
+
// batchFinalized 原始值)。[U3 修正] 原接 collectRecords——其经 recordToSubagent
|
|
512
|
+
// 投影丢 collectMode(U2 披露的投影缺口)→ 真链上闭合判定恒立即闭合、跨轮续累
|
|
513
|
+
// 失效(真链 trace 实证)。非终态 sync 成员必在内存(archive 只删终态;磁盘重建
|
|
514
|
+
// 残留属孤儿恢复域),内存视图语义完整。
|
|
515
|
+
listRecords: (limit) => this.store.listAllActive().slice(0, limit),
|
|
516
|
+
flushBatch: async (members) => {
|
|
517
|
+
// [E9 防御过滤] 排除 dispose 时已转 async 写账的成员(同进程 revive 后协调器
|
|
518
|
+
// 缓冲残留的陈旧快照,见 e9ConvertedIds 字段注释);全被排除 → 空批零副作用
|
|
519
|
+
// 返回(成员已单独写账+落标,无需再动)。
|
|
520
|
+
const live = members.filter((m) => !this.e9ConvertedIds.has(m.id));
|
|
521
|
+
if (live.length === 0) return;
|
|
522
|
+
// 成员全量快照:getFullRecord 冷路径重建(成员在 notifyComplete 前已 archive,
|
|
523
|
+
// 内存无;闭合判定 hasRunningSync 的 listRecords 扫描已建 idToFile 索引,此处
|
|
524
|
+
// 命中)。快照在屏障前一次取定,屏障写与落标共用同一份(manifest 与落标 entry
|
|
525
|
+
// 字段同源)。
|
|
526
|
+
// [S11] getFullRecord 不可达(子 session 文件缺失/已 GC 的窗口)的成员改用
|
|
527
|
+
// 缓冲快照兜底落标,不再跳过——该成员仍随 live 进批写账(成员集 hash 含它),
|
|
528
|
+
// 跳过落标的旧行为会在重启后让 E1 重新收集它(无标记候选),以异成员集 hash
|
|
529
|
+
// 重新补发 → 同成员双投递(异 hash 不触发账本 sync-batch 幂等;旧注释「该
|
|
530
|
+
// 窗口下 E1 本也收不到该成员」不成立:E1 走主 session 文件末条 entry 扫描,
|
|
531
|
+
// 与 getFullRecord 的 manifest/子文件通路相互独立,manifest 屏障 best-effort
|
|
532
|
+
// 写失败同样制造此窗口)。
|
|
533
|
+
const fulls: SubagentRecord[] = [];
|
|
534
|
+
const fullMissed: BgNotifyRecord[] = [];
|
|
535
|
+
for (const m of live) {
|
|
536
|
+
const full = this.store.getFullRecord(m.id);
|
|
537
|
+
if (full) fulls.push(full);
|
|
538
|
+
else fullMissed.push(m);
|
|
539
|
+
}
|
|
540
|
+
// [时序屏障] 成员 manifest 写先于写账并 await 全部落盘——「通知可达 ⇒ 索引
|
|
541
|
+
// 就位」的构造性保证(by construction):批通知的指针行消费依赖
|
|
542
|
+
// records/<sa-id>.json 反查索引,fire-and-forget 下「通知送达时已落盘」只是
|
|
543
|
+
// 大概率成立(探针实测 mtime 相对 notify entry ±2/3ms 方向不定)。写失败仍
|
|
544
|
+
// best-effort(debug 不阻断写账投递,语义与 doFinalizeRecord Step 4 一致)。
|
|
545
|
+
await this.writeSyncBatchManifestBarrier(fulls);
|
|
546
|
+
// 单条批投递:accepted=false(同成员集批已在账——E1 重建重发/重复 flush)或
|
|
547
|
+
// 空批/dispose → 零副作用返回,不落标(设计 §3.1.3 出口①绑「写账成功」)。
|
|
548
|
+
const accepted = this.notifyHost.notifyBatch(live, this.getCollectSyncBudget());
|
|
549
|
+
if (!accepted) return;
|
|
550
|
+
// batchFinalized 纯落标(设计 §3.1.3 两出口之一:批闭合 flush 写账成功后)。
|
|
551
|
+
// collectMode/batchFinalized 显式覆写在 appendBatchFinalizedEntry 内(防非
|
|
552
|
+
// entry 源重建丢标记);末条 entry 带标记 → E1 重建扫描(collectMode=sync 且
|
|
553
|
+
// 无标记才收)据此排除,防双重通知。源序「写账先于落标」不变(v1 幂等窗口
|
|
554
|
+
// 语义);manifest 已在屏障提前写,幂等窗口内崩溃时索引更早已就位。
|
|
555
|
+
for (const full of fulls) {
|
|
556
|
+
this.appendBatchFinalizedEntry(full);
|
|
557
|
+
}
|
|
558
|
+
// [S11] miss 成员兜底落标(缓冲快照 → 最小标记 entry,映射见 sync-rebuild.ts),
|
|
559
|
+
// 与 fulls 同出口①语义(写账成功后统一补标)。
|
|
560
|
+
for (const m of fullMissed) {
|
|
561
|
+
this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? undefined));
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
// #11:注册进程级 observability 单例——inproc UI 请求队列 handleUiRequest(已删) 经
|
|
420
566
|
// globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
|
|
421
567
|
// warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
|
|
422
568
|
registerGlobalObservability(this.uiObservability);
|
|
@@ -443,6 +589,8 @@ export class SubagentService {
|
|
|
443
589
|
if (init.uiRequestHandler !== undefined) {
|
|
444
590
|
this.uiRequestHandler = init.uiRequestHandler ?? undefined;
|
|
445
591
|
this.uiObservability.resetMissingHandlerWarnings();
|
|
592
|
+
// [W6 R3 MF-A] session 级覆盖同步进壳侧应答端登记(三态:null = 显式清空)。
|
|
593
|
+
setHostUiRequestEndpoint(this.uiRequestHandler);
|
|
446
594
|
}
|
|
447
595
|
// SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
|
|
448
596
|
// 透传 undefined,session-runner onClose 跳过 L2 清理(仅清 L1,保留旧行为)。
|
|
@@ -459,11 +607,33 @@ export class SubagentService {
|
|
|
459
607
|
this.initExecContextBaseline(envRoot, init.sessionId);
|
|
460
608
|
// revive(dispose 的逆操作:/resume /fork /new 后复活)
|
|
461
609
|
this._disposed = false;
|
|
610
|
+
// [v2 D4] settled 重扫状态随 revive 重置:新 session 的 E1 若再判「仍有 running」
|
|
611
|
+
// 可重新注册。旧 handler 闭包捕获旧 state:正常时序(session_shutdown →
|
|
612
|
+
// session_start)下已随 dispose() 惰化;未经 dispose 的时序残留仍会在 settled
|
|
613
|
+
// 边沿执行——其扫描 this.mainSessionFile 当前值(非注册时的旧文件),行为等价于
|
|
614
|
+
// 新 session 多注册一次扫描,由账本 sync-batch:<hash> 幂等 + batchFinalized 候选
|
|
615
|
+
// 过滤收敛,无跨 session 污染面。
|
|
616
|
+
this.settledRescanState = null;
|
|
462
617
|
this.store.revive();
|
|
463
618
|
this.notifyHost.revive();
|
|
464
619
|
// 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
|
|
465
620
|
// sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
|
|
466
621
|
this.recoverOrphansIfRootProcess();
|
|
622
|
+
// [W4] boot 分区 + 注册对账 sweep(须在孤儿恢复之后——依赖关系见两方法注释:
|
|
623
|
+
// 孤儿恢复把「重启前在途」record 直断 closed、把 resumable 形态保留 running 落
|
|
624
|
+
// entry,监督器重认领消费后者;sweep 再对终态 record 补发注销落盘——表 3 行 2
|
|
625
|
+
// 「注销经对账 sweep 保证落盘」的编排点)。
|
|
626
|
+
this.roundSupervisor.bootPartition();
|
|
627
|
+
runPendingReconcileSweepForService(
|
|
628
|
+
{
|
|
629
|
+
getStore: () => this.store,
|
|
630
|
+
getPi: () => this.pi,
|
|
631
|
+
getSessionRootId: () => this.sessionRootId,
|
|
632
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
633
|
+
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
634
|
+
},
|
|
635
|
+
(process.env[ENV_SELF_RECORD_ID] ?? "") !== "",
|
|
636
|
+
);
|
|
467
637
|
}
|
|
468
638
|
|
|
469
639
|
/**
|
|
@@ -531,11 +701,13 @@ export class SubagentService {
|
|
|
531
701
|
/** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
|
|
532
702
|
* private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
|
|
533
703
|
* 是 initSession,转 private)。判定语义见 store 侧注释。
|
|
704
|
+
* mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
|
|
705
|
+
* 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
|
|
534
706
|
* 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
|
|
535
707
|
* E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。 */
|
|
536
708
|
private recoverOrphanRecords(): void {
|
|
537
709
|
try {
|
|
538
|
-
this.store.recoverOrphanRecords(this.sessionRootId ?? undefined);
|
|
710
|
+
this.store.recoverOrphanRecords(this.sessionRootId ?? undefined, this.mainSessionFile);
|
|
539
711
|
} catch (err) {
|
|
540
712
|
logger.warn("[subagents] orphan recovery failed", {
|
|
541
713
|
reason: toErrorMessage(err),
|
|
@@ -611,6 +783,9 @@ export class SubagentService {
|
|
|
611
783
|
};
|
|
612
784
|
completeRecord(record, result, "closed", reason);
|
|
613
785
|
this.store.archive(record);
|
|
786
|
+
// [F-5 修复] 本路径不经 doFinalizeRecord(编排性关闭直连 completeRecord+archive),
|
|
787
|
+
// chat 轮路由注销在此补齐(幂等;闭包持 record/stream 引用,防泄漏)。
|
|
788
|
+
this.unregisterChatRoundRoute(record.id);
|
|
614
789
|
// worktree 绑定清理(T3)。cleanup 已 async 化——同步签名(返回计数)不变,
|
|
615
790
|
// 清理 fire-and-forget:失败经 bestEffort 留痕,不阻塞/不影响计数返回。
|
|
616
791
|
if (record.worktreeHandle) {
|
|
@@ -641,10 +816,16 @@ export class SubagentService {
|
|
|
641
816
|
/** SP-4: idle record GC(30 天 TTL,实现抽至 idle-gc.ts)。stop 函数(dispose 调)。 */
|
|
642
817
|
private stopIdleGc: (() => void) | undefined;
|
|
643
818
|
|
|
644
|
-
/** 启动 idle record GC 定时器(session_start 调用,幂等)。
|
|
819
|
+
/** 启动 idle record GC 定时器(session_start 调用,幂等)。
|
|
820
|
+
* [W4] WorkflowRun store(FileRunStore)同批纳入:running 且 startedAt 超 30 天
|
|
821
|
+
* 锚窗的 run 终态化归档(只终态化不补注销,见 idle-gc.ts 头注)。宿主未
|
|
822
|
+
* configureCore 时 loadAll 抛错由 idle-gc 内部吞掉(单轮跳过)。
|
|
823
|
+
* [F-1 修复] stateDir 与 pi 壳 JsonlRunStore 落盘布局同源
|
|
824
|
+
* (resolvePiWorkflowStateDir → <sessionDir>/workflow-state/)——缺省 dataRoot 根
|
|
825
|
+
* 与 pi 生产落盘不相交,WorkflowRun GC 曾恒空转(W4 引入的装配错位)。 */
|
|
645
826
|
startGcTimer(): void {
|
|
646
827
|
if (this.stopIdleGc) return;
|
|
647
|
-
this.stopIdleGc = startIdleGc(this.store);
|
|
828
|
+
this.stopIdleGc = startIdleGc(this.store, new FileRunStore({ stateDir: resolvePiWorkflowStateDir() }));
|
|
648
829
|
}
|
|
649
830
|
|
|
650
831
|
/** 停止 idle record GC 定时器(dispose 调用)。 */
|
|
@@ -659,10 +840,271 @@ export class SubagentService {
|
|
|
659
840
|
* listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
|
|
660
841
|
* 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
|
|
661
842
|
* 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
|
|
843
|
+
/** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
|
|
844
|
+
* 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
|
|
845
|
+
* → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
|
|
846
|
+
* sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
|
|
847
|
+
private getCollectSyncBudget(): BatchBudgetParams | undefined {
|
|
848
|
+
const cs = this.collectSyncSection();
|
|
849
|
+
return cs !== undefined ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** [E9 专用] batchFinalized 落标 + manifest fire-and-forget 写(设计 §3.1.5 E9)。
|
|
853
|
+
* 批通知路径(flush/E1)已改走「manifest 屏障 → 写账 → 纯落标」序列(「通知可达
|
|
854
|
+
* ⇒ 索引就位」的构造性保证,见 flushBatch 闭包 / runSyncCollectRecoveryScan),
|
|
855
|
+
* 不再经本 helper;仅 E9 转换的成员保持原形态——其走 async 单条通知(全文注入、
|
|
856
|
+
* 无指针行消费),manifest 无时序要求,落标后 fire-and-forget 补写(list 后手动
|
|
857
|
+
* 反查的顺带索引)。
|
|
858
|
+
* 路径 = getFullRecord 冷路径重建 → appendBatchFinalizedEntry 纯落标 → fire
|
|
859
|
+
* manifest。getFullRecord 不可达(子 session 文件缺失/已 GC)→ 跳过该成员
|
|
860
|
+
* (详见 flushBatch 闭包注释)。 */
|
|
861
|
+
private markMembersBatchFinalized(memberIds: readonly string[]): void {
|
|
862
|
+
for (const id of memberIds) {
|
|
863
|
+
const full = this.store.getFullRecord(id);
|
|
864
|
+
if (!full) continue;
|
|
865
|
+
this.appendBatchFinalizedEntry(full);
|
|
866
|
+
// 反查索引缺失只影响指针行反查(session-reader 错误文案已指引绝对路径兜底),
|
|
867
|
+
// 不构成落标失败(与屏障路径的 best-effort 语义同源,仅无时序保证)。
|
|
868
|
+
void this.writeBatchMemberManifest(full).catch((err: unknown) => {
|
|
869
|
+
logger.debug(
|
|
870
|
+
`[subagents] batch-finalized manifest write failed (record=${full.id})`,
|
|
871
|
+
{ reason: err instanceof Error ? err.message : String(err) },
|
|
872
|
+
);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** batchFinalized 落标唯一出口(appendEntry 公共末步,纯落标):显式覆写
|
|
878
|
+
* collectMode/batchFinalized → reportSubagentRecord。覆写动机:recordToSubagent
|
|
879
|
+
* 投影已含两字段(U5 修复),但 getFullRecord 冷路径含 sidecar/manifest 重建分支
|
|
880
|
+
* (非 entry 源),显式赋值防非 entry 源重建时丢标记。
|
|
881
|
+
*
|
|
882
|
+
* [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
|
|
883
|
+
* 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
|
|
884
|
+
* records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
|
|
885
|
+
* 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
|
|
886
|
+
* await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
|
|
887
|
+
* fire-and-forget(async 单条通知无指针行消费,无时序要求)。
|
|
888
|
+
* rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
|
|
889
|
+
* 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
|
|
890
|
+
* task/slug/parentRecordId 等可选 undefined 自然缺省。 */
|
|
891
|
+
private appendBatchFinalizedEntry(rec: SubagentRecord): void {
|
|
892
|
+
this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** 批成员 manifest(sa- id → sessionFile 反查索引)写的唯一投影点(D2 字段投影 +
|
|
896
|
+
* status 如实投影:成功成员此刻 record 实态 running+resumable → "running",后续
|
|
897
|
+
* message upgrade 走完整 finalize 时 Step 4 原子覆盖为 "closed")。
|
|
898
|
+
* 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
|
|
899
|
+
* writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
|
|
900
|
+
* fire-and-forget catch(debug 不阻断落标)。 */
|
|
901
|
+
private writeBatchMemberManifest(rec: SubagentRecord): Promise<void> {
|
|
902
|
+
return this.manifestStore.writeManifest({
|
|
903
|
+
id: rec.id,
|
|
904
|
+
rootSessionId: rec.rootSessionId ?? "",
|
|
905
|
+
parentRecordId: rec.parentRecordId,
|
|
906
|
+
agentName: rec.agent,
|
|
907
|
+
status: rec.status,
|
|
908
|
+
createdAt: rec.startedAt,
|
|
909
|
+
completedAt: rec.endedAt,
|
|
910
|
+
sessionFile: rec.sessionFile,
|
|
911
|
+
task: rec.task,
|
|
912
|
+
slug: rec.slug,
|
|
913
|
+
model: rec.model,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** [时序屏障] 批通知路径(flush/E1)专用:成员 manifest 并行写 + await 全部完成
|
|
918
|
+
* (allSettled)后才允许写账投递——「通知可达 ⇒ 索引就位」的构造性保证。写失败
|
|
919
|
+
* 不阻断(best-effort 语义与 doFinalizeRecord Step 4 一致:反查索引缺失只影响指针行
|
|
920
|
+
* 反查,session-reader 错误文案已指引绝对路径兜底,不构成写账失败);warn 留痕
|
|
921
|
+
* (D6 #7a / SC-1:屏障失败意味着该成员指针行反查索引缺失,debug 级在排障时不可见)。 */
|
|
922
|
+
private async writeSyncBatchManifestBarrier(recs: readonly SubagentRecord[]): Promise<void> {
|
|
923
|
+
const results = await Promise.allSettled(recs.map((rec) => this.writeBatchMemberManifest(rec)));
|
|
924
|
+
for (let i = 0; i < results.length; i++) {
|
|
925
|
+
const result = results[i]!;
|
|
926
|
+
if (result.status === "rejected") {
|
|
927
|
+
logger.warn(
|
|
928
|
+
`[subagents] batch-finalized manifest write failed (record=${recs[i]!.id}, manifest=${this.recordsDir}/${recs[i]!.id}.json)`,
|
|
929
|
+
{ reason: result.reason instanceof Error ? result.reason.message : String(result.reason) },
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** [E9] dispose 时批未闭合:缓冲中已终态未通知成员逐条转 async 语义写账(放弃攒批)
|
|
936
|
+
* + 落 batchFinalized 标记(E1 重建扫描据此排除,防双重通知),交由既有 shutdown
|
|
937
|
+
* flush / resume 重放兑底;仍在跑的成员走现有退出路径(disposeAllRecords 关闭,
|
|
938
|
+
* 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
|
|
939
|
+
* 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
|
|
940
|
+
* 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
|
|
941
|
+
private convertPendingSyncBufferToAsync(): void {
|
|
942
|
+
// [U8 拆批修复·E9 交互] 挂起的合批排程先取消——dispose 已选「放弃攒批转 async」
|
|
943
|
+
// 语义(最简语义 = 取消而非同步 flush):放任排程触发会让 flushBatch(批 hash
|
|
944
|
+
// 通道)与下方逐条 notify(async id 通道)双通道并发写账 → 同成员双投递,且触发
|
|
945
|
+
// 点可能落在 notifier/store dispose 之后。取消后缓冲原样保留,本函数既有单通路
|
|
946
|
+
// 完整接管(协调器侧幂等:无排程时 no-op)。
|
|
947
|
+
this.collectCoordinator.cancelScheduledFlush();
|
|
948
|
+
const members = this.collectCoordinator.pendingMembers();
|
|
949
|
+
if (members.length === 0) return;
|
|
950
|
+
for (const member of members) {
|
|
951
|
+
this.e9ConvertedIds.add(member.id);
|
|
952
|
+
this.notifyHost.notify(member);
|
|
953
|
+
}
|
|
954
|
+
this.markMembersBatchFinalized(members.map((m) => m.id));
|
|
955
|
+
logger.warn(
|
|
956
|
+
`[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
|
|
957
|
+
{ ids: members.map((m) => m.id) },
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* [E1] sync 批崩溃恢复钩子(设计 §3.1.5 E1,index.ts session_start 恢复编排处调用,
|
|
963
|
+
* 须晚于 initSession——孤儿终态恢复先行收敛 running 成员,「全员终态」判定才可达):
|
|
964
|
+
*
|
|
965
|
+
* - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
|
|
966
|
+
* collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
|
|
967
|
+
* 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
|
|
968
|
+
* - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
|
|
969
|
+
* 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
|
|
970
|
+
* - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
|
|
971
|
+
* 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
|
|
972
|
+
* entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
|
|
973
|
+
* 「已处理」);
|
|
974
|
+
* - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
|
|
975
|
+
* 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
|
|
976
|
+
* 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
|
|
977
|
+
* running 口径与协调器同构
|
|
978
|
+
* (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
|
|
979
|
+
* - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
|
|
980
|
+
* 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
|
|
981
|
+
* 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
|
|
982
|
+
*
|
|
983
|
+
* async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
|
|
984
|
+
* session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
|
|
985
|
+
* 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
|
|
986
|
+
*
|
|
987
|
+
* [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
|
|
988
|
+
* dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
|
|
989
|
+
* 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
|
|
990
|
+
*/
|
|
991
|
+
async recoverSyncCollectBatch(): Promise<void> {
|
|
992
|
+
try {
|
|
993
|
+
const { outcome } = await this.runSyncCollectRecoveryScan();
|
|
994
|
+
// [v2 D4] 断链 4:等待分支不再死等——挂 settled 有界重扫(幂等单注册)。
|
|
995
|
+
if (outcome === "waiting") {
|
|
996
|
+
this.armSettledRescan();
|
|
997
|
+
}
|
|
998
|
+
} catch (err) {
|
|
999
|
+
// 与 index.ts 调用点原 try/catch 的 warn 容错同语义(该处 catch 对 async 化后
|
|
1000
|
+
// 的 promise 异常兑现不到,容错收敛到本方法内部)。
|
|
1001
|
+
logger.warn("[subagents] sync collect batch recovery failed", {
|
|
1002
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** [E1/D4] 单次「扫描→判定→可达则补发+落标」,E1 首扫与 settled 重扫共用同一实现
|
|
1008
|
+
* (防两处复制粘贴分岔)。三态返回:idle(无 sync 候选——已全部落标/E9 转换/异根,
|
|
1009
|
+
* 无事可等)/ waiting(仍有 running 成员,本次不动)/ dispatched(全员终态,已补发
|
|
1010
|
+
* +统一落标)。async:补发前有 manifest 屏障 await(见函数头 E1 注释)。 */
|
|
1011
|
+
/** 返回 outcome + waitingIds(达限 warn 需滞留成员 id,D6 #7b——仅 waiting 态非空)。 */
|
|
1012
|
+
private async runSyncCollectRecoveryScan(): Promise<{
|
|
1013
|
+
outcome: "idle" | "waiting" | "dispatched";
|
|
1014
|
+
waitingIds: string[];
|
|
1015
|
+
}> {
|
|
1016
|
+
const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
|
|
1017
|
+
if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
1018
|
+
const rootFilter = this.sessionRootId;
|
|
1019
|
+
const candidates = lastRecords.filter(
|
|
1020
|
+
(r) =>
|
|
1021
|
+
r.collectMode === "sync" &&
|
|
1022
|
+
r.batchFinalized !== true &&
|
|
1023
|
+
(rootFilter === undefined || r.rootSessionId === rootFilter),
|
|
1024
|
+
);
|
|
1025
|
+
if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
1026
|
+
// [v2 D3] 与协调器 hasRunningSync 同构口径(collect-coordinator.ts):running+
|
|
1027
|
+
// resumable 视为已完成、不阻止补发——成功成员崩溃时的末条 entry 恒为轮终
|
|
1028
|
+
// running+resumable(SP-5 有意语义),旧口径只看 status !== "closed" 会把主场景
|
|
1029
|
+
// (批内含成功成员)顶死在「等自然终态」永不补发(v2 §2.3 断链 3)。
|
|
1030
|
+
const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
|
|
1031
|
+
if (running.length > 0) {
|
|
1032
|
+
logger.debug(
|
|
1033
|
+
`[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
|
|
1034
|
+
{ ids: running.map((r) => r.id) },
|
|
1035
|
+
);
|
|
1036
|
+
return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
|
|
1037
|
+
}
|
|
1038
|
+
// [时序屏障] manifest 先于写账 await 全部落盘——E1 补发同样是批通知(指针行消费
|
|
1039
|
+
// 依赖反查索引),「通知可达 ⇒ 索引就位」的构造性保证与 flushBatch 同款。
|
|
1040
|
+
await this.writeSyncBatchManifestBarrier(candidates);
|
|
1041
|
+
// 全员终态:单条批补发(budget 热读与 flushBatch 同源);账本同 hash 幂等拒绝也算
|
|
1042
|
+
// 已投递(批已在账/已销账,重放由账本承接)——两种结局统一补标。
|
|
1043
|
+
const members = candidates.map((r) => syncRebuildToNotifyMember(r));
|
|
1044
|
+
const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
|
|
1045
|
+
for (const rec of candidates) {
|
|
1046
|
+
this.appendBatchFinalizedEntry(rec);
|
|
1047
|
+
}
|
|
1048
|
+
logger.warn(
|
|
1049
|
+
`[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
|
|
1050
|
+
{ ids: members.map((m) => m.id) },
|
|
1051
|
+
);
|
|
1052
|
+
return { outcome: "dispatched", waitingIds: [] };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** [v2 D4] 注册 agent_settled 有界重扫(幂等:settledRescanState 非 null 不叠加注册
|
|
1056
|
+
* ——E1 现仅 session_start 单调用点,守卫是防第二入口引入时的注册叠加断言面)。
|
|
1057
|
+
* 每次 settled 边沿重跑同一 E1 扫描(runSyncCollectRecoveryScan):dispatched
|
|
1058
|
+
* (补发+落标完成,scan 的 warn 已留痕)或 idle(候选已被其他通路落标)→ disposed;
|
|
1059
|
+
* 累计 SETTLED_RESCAN_LIMIT 次仍在等 → disposed + warn 留痕(D6 #7b / SC-2:达限
|
|
1060
|
+
* 放弃重扫意味着滞留成员的批通知要等下次 session_start 才收敛,含滞留 id 的 warn
|
|
1061
|
+
* 是唯一线索,debug 级排障不可见;后续事件零处理,下次 session_start 再收敛)。
|
|
1062
|
+
* pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
|
|
1063
|
+
* index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
|
|
1064
|
+
* pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
|
|
1065
|
+
* extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
|
|
1066
|
+
* extension 的全部 handler 逐一 await;故本注册与 ledger host 经
|
|
1067
|
+
* piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
|
|
1068
|
+
private armSettledRescan(): void {
|
|
1069
|
+
if (this.settledRescanState !== null) return;
|
|
1070
|
+
const state = { disposed: false, scans: 0 };
|
|
1071
|
+
this.settledRescanState = state;
|
|
1072
|
+
this.pi?.on?.("agent_settled", async () => {
|
|
1073
|
+
if (state.disposed) return;
|
|
1074
|
+
state.scans += 1;
|
|
1075
|
+
// await 完整补发序列(manifest 屏障 → 写账 → 落标):pi emit 对 handler 逐一
|
|
1076
|
+
// await(P-settled 定谳,0.84.4 dist runner.js),async 化不改变分发语义。
|
|
1077
|
+
const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
|
|
1078
|
+
if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
|
|
1079
|
+
state.disposed = true;
|
|
1080
|
+
if (outcome === "waiting") {
|
|
1081
|
+
// D6 #7b / SC-2:warn + 滞留成员 id(debug 级排障不可见——达限即批通知挂起至下次 session_start)
|
|
1082
|
+
logger.warn(
|
|
1083
|
+
`[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
|
|
1084
|
+
{ ids: waitingIds },
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// [E1 语义对齐 toNotifyRecord] 补发成员映射 syncRebuildToNotifyMember 拆至
|
|
1091
|
+
// sync-rebuild.ts(变化轴:恢复批通知语义):one-shot 成功成员末条恒
|
|
1092
|
+
// running+resumable(SP-5),直通 status 会让恢复批批头「0 finished」且丢
|
|
1093
|
+
// patchFile 的 git-apply 指针——对齐后补发记录为 closed + outcome 物化 +
|
|
1094
|
+
// patchFile 透传。调用点:runSyncCollectRecoveryScan。
|
|
1095
|
+
|
|
662
1096
|
dispose(): void {
|
|
663
1097
|
if (this._disposed) return;
|
|
664
1098
|
this._disposed = true;
|
|
665
1099
|
this.stopGcTimer();
|
|
1100
|
+
// [v2 D4] settled 重扫 handler 惰化:dispose 后 trailing settled 边沿若仍触发,
|
|
1101
|
+
// 旧 handler 不得再跑扫描——notifier 随后将 dispose,notifyBatch 短路返回 false
|
|
1102
|
+
// 且不写账,而 E1 dispatched 段不判 accepted 仍统一落标 → 批被标 batchFinalized
|
|
1103
|
+
// 而通知从未写账(永久丢失,不可逆)。惰化后通知由下次重启的 E1 首扫兑现
|
|
1104
|
+
// (成员无标记,候选可达)。与 initSession revive 重置不冲突:dispose 是终态置
|
|
1105
|
+
// disposed,revive 置 null 是新 session 的重新注册,旧 state 对象随旧 handler
|
|
1106
|
+
// 闭包保持 disposed 永久惰化。
|
|
1107
|
+
if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
|
|
666
1108
|
// [dispose stub] 第一时间换 stub,防 trailing ui_request 调到 stale handler 闭包
|
|
667
1109
|
// (仍持有 disposed session 的 ctx)产生误导性 console.error。stub 干净降级为 cancelled。
|
|
668
1110
|
// 必须在 emit/abort 之前——这些步骤可能同步触发 trailing pump。
|
|
@@ -670,20 +1112,31 @@ export class SubagentService {
|
|
|
670
1112
|
// 此处内联其方法体(赋值 + 缺失告警去重重置)。
|
|
671
1113
|
this.uiRequestHandler = disposedUiRequestStub;
|
|
672
1114
|
this.uiObservability.resetMissingHandlerWarnings();
|
|
1115
|
+
// [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
|
|
1116
|
+
// cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
|
|
1117
|
+
setHostUiRequestEndpoint(disposedUiRequestStub);
|
|
673
1118
|
// [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
|
|
674
1119
|
// abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
|
|
675
1120
|
this.store.abortRunningControllers();
|
|
676
1121
|
killAllSpawnedChildren();
|
|
1122
|
+
// [W3] chat 反向通道路由全量注销(inproc 轮次交接包清空的协议形态替位)。
|
|
1123
|
+
for (const recordId of [...this.chatRoundRoutes.keys()]) {
|
|
1124
|
+
this.unregisterChatRoundRoute(recordId);
|
|
1125
|
+
}
|
|
1126
|
+
// [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
|
|
1127
|
+
// 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
|
|
1128
|
+
// running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
|
|
1129
|
+
// 冷 idToFile(无目录扫描则 miss → 跳过落标 → E1 重建误收已转换成员);先转换取
|
|
1130
|
+
// 内存命中,与 flushBatch 出口①同款通路。同样在 flushPendingNotifications 之前
|
|
1131
|
+
//(转换条目加入本次 flush)与 notifier/store dispose 之前(写账与 appendEntry
|
|
1132
|
+
// 通道仍可用)。仍在跑成员不在此处理——后续 disposeAllRecords 按现有退出路径关闭。
|
|
1133
|
+
this.convertPendingSyncBufferToAsync();
|
|
677
1134
|
// SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
|
|
678
1135
|
// 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
|
|
679
1136
|
this.disposeAllRecords("parent-shutdown");
|
|
680
|
-
// [v4 A-1] EPIPE 连续失败计数器清零(计数器已迁移到 stdin-writer,防跨 session 泄漏)
|
|
681
|
-
resetAllEpipeFailures();
|
|
682
1137
|
// [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
|
|
683
1138
|
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
684
1139
|
this.resumesInFlight.clear();
|
|
685
|
-
// chat 轮次交接包清空(正常由 PiEngine.run 消费;此处兜底 kill 后仍挂着的条目)
|
|
686
|
-
this.chatRoundTickets.clear();
|
|
687
1140
|
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
688
1141
|
this.notifyHost.flushPendingNotifications();
|
|
689
1142
|
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
@@ -693,6 +1146,9 @@ export class SubagentService {
|
|
|
693
1146
|
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
694
1147
|
this.persistUndeliveredNotificationsForReplay();
|
|
695
1148
|
this.notifyHost.dispose();
|
|
1149
|
+
// [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
|
|
1150
|
+
// 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
|
|
1151
|
+
this.roundSupervisor.dispose();
|
|
696
1152
|
this.store.dispose();
|
|
697
1153
|
}
|
|
698
1154
|
|
|
@@ -791,7 +1247,9 @@ export class SubagentService {
|
|
|
791
1247
|
// 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
|
|
792
1248
|
// 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
|
|
793
1249
|
// 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
|
|
794
|
-
// record。pi 请求路径同步短路(routed 非 Promise
|
|
1250
|
+
// record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
|
|
1251
|
+
// 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
|
|
1252
|
+
// 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
|
|
795
1253
|
// [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
|
|
796
1254
|
// engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
|
|
797
1255
|
// 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
|
|
@@ -807,7 +1265,9 @@ export class SubagentService {
|
|
|
807
1265
|
taskModel: opts.model,
|
|
808
1266
|
strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
|
|
809
1267
|
probe: (engineId) => getEngine(engineId).probe(),
|
|
810
|
-
|
|
1268
|
+
// [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
|
|
1269
|
+
// 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
|
|
1270
|
+
piEngine: this.resolveChatEnginePort(),
|
|
811
1271
|
});
|
|
812
1272
|
const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
|
|
813
1273
|
return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
|
|
@@ -867,42 +1327,74 @@ export class SubagentService {
|
|
|
867
1327
|
// deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
|
|
868
1328
|
|
|
869
1329
|
/**
|
|
870
|
-
* [V2 决策 3] chatMode 统一投递入口(message action 的 Service
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
1330
|
+
* [V2 决策 3 → W3 协议形态] chatMode 统一投递入口(message action 的 Service 面)
|
|
1331
|
+
* ——经协议 interact(message action)发往 pi-subagent-cli 引擎进程。协议知识
|
|
1332
|
+
* (stdin prompt 命令 + streamingBehavior 映射 + EPIPE 兜底)在引擎进程内
|
|
1333
|
+
* (chat-session.deliverMessage);编排层按结构化结果分流:
|
|
874
1334
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* timeout kill/跨重启命中)。
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
* 经 interact 结构化结果回传后原样 throw(错误文本逐字节保持)。
|
|
1335
|
+
* 受理(引擎侧进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle。
|
|
1336
|
+
* 冷路径(engine_session_not_resumable = 进程死/无活进程):冷路径续轮
|
|
1337
|
+
* (resumeColdRound → run chat + resume 锚点接续,仅崩溃/timeout kill/跨重启命中)。
|
|
1338
|
+
* 其他拒绝(EPIPE 兜底耗尽 / engine_interact_failed):业务拒绝原样 throw
|
|
1339
|
+
* (文案自带行动语言——与 inproc 形态逐字节一致)。
|
|
881
1340
|
*
|
|
882
1341
|
* @param record 目标 record(chatMode,running 或 idle)
|
|
883
1342
|
* @param text 消息正文
|
|
884
|
-
* @param interrupt true=steer(抢占)/ false=followUp
|
|
1343
|
+
* @param interrupt true=steer(抢占)/ false=followUp(排队)
|
|
885
1344
|
*/
|
|
886
1345
|
private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
|
|
887
1346
|
this.assertReady();
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1347
|
+
// 终态守卫(MF-4):closed record 不可续聊(对齐原冷路径守卫前置——状态检查
|
|
1348
|
+
// 先于引擎交互,避免对已终态对话产生引擎侧副作用)。
|
|
1349
|
+
if (record.status !== "running") {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
`subagent ${record.id} is not ready for a new message (current state: ${record.status}). ` +
|
|
1352
|
+
`Recovery: use action:'list' to confirm state; wait for the current round to finish, or send the message again once it is idle.`,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
// 新 turn 开跑先 disarm idle timer(防 turn 期间误杀活进程——V2 决策 4,
|
|
1356
|
+
// 原 PiEngine.deliverPrompt 入口语义,编排侧承接)。
|
|
1357
|
+
disarmIdleTimer(record.id);
|
|
1358
|
+
const engine = this.resolveChatEnginePort();
|
|
1359
|
+
// recordId 键路由兜底注册(首轮已注册时幂等跳过——kickOffChatRound 注册面):
|
|
1360
|
+
// 续聊轮 delta/生命周期帧的分发目标。stream 缺省(首轮流已 dispose——widget 清除后
|
|
1361
|
+
// delta 为 no-op,与 inproc 形态一致),守护刷新与相位消费不受影响。
|
|
1362
|
+
if (!this.chatRoundRoutes.has(record.id)) {
|
|
1363
|
+
this.chatRoundRoutes.set(
|
|
1364
|
+
record.id,
|
|
1365
|
+
engine.registerChatRoundRoute?.(record.id, {
|
|
1366
|
+
onStreamDelta: (_delta) => refreshFromProtocolEvent(record.id),
|
|
1367
|
+
onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
|
|
1368
|
+
}) ?? (() => {}),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const result = await engine.interact(this.chatHandleFor(record), {
|
|
1372
|
+
kind: "message",
|
|
1373
|
+
payload: text,
|
|
1374
|
+
interrupt,
|
|
1375
|
+
});
|
|
894
1376
|
if (!result.ok) {
|
|
1377
|
+
// 冷路径分流:引擎无活进程 → 冷路径续轮(resume spawn 接续原 session——
|
|
1378
|
+
// W2 契约「引擎不自发 resume,宿主经 run chat+resume 接续」)。
|
|
1379
|
+
if (result.code === "engine_session_not_resumable") {
|
|
1380
|
+
this.resumeColdRound(record, text);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
895
1383
|
throw new Error(result.message);
|
|
896
1384
|
}
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
//
|
|
905
|
-
//
|
|
1385
|
+
// 受理成功:轮始执行态信号清除 + 迁移上报(原 deliverPrompt 热路径同步段语义——
|
|
1386
|
+
// 清上一轮 result(§5.4 isStreaming 公式要求 result undefined 才显示 streaming)
|
|
1387
|
+
// 与 resumable,appendEntry 让 runtime 派生缓存失效、GUI 从 waiting 切回 spinner)。
|
|
1388
|
+
record.status = "running";
|
|
1389
|
+
record.result = undefined;
|
|
1390
|
+
record.resumable = undefined;
|
|
1391
|
+
this.store.reportRecordTransition(record);
|
|
1392
|
+
// [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点;首轮/
|
|
1393
|
+
// 冷续轮调用点在 kickOffChatRound 的 run 派发前——[F-2 修复] 该点此前声称存在
|
|
1394
|
+
// 实则缺失,被删的 inproc stdout-pump 是首轮唯一中段守护)。prompt 发出后挂
|
|
1395
|
+
// **中段**无进展检测,刷新源 = 协议事件(recordId 键 streamDelta / roundLifecycle
|
|
1396
|
+
// 帧,见 chatRoundRoutes;首轮 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle);
|
|
1397
|
+
// settled 相位交棒(noteRoundSettledFromProtocol)、idle/close/cancel 终态化即清。
|
|
906
1398
|
armMidRoundNoProgress(record.id, {
|
|
907
1399
|
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
908
1400
|
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
@@ -997,24 +1489,28 @@ export class SubagentService {
|
|
|
997
1489
|
// W16 [D4]:冷路径续轮是类外状态写点(不走 register/archive),显式上报迁移。
|
|
998
1490
|
this.store.reportRecordTransition(record);
|
|
999
1491
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1492
|
+
// [W3 协议形态] 冷续锚点 = record identity(防多轮对话模型漂移,探针 P-10):
|
|
1493
|
+
// sessionFile 经 resume.sessionRef 携带(引擎 --session 续写原文件);model/
|
|
1494
|
+
// thinkingLevel 属 record 身份字段(锚点轮引擎侧覆盖解析)。原 SpawnResumeOpts
|
|
1495
|
+
// 形态随 inproc pi 引擎目录 删除,锚点载体 = 协议 ResumeAnchor(W1 SDK 契约)。
|
|
1496
|
+
const resume: ResumeAnchor = {
|
|
1497
|
+
sessionRef: {
|
|
1498
|
+
recordId: record.id,
|
|
1499
|
+
...(record.sessionFile !== undefined ? { sessionFile: record.sessionFile } : {}),
|
|
1500
|
+
},
|
|
1501
|
+
poolKey: PI_POOL_KEY,
|
|
1005
1502
|
};
|
|
1006
1503
|
|
|
1007
|
-
// 重建 resolved
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
const modelId = slashIdx >= 0 ? record.model.slice(slashIdx + 1) : record.model;
|
|
1504
|
+
// 重建 resolved:锚点轮的模型解析兜底(ctxModel 第三层)。从 record.model
|
|
1505
|
+
// (createRecordForMode 写入的 "provider/id" 或无斜杠 ref——契约变更④)拆分构造
|
|
1506
|
+
// 最小 ModelInfo;拆分与写入侧同源(splitEngineModelRef):无斜杠 ref 的 provider
|
|
1507
|
+
// 为空串(旧行为 "unknown" 会给续轮注入虚构 provider),整串进 name。
|
|
1508
|
+
const model = splitEngineModelRef(record.model);
|
|
1013
1509
|
const identity: ResolvedIdentity = {
|
|
1014
1510
|
agent: record.agent,
|
|
1015
1511
|
agentConfig: undefined,
|
|
1016
1512
|
resolved: {
|
|
1017
|
-
model: { id:
|
|
1513
|
+
model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
|
|
1018
1514
|
thinkingLevel: record.thinkingLevel,
|
|
1019
1515
|
},
|
|
1020
1516
|
};
|
|
@@ -1023,15 +1519,17 @@ export class SubagentService {
|
|
|
1023
1519
|
task: text,
|
|
1024
1520
|
slug: record.slug,
|
|
1025
1521
|
worktree: record.worktreeHandle,
|
|
1522
|
+
// 冷续轮同样是 chat 会话形态(run.params.chat 必传 + conversation gate 放行)。
|
|
1523
|
+
conversation: true,
|
|
1026
1524
|
};
|
|
1027
|
-
const ctx = this.buildSessionRunnerContext();
|
|
1028
1525
|
|
|
1029
|
-
// detached 编排:轮次在 background
|
|
1030
|
-
//
|
|
1526
|
+
// detached 编排:轮次在 background 跑(协议 run chat + resume),pool 重新
|
|
1527
|
+
// acquire(轮次间 idle 已 release)。chatMode 轮终 settle 分流自动
|
|
1528
|
+
// finalizeRoundToIdle(record 回 idle、round+1)。
|
|
1031
1529
|
// [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
|
|
1032
|
-
//
|
|
1530
|
+
// (冷路径重入)在守卫处被拒;轮次收尾 finally 统一清除。
|
|
1033
1531
|
this.resumesInFlight.add(record.id);
|
|
1034
|
-
this.kickOffChatRound(record, opts, identity,
|
|
1532
|
+
this.kickOffChatRound(record, opts, identity, record.controller.signal, PRIORITY_BACKGROUND, resume);
|
|
1035
1533
|
}
|
|
1036
1534
|
|
|
1037
1535
|
/**
|
|
@@ -1059,6 +1557,9 @@ export class SubagentService {
|
|
|
1059
1557
|
`terminating (LC-1 wedge recovery)`,
|
|
1060
1558
|
);
|
|
1061
1559
|
killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
|
|
1560
|
+
// [W3] 子进程在引擎进程内——终止经协议 interact cancel(SIGTERM → 引擎侧
|
|
1561
|
+
// settle 等待 → 杀链升级);镜像置死位由上方 killRecordChildWithEscalation 记账。
|
|
1562
|
+
this.terminateChatSession(record, "cancel", "settled watchdog (hot path)");
|
|
1062
1563
|
const failedResult: AgentResult = {
|
|
1063
1564
|
text: "",
|
|
1064
1565
|
turns: record.turnCount,
|
|
@@ -1081,12 +1582,12 @@ export class SubagentService {
|
|
|
1081
1582
|
// [D4-①] 通知簇搬移 notify-host 后的遗留调用点修正:this.notifyComplete 方法已
|
|
1082
1583
|
// 不存在(其余三处调用点均 this.notifyHost.notifyComplete),旧引用 throw
|
|
1083
1584
|
// TypeError 被本 .catch 吞掉 → settled watchdog 失败通知静默丢失
|
|
1084
|
-
.then(() => this.
|
|
1585
|
+
.then(() => this.collectCoordinator.route(record))
|
|
1085
1586
|
.catch((err: unknown) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
|
|
1086
1587
|
}
|
|
1087
1588
|
|
|
1088
1589
|
// [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
|
|
1089
|
-
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/
|
|
1590
|
+
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/inproc pi-engine(已删) 的
|
|
1090
1591
|
// rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
|
|
1091
1592
|
// 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
|
|
1092
1593
|
|
|
@@ -1210,13 +1711,16 @@ export class SubagentService {
|
|
|
1210
1711
|
*/
|
|
1211
1712
|
private async closeChatIdle(record: ExecutionRecord): Promise<void> {
|
|
1212
1713
|
// [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
|
|
1213
|
-
// [T2④ / LC-2]
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1714
|
+
// [T2④ / LC-2] 终止语义收敛:SIGTERM 被无视时 30s 升级 SIGKILL——终态化后
|
|
1715
|
+
// record 已 archive,挂住 = 幽灵进程无任何后续回收通道。同时 disarm settled
|
|
1716
|
+
// watchdog(本路径终态化 = settled 等待窗口终结,防 watchdog 误杀后续同 id 资源)。
|
|
1717
|
+
// [W3] 实际终止在引擎进程内(协议 interact close force → 引擎侧杀链);
|
|
1718
|
+
// killRecordChildWithEscalation 只做镜像置死记账。
|
|
1217
1719
|
disarmIdleTimer(record.id);
|
|
1218
1720
|
disarmSettledWatchdog(record.id);
|
|
1721
|
+
disarmRoundFromProtocol(record.id);
|
|
1219
1722
|
killRecordChildWithEscalation(record.id, "closeChatIdle");
|
|
1723
|
+
this.terminateChatSession(record, "close", "closeChatIdle");
|
|
1220
1724
|
// 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
|
|
1221
1725
|
// `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
|
|
1222
1726
|
// completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
|
|
@@ -1238,6 +1742,8 @@ export class SubagentService {
|
|
|
1238
1742
|
modelService: this.modelService,
|
|
1239
1743
|
pi: this.pi,
|
|
1240
1744
|
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
1745
|
+
// [F-5 修复] 同 finalizeRecord——chat 轮路由注销单一汇聚点钩子。
|
|
1746
|
+
onFinalized: (id) => this.unregisterChatRoundRoute(id),
|
|
1241
1747
|
sessionDir: this.sessionsDir,
|
|
1242
1748
|
},
|
|
1243
1749
|
record,
|
|
@@ -1269,11 +1775,14 @@ export class SubagentService {
|
|
|
1269
1775
|
*/
|
|
1270
1776
|
private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
|
|
1271
1777
|
// 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
|
|
1272
|
-
// [T2④ / LC-2]
|
|
1273
|
-
//
|
|
1778
|
+
// [T2④ / LC-2] 终止语义收敛(30s 升级 SIGKILL,终态化后无后续回收通道);
|
|
1779
|
+
// settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
|
|
1780
|
+
// force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
|
|
1274
1781
|
disarmIdleTimer(record.id);
|
|
1275
1782
|
disarmSettledWatchdog(record.id);
|
|
1783
|
+
disarmRoundFromProtocol(record.id);
|
|
1276
1784
|
killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
|
|
1785
|
+
this.terminateChatSession(record, "close", "closeAfterRoundSettled");
|
|
1277
1786
|
if (!tryTransition(record, "closed", "user-close")) {
|
|
1278
1787
|
return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
|
|
1279
1788
|
}
|
|
@@ -1364,17 +1873,13 @@ export class SubagentService {
|
|
|
1364
1873
|
}
|
|
1365
1874
|
}
|
|
1366
1875
|
|
|
1367
|
-
// ── 步骤 3
|
|
1368
|
-
const ctx = this.buildSessionRunnerContext(opts.cwd);
|
|
1369
|
-
|
|
1370
|
-
// ── 步骤 4: signal 决议 ──
|
|
1876
|
+
// ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
|
|
1371
1877
|
const effectiveSignal = signal ?? record.controller?.signal;
|
|
1372
1878
|
|
|
1373
1879
|
// 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
|
|
1374
1880
|
const result = await this.runAndFinalize(
|
|
1375
1881
|
record,
|
|
1376
1882
|
{ ...opts, worktree: worktreeHandle },
|
|
1377
|
-
ctx,
|
|
1378
1883
|
identity,
|
|
1379
1884
|
effectiveSignal,
|
|
1380
1885
|
PRIORITY_BACKGROUND,
|
|
@@ -1427,6 +1932,23 @@ export class SubagentService {
|
|
|
1427
1932
|
return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? undefined);
|
|
1428
1933
|
}
|
|
1429
1934
|
|
|
1935
|
+
/**
|
|
1936
|
+
* collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
|
|
1937
|
+
* startHandler 缺省 collect 解析用)。
|
|
1938
|
+
* config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
|
|
1939
|
+
* 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
|
|
1940
|
+
* reloadGlobalConfig 刷新)。
|
|
1941
|
+
*/
|
|
1942
|
+
/** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
|
|
1943
|
+
* budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
|
|
1944
|
+
private collectSyncSection() {
|
|
1945
|
+
return this.modelService.getGlobalConfig().collectSync;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
getCollectSyncDefault(): "async" | "sync" {
|
|
1949
|
+
return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1430
1952
|
/** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
|
|
1431
1953
|
* 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
|
|
1432
1954
|
* 返回 undefined:id 不存在于内存与磁盘。 */
|
|
@@ -1506,19 +2028,21 @@ export class SubagentService {
|
|
|
1506
2028
|
opts: ExecuteOptions,
|
|
1507
2029
|
): ResolvedIdentity {
|
|
1508
2030
|
const canonical = validateModelForEngine(engine, engineModel);
|
|
1509
|
-
// record.model 留痕:canonical
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
2031
|
+
// record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
|
|
2032
|
+
// 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
|
|
2033
|
+
// 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
|
|
2034
|
+
// 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
|
|
2035
|
+
// 整串进 name,不再落 "<ref>/" 畸形)。
|
|
1512
2036
|
const modelStr = canonical ?? engineModel ?? "";
|
|
1513
|
-
const
|
|
2037
|
+
const model = splitEngineModelRef(modelStr);
|
|
1514
2038
|
return {
|
|
1515
2039
|
agent,
|
|
1516
2040
|
agentConfig,
|
|
1517
2041
|
resolved: {
|
|
1518
2042
|
model: {
|
|
1519
|
-
id:
|
|
1520
|
-
name:
|
|
1521
|
-
provider:
|
|
2043
|
+
id: model.id,
|
|
2044
|
+
name: model.name,
|
|
2045
|
+
provider: model.provider,
|
|
1522
2046
|
reasoning: false,
|
|
1523
2047
|
},
|
|
1524
2048
|
thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
@@ -1548,7 +2072,10 @@ export class SubagentService {
|
|
|
1548
2072
|
|
|
1549
2073
|
const record = createRecord(id, {
|
|
1550
2074
|
agent: identity.agent,
|
|
1551
|
-
model
|
|
2075
|
+
// model 留痕词形与拆分同源(joinEngineModelRef):provider 为空串只写 id——
|
|
2076
|
+
// 契约变更④的无斜杠 ref(provider=""/id=ref)不得落成 "/ref" 或 "ref/" 畸形;
|
|
2077
|
+
// 续聊回读侧 splitEngineModelRef 对无斜杠串还原 provider=""/id=ref,往返自洽。
|
|
2078
|
+
model: joinEngineModelRef(identity.resolved.model),
|
|
1552
2079
|
thinkingLevel: identity.resolved.thinkingLevel,
|
|
1553
2080
|
mode,
|
|
1554
2081
|
task: opts.task,
|
|
@@ -1563,6 +2090,10 @@ export class SubagentService {
|
|
|
1563
2090
|
// 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
|
|
1564
2091
|
engine: opts.engine,
|
|
1565
2092
|
engineFallback: opts.engineFallback,
|
|
2093
|
+
// subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
|
|
2094
|
+
// 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
|
|
2095
|
+
// undefined = async(缺省语义,旧记录零迁移)。
|
|
2096
|
+
collectMode: opts.collect === "sync" ? "sync" : undefined,
|
|
1566
2097
|
controller,
|
|
1567
2098
|
});
|
|
1568
2099
|
|
|
@@ -1598,6 +2129,12 @@ export class SubagentService {
|
|
|
1598
2129
|
// 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
|
|
1599
2130
|
// 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
|
|
1600
2131
|
// 检查若只落在 engine.run 内则拒绝异步化为「派发成功 + 静默失败 record」)。
|
|
2132
|
+
// [W3 契约变更③补注(协议化能力位方向判定)] 同步拒只覆盖「manifest 少声明」
|
|
2133
|
+
// 方向;**manifest 多声明**(声明支持而引擎实际不支持)由首个 run 的协议握手
|
|
2134
|
+
// `initialize` 发现 → engine_capability_mismatch 该 run 失败 + record 标 failed,
|
|
2135
|
+
// 并**清理 run 前已建的前置副作用**(worktree 经 finalizeFailed → finalizeRecord
|
|
2136
|
+
// Step 3b cleanupWorktreeIfBound 清理)。非 gate 位不一致(无论强弱)一律
|
|
2137
|
+
// warn 留痕不阻断(诊断面归 EngineClient,设计 §3.3 能力位段)。
|
|
1601
2138
|
assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
|
|
1602
2139
|
|
|
1603
2140
|
// identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
|
|
@@ -1668,13 +2205,13 @@ export class SubagentService {
|
|
|
1668
2205
|
}
|
|
1669
2206
|
|
|
1670
2207
|
if (isPiRoute) {
|
|
1671
|
-
// pi:
|
|
1672
|
-
|
|
2208
|
+
// pi:chat 轮次经协议 run(会话形态 chat{recordId})发往 pi-subagent-cli 引擎
|
|
2209
|
+
// 进程(W3——与 run 域 remote-engine 路由同构),编排收尾(notify/终态迁移)
|
|
2210
|
+
// 与旧 pi 主路径语义一致。
|
|
1673
2211
|
this.kickOffChatRound(
|
|
1674
2212
|
record,
|
|
1675
2213
|
{ ...recordOpts, worktree: worktreeHandle },
|
|
1676
2214
|
identity,
|
|
1677
|
-
this.buildSessionRunnerContext(opts.cwd),
|
|
1678
2215
|
record.controller!.signal,
|
|
1679
2216
|
PRIORITY_BACKGROUND,
|
|
1680
2217
|
);
|
|
@@ -1695,6 +2232,8 @@ export class SubagentService {
|
|
|
1695
2232
|
*/
|
|
1696
2233
|
private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
|
|
1697
2234
|
const signal = record.controller?.signal;
|
|
2235
|
+
// [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
|
|
2236
|
+
this.roundSupervisor.noteRunStarted(record.id);
|
|
1698
2237
|
void (async () => {
|
|
1699
2238
|
try {
|
|
1700
2239
|
await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
|
|
@@ -1711,13 +2250,17 @@ export class SubagentService {
|
|
|
1711
2250
|
// DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
|
|
1712
2251
|
// maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
|
|
1713
2252
|
try {
|
|
1714
|
-
|
|
2253
|
+
// [W4] adopted = 走了表 3 行 1 接管分支(record 保持 resumable 交监督器)——
|
|
2254
|
+
// 跳过 bg 完成回注(合并单条通知由监督器 sendMergedFailureNotice 承担,
|
|
2255
|
+
// route 会再发一条 toNotifyRecord 投影通知 = 双通知,正是 R3 要消除的时序窗口)。
|
|
2256
|
+
const adopted = await this.runEngineTask(record, opts, engine, signal);
|
|
1715
2257
|
// cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
|
|
1716
|
-
if (record.closedReason !== "cancelled") {
|
|
1717
|
-
this.
|
|
2258
|
+
if (!adopted && record.closedReason !== "cancelled") {
|
|
2259
|
+
this.collectCoordinator.route(record);
|
|
1718
2260
|
}
|
|
1719
2261
|
} finally {
|
|
1720
2262
|
this.pool.release();
|
|
2263
|
+
this.roundSupervisor.noteRunEnded(record.id);
|
|
1721
2264
|
}
|
|
1722
2265
|
})();
|
|
1723
2266
|
}
|
|
@@ -1733,7 +2276,7 @@ export class SubagentService {
|
|
|
1733
2276
|
opts: ExecuteOptions,
|
|
1734
2277
|
engine: EnginePort,
|
|
1735
2278
|
signal: AbortSignal | undefined,
|
|
1736
|
-
): Promise<
|
|
2279
|
+
): Promise<boolean> {
|
|
1737
2280
|
// [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
|
|
1738
2281
|
//(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
|
|
1739
2282
|
// 不传 forwardEvents。
|
|
@@ -1784,15 +2327,49 @@ export class SubagentService {
|
|
|
1784
2327
|
journalPath: journal.path,
|
|
1785
2328
|
};
|
|
1786
2329
|
await journal.close();
|
|
1787
|
-
await this.finalizeEngineOutcome(record, outcome);
|
|
2330
|
+
return await this.finalizeEngineOutcome(record, outcome);
|
|
1788
2331
|
} catch (err) {
|
|
1789
2332
|
// engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
|
|
1790
2333
|
// journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
|
|
1791
2334
|
await journal.close();
|
|
2335
|
+
// [W4 表 3 行 1] 引擎进程死亡(engine_crashed——run 帧已受理后进程死亡/stdin
|
|
2336
|
+
// 写失败)且宿主存活 → record 保持 resumable 交监督器接管(禁 completed 谎报、
|
|
2337
|
+
// 禁直接 closed 终局——resume 锚点在盘,逻辑任务可续)。prepare 期失败
|
|
2338
|
+
// (handshake/protocol/model 拒——进程创建前)维持现状 closed(确定性失败,
|
|
2339
|
+
// 保持 resumable 无意义)。
|
|
2340
|
+
if (
|
|
2341
|
+
err instanceof EngineSdkError &&
|
|
2342
|
+
err.code === "engine_crashed" &&
|
|
2343
|
+
record.chatMode !== true &&
|
|
2344
|
+
record.status === "running"
|
|
2345
|
+
) {
|
|
2346
|
+
this.adoptResumableAfterEngineDeath(record, toErrorMessage(err));
|
|
2347
|
+
return true;
|
|
2348
|
+
}
|
|
1792
2349
|
await this.finalizeFailed(record, err);
|
|
2350
|
+
return false;
|
|
1793
2351
|
}
|
|
1794
2352
|
}
|
|
1795
2353
|
|
|
2354
|
+
/**
|
|
2355
|
+
* [W4 表 3 行 1] 引擎/子进程死亡、宿主存活的 record 处置:run 终态如实记 failed
|
|
2356
|
+
* 证据(record.error),record **保持 resumable**(session 文件在盘,逻辑任务可续
|
|
2357
|
+
* ——冷路径 resume 可直接续写),交监督器接管(合并单条通知 + 三态判定)。禁止
|
|
2358
|
+
* 两个事故方向:completed 谎报(G3)与 closed 直接终局(resume 可能性丢失,等待
|
|
2359
|
+
* 无主——2026-09-08 事故环 2/3 的 core 侧形态)。
|
|
2360
|
+
*
|
|
2361
|
+
* 判据状态源钉死 record 级:写点只动 record 字段(resumable/result/error),
|
|
2362
|
+
* 不清镜像不查引擎——引擎进程被动重建(ensureConnected 退避重填镜像)不翻转
|
|
2363
|
+
* 本处置(纳管模型:死亡事件纳管、重建不解管)。
|
|
2364
|
+
*/
|
|
2365
|
+
private adoptResumableAfterEngineDeath(record: ExecutionRecord, errMsg: string): void {
|
|
2366
|
+
record.error = errMsg;
|
|
2367
|
+
record.result = undefined;
|
|
2368
|
+
record.resumable = true;
|
|
2369
|
+
this.store.reportRecordTransition(record);
|
|
2370
|
+
this.roundSupervisor.adoptOnProcessDeath(record, errMsg);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
1796
2373
|
/**
|
|
1797
2374
|
* 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
|
|
1798
2375
|
* 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
|
|
@@ -1804,43 +2381,50 @@ export class SubagentService {
|
|
|
1804
2381
|
/**
|
|
1805
2382
|
* engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
|
|
1806
2383
|
* 否则 done(result=content)。CAS 抢锁(tryTransition)防与 cancelBackground 双收尾。
|
|
2384
|
+
*
|
|
2385
|
+
* [W4 表 3 行 1] 进程死亡分诊:outcome.error 存在且 **exitCode === null**(引擎侧
|
|
2386
|
+
* 进程被信号终止/崩溃的合成 outcome 形态——RemoteEngine 运行中失败合成分支恒
|
|
2387
|
+
* exitCode:null,引擎如实上报的 turn 失败带数值 exitCode;engine-client 注释同源
|
|
2388
|
+
* 「exitCode null = 被信号杀死,杀链判据」)且宿主存活且非 conversation 形态 →
|
|
2389
|
+
* record 保持 resumable 交监督器接管(如实 failed 证据 + 合并通知 + 三态判定),
|
|
2390
|
+
* 不再 closed 终局。返回 true = 走了接管分支(调用方跳过 bg 完成回注)。
|
|
1807
2391
|
*/
|
|
1808
|
-
private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<
|
|
2392
|
+
private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
|
|
1809
2393
|
if (outcome.sessionFile !== undefined) {
|
|
1810
2394
|
record.sessionFile = outcome.sessionFile;
|
|
1811
2395
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2396
|
+
if (
|
|
2397
|
+
outcome.error !== undefined &&
|
|
2398
|
+
outcome.exitCode === null &&
|
|
2399
|
+
record.chatMode !== true &&
|
|
2400
|
+
record.status === "running"
|
|
2401
|
+
) {
|
|
2402
|
+
this.adoptResumableAfterEngineDeath(record, outcome.error);
|
|
2403
|
+
return true;
|
|
2404
|
+
}
|
|
2405
|
+
const result = this.outcomeToAgentResult(record, outcome);
|
|
1822
2406
|
if (tryTransition(record, "closed", "gc")) {
|
|
1823
2407
|
await this.finalizeRecord(record, result, "closed", "gc");
|
|
1824
2408
|
}
|
|
2409
|
+
return false;
|
|
1825
2410
|
}
|
|
1826
2411
|
|
|
1827
|
-
// ── 执行内部:run + finalize(sync
|
|
2412
|
+
// ── 执行内部:run + finalize(workflow 域 sync 面;chat 域轮次走 kickOffChatRound)──
|
|
1828
2413
|
|
|
1829
|
-
/**
|
|
2414
|
+
/**
|
|
2415
|
+
* workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
|
|
2416
|
+
* runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
|
|
2417
|
+
* 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
|
|
2418
|
+
*/
|
|
1830
2419
|
private async runAndFinalize(
|
|
1831
2420
|
record: ExecutionRecord,
|
|
1832
2421
|
opts: ExecuteOptions,
|
|
1833
|
-
ctx: SessionRunnerContext,
|
|
1834
2422
|
identity: ResolvedIdentity,
|
|
1835
2423
|
signal: AbortSignal | undefined,
|
|
1836
2424
|
priority: number,
|
|
1837
|
-
|
|
2425
|
+
onEvent?: (event: AgentEvent) => void,
|
|
1838
2426
|
stream?: SubagentStream,
|
|
1839
|
-
/** resume 选项(M2-B1):透传 runSpawn,重开已 idle 的 session 续聊。undefined = 新 session。 */
|
|
1840
|
-
resume?: SpawnResumeOpts,
|
|
1841
2427
|
): Promise<AgentResult> {
|
|
1842
|
-
// [U04 阶段化提取] 主函数只留编排,按「装配(池槽/worktree/深度)→ 执行(ALS 包装
|
|
1843
|
-
// spawn)→ 回收(finally)→ 错误收口 → 终态收口」分段;行为逐字节不变。
|
|
1844
2428
|
const pooled = record.mode === "background";
|
|
1845
2429
|
let acquired = false;
|
|
1846
2430
|
if (pooled) {
|
|
@@ -1848,48 +2432,106 @@ export class SubagentService {
|
|
|
1848
2432
|
if (acquireFailure !== undefined) return acquireFailure;
|
|
1849
2433
|
acquired = true;
|
|
1850
2434
|
}
|
|
1851
|
-
// onEvent 直通(原此处曾有 onUpdate(project(record)) 节流回流包装——生产死路径,
|
|
1852
|
-
// 三调用点恒 onUpdate: undefined、仅测试触达,已按 swf-perf-impl ledger #22 删除
|
|
1853
|
-
// (决策 TC4/IF14,详见 .cw/swf-perf-impl/cleanup-slice-design.json;git 历史可完整恢复)。
|
|
1854
|
-
// ExecuteOptions.onUpdate 字段一并删除,未来误用将编译期失败而非静默无效。
|
|
1855
|
-
const onEvent = rawOnEvent;
|
|
1856
2435
|
|
|
1857
|
-
|
|
1858
|
-
|
|
2436
|
+
// worktree 句柄经 executeOptionsToEngineTaskSpec(opts).worktree 直传引擎
|
|
2437
|
+
//(AgentCallOpts.worktree 接受 WorktreeHandle——原 runSpawn 显式传参的协议形态)。
|
|
2438
|
+
const engine = this.resolveChatEnginePort();
|
|
2439
|
+
// journal 接线(D6 第②级,与 kickOffEngineRun 同一实现):forwardEvents = onEvent
|
|
2440
|
+
//(workflow liveRecord 桥接,D-A8)。
|
|
2441
|
+
const journal = wireEventJournal({ engineId: engine.id, taskId: record.id, forwardEvents: onEvent });
|
|
1859
2442
|
|
|
1860
2443
|
let result: AgentResult;
|
|
1861
2444
|
try {
|
|
1862
|
-
|
|
1863
|
-
record,
|
|
1864
|
-
|
|
1865
|
-
|
|
2445
|
+
const runCtx: RunContext = {
|
|
2446
|
+
taskId: record.id,
|
|
2447
|
+
poolKey: JOURNAL_INITIAL_POOL_KEY,
|
|
2448
|
+
signal,
|
|
2449
|
+
ctxModel: identity.resolved.model,
|
|
2450
|
+
onEvent: journal.onEvent,
|
|
2451
|
+
onPoolResolved: journal.onPoolResolved,
|
|
2452
|
+
...(stream !== undefined ? { stream } : {}),
|
|
2453
|
+
...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
|
|
2454
|
+
// D10 终止链:引擎 spawn 的子进程注册进 spawnedChildren 镜像记账
|
|
2455
|
+
onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
|
|
2456
|
+
};
|
|
2457
|
+
const { handle, outcome } = await engine.run(this.taskSpecWithModel(opts, record.model), runCtx);
|
|
2458
|
+
record.engineHandle = {
|
|
2459
|
+
sessionRef: handle.data.sessionRef,
|
|
2460
|
+
poolKey: handle.data.poolKey,
|
|
2461
|
+
journalPath: journal.path,
|
|
2462
|
+
};
|
|
2463
|
+
await journal.close();
|
|
2464
|
+
result = this.outcomeToAgentResult(record, outcome);
|
|
1866
2465
|
} catch (err) {
|
|
1867
|
-
|
|
1868
|
-
//
|
|
1869
|
-
// swallow(不 re-throw):sync 调用方拿到合成 failed result
|
|
1870
|
-
//
|
|
2466
|
+
await journal.close();
|
|
2467
|
+
// engine.run prepare 期 reject(进程创建前)→ 合成 failed result + 收尾。
|
|
2468
|
+
// swallow(不 re-throw):sync 调用方拿到合成 failed result,避免异常逃逸到
|
|
2469
|
+
// tool 层 + record 卡 running。
|
|
1871
2470
|
if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
|
|
1872
2471
|
return this.finalizeFailed(record, err);
|
|
1873
2472
|
} finally {
|
|
1874
2473
|
this.releaseRoundResources(record, pooled && acquired, stream);
|
|
1875
2474
|
}
|
|
1876
2475
|
|
|
1877
|
-
// [V2 决策 2/3] chatMode 首轮闭环:runSpawn 因 agent_settled 提前 resolve(onRoundSettled
|
|
1878
|
-
// 已设 record.status=idle + round+=1 + notify 主 agent)。进程仍保活(idle timer armed),
|
|
1879
|
-
// 不进下方 chatMode 分流(那是 close 后 done/failed/cancelled 终态化的,走 finalizeRoundToIdle
|
|
1880
|
-
// / finalizeRecord)。tryTransition(idle→done) 天然失败(要求 status==="running"),此处显式
|
|
1881
|
-
// early return 让语义清晰 + 防状态机未来改动。防 double-notify 由 notifier dedup 兜底
|
|
1882
|
-
//(同 id:round 60s 内吞,chat 轮次收尾 .then 的 notify 是 no-op,见 notifier.ts L122)。
|
|
1883
|
-
if (record.chatMode && isIdle(record)) {
|
|
1884
|
-
return result;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
2476
|
// v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
|
|
1888
|
-
|
|
1889
|
-
await this.settleFinalOutcome(record, result, aborted, this.deriveClosedReason(aborted, result.success));
|
|
2477
|
+
await this.settleOneShotOutcome(record, result, signal?.aborted === true);
|
|
1890
2478
|
return result;
|
|
1891
2479
|
}
|
|
1892
2480
|
|
|
2481
|
+
/** engine.run taskSpec 装配单一来源(runAndFinalize 与 kickOffChatRound 共用):
|
|
2482
|
+
* executeOptions 协议映射 + model = record 留痕词形(resolved 解析产物,
|
|
2483
|
+
* joinEngineModelRef 规范形)覆盖——原 identity.resolved 经 runSpawn --model
|
|
2484
|
+
* 兜底的协议等价承载。 */
|
|
2485
|
+
private taskSpecWithModel(opts: ExecuteOptions, model: string | undefined): AgentCallOpts {
|
|
2486
|
+
return { ...executeOptionsToEngineTaskSpec(opts), ...(model !== undefined ? { model } : {}) };
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* one-shot(非 chatMode)终态收口(原 settleOneShotOutcome 分支):成功轮消费
|
|
2491
|
+
* closeAfterRound 挂起标志;失败/取消一次性销毁。CAS 抢锁失败(cancel/dispose 抢先
|
|
2492
|
+
* 终态化)静默跳过。runAndFinalize(workflow 域)与 kickOffChatRound 非 chatMode
|
|
2493
|
+
* 分支共用。
|
|
2494
|
+
*/
|
|
2495
|
+
private async settleOneShotOutcome(
|
|
2496
|
+
record: ExecutionRecord,
|
|
2497
|
+
result: AgentResult,
|
|
2498
|
+
aborted: boolean,
|
|
2499
|
+
): Promise<void> {
|
|
2500
|
+
if (!tryTransition(record, "closed", aborted ? "cancelled" : result.success ? "user-close" : "gc")) return;
|
|
2501
|
+
if (!aborted && result.success && record.closeAfterRound === true) {
|
|
2502
|
+
// [M5] busy 时 close(force:false) 置的标志在本轮完成时消费终态化。
|
|
2503
|
+
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2504
|
+
} else if (!aborted && result.success) {
|
|
2505
|
+
// [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
|
|
2506
|
+
await this.finalizeRoundToIdle(record, result);
|
|
2507
|
+
} else if (!aborted && record.closeAfterRound === true) {
|
|
2508
|
+
// [M5] 优雅关闭挂起的失败轮:轮已完成即兑现 close 意图终态化(含本轮 result)。
|
|
2509
|
+
await this.consumeCloseAfterRound(record, result, "gc");
|
|
2510
|
+
} else {
|
|
2511
|
+
// 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
|
|
2512
|
+
await this.finalizeRecord(record, result, "closed", aborted ? "cancelled" : "gc");
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/** AgentOutcome → execution AgentResult 单一映射源(workflow run 域映射;
|
|
2517
|
+
* finalizeEngineOutcome 终态 result 复用此处构造——exitCode null = 被信号杀死的
|
|
2518
|
+
* 合成终态,error 如实透传)。 */
|
|
2519
|
+
private outcomeToAgentResult(record: ExecutionRecord, outcome: AgentOutcome): AgentResult {
|
|
2520
|
+
if (outcome.sessionFile !== undefined) {
|
|
2521
|
+
record.sessionFile = outcome.sessionFile;
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
text: outcome.content,
|
|
2525
|
+
turns: outcome.usage?.turns ?? 0,
|
|
2526
|
+
durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
|
|
2527
|
+
success: outcome.error === undefined,
|
|
2528
|
+
...(outcome.error !== undefined ? { error: outcome.error } : {}),
|
|
2529
|
+
sessionId: outcome.sessionId ?? record.id,
|
|
2530
|
+
toolCalls: [],
|
|
2531
|
+
...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
|
|
1893
2535
|
/** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
|
|
1894
2536
|
* 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
|
|
1895
2537
|
* 不触发轮次资源回收——与原控制流逐字节一致)。 */
|
|
@@ -1913,69 +2555,16 @@ export class SubagentService {
|
|
|
1913
2555
|
return typeof opts.worktree === "object" ? opts.worktree : undefined;
|
|
1914
2556
|
}
|
|
1915
2557
|
|
|
1916
|
-
/** [U04 提取·装配] [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。 */
|
|
1917
|
-
private resolveForkDepths(opts: ExecuteOptions): { parentDepth: number; effectiveDepth: number } {
|
|
1918
|
-
const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
|
|
1919
|
-
const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
|
|
1920
|
-
return { parentDepth, effectiveDepth };
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
/** [U04 提取·执行] runSpawn 的两层 ALS 包装(forkDepthAls 外层 + execNesting 内层)。 */
|
|
1924
|
-
private runSpawnNested(
|
|
1925
|
-
record: ExecutionRecord,
|
|
1926
|
-
opts: ExecuteOptions,
|
|
1927
|
-
ctx: SessionRunnerContext,
|
|
1928
|
-
identity: ResolvedIdentity,
|
|
1929
|
-
signal: AbortSignal | undefined,
|
|
1930
|
-
onEvent: ((event: AgentEvent) => void) | undefined,
|
|
1931
|
-
stream: SubagentStream | undefined,
|
|
1932
|
-
resume: SpawnResumeOpts | undefined,
|
|
1933
|
-
worktreeHandle: WorktreeHandle | undefined,
|
|
1934
|
-
parentDepth: number,
|
|
1935
|
-
effectiveDepth: number,
|
|
1936
|
-
): Promise<AgentResult> {
|
|
1937
|
-
// 嵌套上下文包在 forkDepthAls 内层:B run() 期间挂 {recordId:B.id,depth:B.depth},
|
|
1938
|
-
// B 内创建 C 时 createRecordForMode 读到 B → C 挂到 B 名下。两层 ALS 独立但同生命周期。
|
|
1939
|
-
return this.forkDepthAls.run(effectiveDepth, () =>
|
|
1940
|
-
this.execNesting.run(
|
|
1941
|
-
{ recordId: record.id, depth: record.depth },
|
|
1942
|
-
() => runSpawn(record, opts.task, {
|
|
1943
|
-
resolved: identity.resolved,
|
|
1944
|
-
agentConfig: identity.agentConfig,
|
|
1945
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
1946
|
-
skillPath: opts.skillPath,
|
|
1947
|
-
schema: opts.schema,
|
|
1948
|
-
schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
|
|
1949
|
-
maxTurns: opts.maxTurns,
|
|
1950
|
-
graceTurns: opts.graceTurns,
|
|
1951
|
-
signal,
|
|
1952
|
-
onEvent,
|
|
1953
|
-
stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
|
|
1954
|
-
fork: opts.fork,
|
|
1955
|
-
// [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
|
|
1956
|
-
// opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
|
|
1957
|
-
forkSource: opts.forkFromSessionFile,
|
|
1958
|
-
worktree: worktreeHandle,
|
|
1959
|
-
parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
|
|
1960
|
-
}, ctx, resume),
|
|
1961
|
-
),
|
|
1962
|
-
);
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
2558
|
/** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
|
|
1966
|
-
* 成功)、streaming widget
|
|
2559
|
+
* 成功)、streaming widget 清除。 */
|
|
1967
2560
|
private releaseRoundResources(
|
|
1968
|
-
|
|
2561
|
+
_record: ExecutionRecord,
|
|
1969
2562
|
holdSlot: boolean,
|
|
1970
2563
|
stream: SubagentStream | undefined,
|
|
1971
2564
|
): void {
|
|
1972
2565
|
if (holdSlot) this.pool.release();
|
|
1973
2566
|
// 清除 streaming widget(subagent 终态,幂等)
|
|
1974
2567
|
stream?.dispose();
|
|
1975
|
-
// [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
|
|
1976
|
-
// MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
|
|
1977
|
-
// execute() 新建 record 不在集合,delete 是 no-op。
|
|
1978
|
-
this.resumesInFlight.delete(record.id);
|
|
1979
2568
|
}
|
|
1980
2569
|
|
|
1981
2570
|
/** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
|
|
@@ -1992,6 +2581,7 @@ export class SubagentService {
|
|
|
1992
2581
|
sessionId: record.id,
|
|
1993
2582
|
toolCalls: [],
|
|
1994
2583
|
};
|
|
2584
|
+
disarmRoundFromProtocol(record.id);
|
|
1995
2585
|
if (tryTransition(record, "closed", "gc")) {
|
|
1996
2586
|
// 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
|
|
1997
2587
|
await this.finalizeRoundToIdle(record, failedResult);
|
|
@@ -1999,81 +2589,6 @@ export class SubagentService {
|
|
|
1999
2589
|
return failedResult;
|
|
2000
2590
|
}
|
|
2001
2591
|
|
|
2002
|
-
/** [U04 提取·收口派生] closedReason 派生:aborted → cancelled;否则 success → user-close,!success → gc。 */
|
|
2003
|
-
private deriveClosedReason(aborted: boolean, success: boolean): ClosedReason {
|
|
2004
|
-
return aborted ? "cancelled" : success ? "user-close" : "gc";
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
/** [U04 提取·终态收口] CAS 抢锁:抢到则按 chatMode 分流完整收尾;没抢到(cancel 已先设
|
|
2008
|
-
* closed+cancelled)则跳过。 */
|
|
2009
|
-
private async settleFinalOutcome(
|
|
2010
|
-
record: ExecutionRecord,
|
|
2011
|
-
result: AgentResult,
|
|
2012
|
-
aborted: boolean,
|
|
2013
|
-
closedReason: ClosedReason,
|
|
2014
|
-
): Promise<void> {
|
|
2015
|
-
if (!tryTransition(record, "closed", closedReason)) return;
|
|
2016
|
-
if (record.chatMode) {
|
|
2017
|
-
await this.settleChatRoundOutcome(record, result, aborted, closedReason);
|
|
2018
|
-
} else {
|
|
2019
|
-
await this.settleOneShotOutcome(record, result, aborted, closedReason);
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
/** [U04 提取·终态收口] chatMode 轮次分流(原 A/B 分支,De Morgan 等价拆分):
|
|
2024
|
-
* 成功轮(!aborted && success)与失败/取消轮(其补集 !success || aborted)各自处理
|
|
2025
|
-
* closeAfterRound 挂起语义。 */
|
|
2026
|
-
private async settleChatRoundOutcome(
|
|
2027
|
-
record: ExecutionRecord,
|
|
2028
|
-
result: AgentResult,
|
|
2029
|
-
aborted: boolean,
|
|
2030
|
-
closedReason: ClosedReason,
|
|
2031
|
-
): Promise<void> {
|
|
2032
|
-
if (!aborted && result.success) {
|
|
2033
|
-
if (record.closeAfterRound) {
|
|
2034
|
-
// close 优雅关闭(force:false):当前轮完成后终态化为 closed。
|
|
2035
|
-
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2036
|
-
} else {
|
|
2037
|
-
// 对话模式轮次成功完成 → 保持 running(旧 idle 折入 running,finalizeRoundToIdle 设回 running)。
|
|
2038
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2039
|
-
}
|
|
2040
|
-
} else {
|
|
2041
|
-
// MF-6:chatMode 轮次失败/取消不销毁对话——回退 running-resumable(旧 idle,可恢复)。
|
|
2042
|
-
if (record.closeAfterRound) {
|
|
2043
|
-
// [M5] 优雅关闭挂起的失败/取消轮:轮已完成即兑现 close 意图终态化(含本轮 result),
|
|
2044
|
-
// 不再回退 resumable——否则标志残留到下一轮,record 已被 tool 谎报 closed。
|
|
2045
|
-
await this.consumeCloseAfterRound(record, result, closedReason);
|
|
2046
|
-
} else {
|
|
2047
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/** [U04 提取·终态收口] one-shot(非 chatMode)分流(原 C/D 分支):成功轮消费
|
|
2053
|
-
* closeAfterRound 挂起标志;失败/取消一次性销毁。 */
|
|
2054
|
-
private async settleOneShotOutcome(
|
|
2055
|
-
record: ExecutionRecord,
|
|
2056
|
-
result: AgentResult,
|
|
2057
|
-
aborted: boolean,
|
|
2058
|
-
closedReason: ClosedReason,
|
|
2059
|
-
): Promise<void> {
|
|
2060
|
-
if (!aborted && result.success) {
|
|
2061
|
-
if (record.closeAfterRound) {
|
|
2062
|
-
// [M5] 非 chatMode(one-shot)busy 时 close(force:false) 置的标志在本轮完成时消费
|
|
2063
|
-
// 终态化(对齐 close schema 文案 "release its resources")。旧代码走
|
|
2064
|
-
// finalizeRoundToIdle 不消费——tool 返回 {closed:true} 谎报,record 永久
|
|
2065
|
-
// running-resumable、5min idle timer 杀进程、期间还能继续收 message。
|
|
2066
|
-
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2067
|
-
} else {
|
|
2068
|
-
// [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
|
|
2069
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2070
|
-
}
|
|
2071
|
-
} else {
|
|
2072
|
-
// 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
|
|
2073
|
-
await this.finalizeRecord(record, result, "closed", closedReason);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
2592
|
/** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
|
|
2078
2593
|
* 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
|
|
2079
2594
|
private async consumeCloseAfterRound(
|
|
@@ -2086,13 +2601,15 @@ export class SubagentService {
|
|
|
2086
2601
|
}
|
|
2087
2602
|
|
|
2088
2603
|
/**
|
|
2089
|
-
* pi chat 域轮次的 detached 编排(
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2604
|
+
* pi chat 域轮次的 detached 编排([W3 协议形态]):轮次经协议 run(会话形态
|
|
2605
|
+
* chat{recordId, resume?})发往 pi-subagent-cli 引擎进程——
|
|
2606
|
+
* - 应答时点 = 首轮 agent_settled(W2 契约:idle 帧先于应答帧;outcome = 本轮
|
|
2607
|
+
* 内容非会话终态)→ 应答到达即本轮 settle(round+1 / 增量通知 / closeAfterRound
|
|
2608
|
+
* 消费,见 settleChatRoundFromResponse);
|
|
2609
|
+
* - 流式 delta / 轮次生命周期经 host/streamDelta + host/roundLifecycle 反向通道
|
|
2610
|
+
* 回流:首轮 runId 键(RunContext.stream / onRoundLifecycle)、续聊轮 recordId
|
|
2611
|
+
* 键(chatRoundRoutes 注册的 recordId 路由);
|
|
2612
|
+
* - recordId 路由在本方法注册(首轮 + 冷续轮替换式重注册),record 终态化路径注销。
|
|
2096
2613
|
* chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
|
|
2097
2614
|
* 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
|
|
2098
2615
|
*/
|
|
@@ -2100,99 +2617,356 @@ export class SubagentService {
|
|
|
2100
2617
|
record: ExecutionRecord,
|
|
2101
2618
|
opts: ExecuteOptions,
|
|
2102
2619
|
identity: ResolvedIdentity,
|
|
2103
|
-
ctx: SessionRunnerContext,
|
|
2104
2620
|
signal: AbortSignal | undefined,
|
|
2105
2621
|
priority: number,
|
|
2106
|
-
/**
|
|
2107
|
-
resume?:
|
|
2622
|
+
/** 冷续锚点(M2-B1 协议形态):run.params.chat.resume。undefined = 新 session。 */
|
|
2623
|
+
resume?: ResumeAnchor,
|
|
2108
2624
|
): void {
|
|
2109
2625
|
// 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
|
|
2110
2626
|
// 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
|
|
2111
|
-
const stream = createBackgroundStream(record.id, this.streamSink,
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2627
|
+
const stream = createBackgroundStream(record.id, this.streamSink, this.uiObservability.getMode(), process.env);
|
|
2628
|
+
|
|
2629
|
+
// 注册 recordId 键反向通道路由(替换式——冷续轮重注册):interact 续聊轮的
|
|
2630
|
+
// streamDelta / roundLifecycle 分发目标;首轮流式 delta 走 ctx.stream(runId 键),
|
|
2631
|
+
// 本路由的 onStreamDelta 承担中段守护刷新 + dispose 后的续轮 delta(幂等 no-op)。
|
|
2632
|
+
// 仅 chat 会话形态注册(一次性 run 无反向轮次面)。
|
|
2633
|
+
const prevUnregister = this.chatRoundRoutes.get(record.id);
|
|
2634
|
+
prevUnregister?.();
|
|
2635
|
+
const engine = this.resolveChatEnginePort();
|
|
2636
|
+
if (record.chatMode) {
|
|
2637
|
+
this.chatRoundRoutes.set(
|
|
2638
|
+
record.id,
|
|
2639
|
+
engine.registerChatRoundRoute?.(record.id, {
|
|
2640
|
+
onStreamDelta: (delta) => {
|
|
2641
|
+
refreshFromProtocolEvent(record.id);
|
|
2642
|
+
stream?.onDelta(delta);
|
|
2643
|
+
},
|
|
2644
|
+
onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
|
|
2645
|
+
}) ?? (() => {}),
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// [W4] 冷路径 resume 轮的在途记账:死亡纳管 record 被主 agent resume = 决策收敛
|
|
2650
|
+
// (清指引标记与看门狗,回归「该等」)。热路径轮(deliverChatMessage)不经此处,
|
|
2651
|
+
// 其「该等」由 hasLiveProcess 判据覆盖(进程活)——conversation 形态本就豁免监督域。
|
|
2652
|
+
this.roundSupervisor.noteRunStarted(record.id);
|
|
2653
|
+
void (async () => {
|
|
2654
|
+
try {
|
|
2655
|
+
await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
|
|
2656
|
+
} catch {
|
|
2657
|
+
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
2658
|
+
if (signal?.aborted) {
|
|
2659
|
+
await this.finalizeAborted(record);
|
|
2660
|
+
} else {
|
|
2661
|
+
await this.finalizeFailed(record, new Error("aborted"));
|
|
2662
|
+
}
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
try {
|
|
2666
|
+
// [F-2 首轮/冷续轮 arm 重接] 轮开跑(pool 槽已到手、run 协议帧即将派发)挂
|
|
2667
|
+
// **中段**无进展检测——被删的 inproc stdout-pump 是首轮唯一中段守护,协议化后
|
|
2668
|
+
// 引擎侧 spawn-runner 仅 turn 计数无墙钟,本 arm 是首轮 wedged 的唯一熔断
|
|
2669
|
+
// (LC-1 场景①:pi 无事件行输出)。refresh 源(对照 deliverChatMessage 热路径
|
|
2670
|
+
// 的 arm/refresh 形态,同一守护实例语义):① runId 键协议事件行(ctx.onEvent,
|
|
2671
|
+
// 含 text_delta——引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta
|
|
2672
|
+
// 两通道,刷新面与热路径 streamDelta 等效);② settled 相位交棒
|
|
2673
|
+
// (ctx.onRoundLifecycle → handleChatRoundPhase → noteRoundSettledFromProtocol,
|
|
2674
|
+
// 首轮 runId 键同样生效);idle 相位 disarmRoundFromProtocol 两段一并清。
|
|
2675
|
+
// arm 置于 acquire 之后:排队窗口不计入 no-progress 静默(窗语义 = 轮开跑后)。
|
|
2676
|
+
armMidRoundNoProgress(record.id, {
|
|
2677
|
+
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
2678
|
+
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
2679
|
+
});
|
|
2680
|
+
// 协议 run:chatMode = 会话形态(chat.recordId = 关联键;resume 存在 = 冷续;
|
|
2681
|
+
// 应答时点 = 首轮 agent_settled,本轮 settle / 守护交棒 / idle 定时器挂载由
|
|
2682
|
+
// handleChatRoundPhase + settleChatRoundFromResponse 承接);非 chatMode =
|
|
2683
|
+
// 一次性 run(协议面无 chat 键,终态语义对齐原 settleOneShotOutcome)。
|
|
2684
|
+
const { outcome } = await engine.run(
|
|
2685
|
+
// resume 锚点轮引擎侧覆盖 model 解析(taskSpec 装配单一来源见 taskSpecWithModel)。
|
|
2686
|
+
this.taskSpecWithModel(opts, record.model),
|
|
2687
|
+
{
|
|
2688
|
+
taskId: record.id,
|
|
2689
|
+
poolKey: PI_POOL_KEY,
|
|
2690
|
+
signal,
|
|
2691
|
+
...(stream !== undefined ? { stream } : {}),
|
|
2692
|
+
ctxModel: identity.resolved.model,
|
|
2693
|
+
// chat 会话形态参数(conversation 形态必传;冷续带锚点);一次性 run 不携带。
|
|
2694
|
+
...(record.chatMode
|
|
2695
|
+
? {
|
|
2696
|
+
chat: {
|
|
2697
|
+
recordId: record.id,
|
|
2698
|
+
...(resume !== undefined ? { resume } : {}),
|
|
2699
|
+
},
|
|
2700
|
+
// 首轮 runId 键生命周期帧:守护交棒 / idle 定时器挂载 / failed 分诊。
|
|
2701
|
+
onRoundLifecycle: (phase: HostRoundLifecycleParams) => this.handleChatRoundPhase(record, phase),
|
|
2702
|
+
// 中段守护刷新源①:首轮协议事件行(message_*/tool_*/turn_end——有效
|
|
2703
|
+
// 事件到达即刷新;chat 域不接 journal,事件仅作活性信号消费)。
|
|
2704
|
+
onEvent: () => refreshFromProtocolEvent(record.id),
|
|
2705
|
+
}
|
|
2706
|
+
: {}),
|
|
2707
|
+
},
|
|
2708
|
+
);
|
|
2709
|
+
if (outcome.sessionFile !== undefined) record.sessionFile = outcome.sessionFile;
|
|
2710
|
+
const result = this.outcomeToAgentResult(record, outcome);
|
|
2711
|
+
if (record.chatMode) {
|
|
2712
|
+
// 本轮 settle(应答 = 首轮 agent_settled):round+1 / 增量通知 / closeAfterRound 消费。
|
|
2713
|
+
this.settleChatRoundFromResponse(record, outcome);
|
|
2714
|
+
} else {
|
|
2715
|
+
// 一次性 run 终态收口(对齐原 settleOneShotOutcome:成功轮 SP-5 回退
|
|
2716
|
+
// running-resumable 等待 upgrade;失败/取消一次性销毁)。
|
|
2717
|
+
await this.settleOneShotOutcome(record, result, signal?.aborted === true);
|
|
2718
|
+
}
|
|
2123
2719
|
// background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
|
|
2124
2720
|
// 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
|
|
2125
2721
|
// 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
|
|
2126
2722
|
// 表达)——迟到的完成回注不注入(可能已切换的)新 session。
|
|
2127
2723
|
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2128
|
-
this.
|
|
2724
|
+
this.collectCoordinator.route(record);
|
|
2725
|
+
}
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
// 轮次 run 失败(prepare 期 reject / 引擎进程死亡 / cancel 后未收敛合成终态):
|
|
2728
|
+
// chatMode MF-6——不销毁对话,回退可恢复(session 文件在盘,冷续 run 接续);
|
|
2729
|
+
// 非 chatMode 终态销毁(finalizeFailed)。cancel 抢先时 record 已终态化
|
|
2730
|
+
//(tryTransition 失败跳过),此处仅吞错。
|
|
2731
|
+
if (record.chatMode) {
|
|
2732
|
+
await this.finalizeChatSpawnFailure(record, err);
|
|
2733
|
+
} else {
|
|
2734
|
+
await this.finalizeFailed(record, err);
|
|
2129
2735
|
}
|
|
2130
|
-
})
|
|
2131
|
-
.catch((err: unknown) => {
|
|
2132
|
-
// detached 吞错:轮次执行内部已 finalize record(含 emitPendingUnregister),
|
|
2133
|
-
// 且 finalizeRecord 的 manifest 写入已降级为 best-effort(失败仅 logger.error + appendEntry,
|
|
2134
|
-
// 不外抛)。因此此处不应走到——但作为最后一道兼底,记录调试日志后吞下,不外抛。
|
|
2135
|
-
// 完成通知由 finalizeRecord 内的 emitPendingUnregister 承担(pending-notifications 消费)。
|
|
2136
|
-
// cancel 抢先时 status=cancelled,cancelBackground 自己 emit,此处无需重复。
|
|
2137
|
-
// 交接包未被消费(engine.run 前置 throw 的极端形态)时的泄漏兜底。
|
|
2138
|
-
this.chatRoundTickets.delete(record.id);
|
|
2139
2736
|
if (err instanceof Error) {
|
|
2140
|
-
logger.debug(`[subagent]
|
|
2737
|
+
logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
|
|
2141
2738
|
}
|
|
2142
|
-
}
|
|
2739
|
+
} finally {
|
|
2740
|
+
this.pool.release();
|
|
2741
|
+
// 在途 resume 守卫清除(幂等;冷续轮收尾)+ streaming widget 清除(轮终,幂等——
|
|
2742
|
+
// 续轮 delta 落已 dispose 的 stream 为 no-op,与 inproc 形态逐点一致)。
|
|
2743
|
+
this.resumesInFlight.delete(record.id);
|
|
2744
|
+
stream?.dispose();
|
|
2745
|
+
// [W4] 轮收口重评估(死亡纳管 record 的轮终 → 驱动可能又死 → 重新三态判定)。
|
|
2746
|
+
this.roundSupervisor.noteRunEnded(record.id);
|
|
2747
|
+
}
|
|
2748
|
+
})();
|
|
2143
2749
|
}
|
|
2144
2750
|
|
|
2145
|
-
/**
|
|
2146
|
-
*
|
|
2147
|
-
*
|
|
2148
|
-
|
|
2751
|
+
/**
|
|
2752
|
+
* [W3] chat 轮次生命周期帧消费(host/roundLifecycle,runId 键首轮 / recordId 键续聊轮
|
|
2753
|
+
* 经本方法收敛)。相位 → 宿主编排语义(W4 协议事件面三入口):
|
|
2754
|
+
* - settled:轮收敛——settled-watchdog 中段让位收尾段(noteRoundSettledFromProtocol);
|
|
2755
|
+
* - idle:轮收口进 idle 稳态——两段守护一并清(disarmRoundFromProtocol)+ 冷续锚点
|
|
2756
|
+
* 回填(sessionFile/engineHandle,idle 帧先于 run 应答帧)+ **idle 定时器挂载**
|
|
2757
|
+
* (引擎不实现 idle 定时器——core arm,W2 交接契约);
|
|
2758
|
+
* - failed:轮异常终止(engine_round_aborted / engine_round_crashed /
|
|
2759
|
+
* engine_round_epipe_exhausted)——record 如实标 failed(禁 completed 谎报),
|
|
2760
|
+
* chatMode 按 MF-6 回退可恢复(onChatRoundFailed)。
|
|
2761
|
+
*/
|
|
2762
|
+
private handleChatRoundPhase(record: ExecutionRecord, phase: HostRoundLifecycleParams): void {
|
|
2763
|
+
switch (phase.phase) {
|
|
2764
|
+
case "settled":
|
|
2765
|
+
noteRoundSettledFromProtocol(record.id);
|
|
2766
|
+
break;
|
|
2767
|
+
case "idle":
|
|
2768
|
+
disarmRoundFromProtocol(record.id);
|
|
2769
|
+
this.armChatIdleTimer(record);
|
|
2770
|
+
if (phase.anchor !== undefined) {
|
|
2771
|
+
this.backfillChatAnchor(record, phase.anchor);
|
|
2772
|
+
}
|
|
2773
|
+
break;
|
|
2774
|
+
case "failed":
|
|
2775
|
+
this.onChatRoundFailed(record, phase.error);
|
|
2776
|
+
break;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* [W3] idle 相位 = idle 定时器锚点(原 session-runner handleAgentSettledForChatMode
|
|
2782
|
+
* 的 armIdleTimer 段):挂载降级链(配置值 → DEFAULT 兜底 + warn)语义保持;超时
|
|
2783
|
+
* 处置 = 引擎侧进程回收(协议 interact close force)——idle timer SIGTERM 的协议形态。
|
|
2784
|
+
*/
|
|
2785
|
+
private armChatIdleTimer(record: ExecutionRecord): void {
|
|
2786
|
+
const onTimeout = (): void => {
|
|
2787
|
+
killRecordChildWithEscalation(record.id, "idle timer");
|
|
2788
|
+
this.terminateChatSession(record, "close", "idle timer");
|
|
2789
|
+
};
|
|
2790
|
+
try {
|
|
2791
|
+
armIdleTimer(record.id, onTimeout, record.idleTimeoutMs);
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
bestEffort(err, "armIdleTimer (chat idle phase)", "error");
|
|
2794
|
+
try {
|
|
2795
|
+
armIdleTimer(record.id, onTimeout, DEFAULT_IDLE_TIMEOUT_MS);
|
|
2796
|
+
logger.warn(
|
|
2797
|
+
`[subagents] idleTimeoutMs invalid for ${record.id}, fell back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms) — idle GC and round notification gate stay active`,
|
|
2798
|
+
);
|
|
2799
|
+
} catch (fallbackErr) {
|
|
2800
|
+
bestEffort(fallbackErr, "armIdleTimer fallback (chat idle phase)", "error");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
/** [W3] idle 帧锚点回填(冷续锚点 = 引擎侧会话滚动/compaction 后的最新定位)。 */
|
|
2806
|
+
private backfillChatAnchor(
|
|
2807
|
+
record: ExecutionRecord,
|
|
2808
|
+
anchor: { sessionRef: Record<string, string>; poolKey: string; journalPath?: string },
|
|
2809
|
+
): void {
|
|
2810
|
+
const sessionFile = anchor.sessionRef["sessionFile"];
|
|
2811
|
+
if (typeof sessionFile === "string" && sessionFile !== "") {
|
|
2812
|
+
record.sessionFile = sessionFile;
|
|
2813
|
+
}
|
|
2814
|
+
// engineHandle 回填(①级读取钥匙;幂等守卫——终态回填已落则不覆盖)。
|
|
2815
|
+
if (record.engineHandle === undefined || record.engineHandle.sessionRef["sessionId"] === undefined) {
|
|
2816
|
+
record.engineHandle = {
|
|
2817
|
+
sessionRef: { ...anchor.sessionRef },
|
|
2818
|
+
poolKey: anchor.poolKey,
|
|
2819
|
+
...(anchor.journalPath !== undefined ? { journalPath: anchor.journalPath } : {}),
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
this.store.reportRecordTransition(record);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* [W3] chat 轮 failed 相位分诊:错误如实落 record(失败相位错误码——engine_round_
|
|
2827
|
+
* aborted(宿主 cancel/close)/ engine_round_crashed(子进程外部死亡)/
|
|
2828
|
+
* engine_round_epipe_exhausted(EPIPE 兜底耗尽)),禁 completed 谎报。chatMode 按
|
|
2829
|
+
* MF-6 不销毁对话——回退可恢复(finalizeRoundToIdle + 通知),冷续 run 接续;
|
|
2830
|
+
* record 已终态(cancel/close 抢先)= 迟到帧,幂等跳过。
|
|
2831
|
+
*/
|
|
2832
|
+
private onChatRoundFailed(record: ExecutionRecord, error: ProtocolError): void {
|
|
2833
|
+
disarmRoundFromProtocol(record.id);
|
|
2834
|
+
if (record.status !== "running") return;
|
|
2835
|
+
const errMsg = `${error.code}: ${error.message}`;
|
|
2836
|
+
record.lastError = errMsg;
|
|
2837
|
+
const failedResult: AgentResult = {
|
|
2838
|
+
text: "",
|
|
2839
|
+
turns: record.turnCount,
|
|
2840
|
+
durationMs: Date.now() - record.startedAt,
|
|
2841
|
+
success: false,
|
|
2842
|
+
error: errMsg,
|
|
2843
|
+
sessionId: record.id,
|
|
2844
|
+
toolCalls: [],
|
|
2845
|
+
};
|
|
2846
|
+
void this.finalizeRoundToIdle(record, failedResult)
|
|
2847
|
+
.then(() => {
|
|
2848
|
+
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2849
|
+
this.collectCoordinator.route(record);
|
|
2850
|
+
}
|
|
2851
|
+
})
|
|
2852
|
+
.catch((err: unknown) => bestEffort(err, "chat round failed finalize", "error"));
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* [W3] 首轮 run 应答到达(= 首轮 agent_settled,W2 契约)的本轮 settle:round+1 /
|
|
2857
|
+
* 轮次增量通知 / base 推进 / reportRecordTransition / closeAfterRound 消费——语义
|
|
2858
|
+
* 权威自持于此(原 round-settlement.ts createRoundSettler 已删,本闭包为其唯一后继),
|
|
2859
|
+
* 唯一偏差 = 轮次文本源:协议形态下 live turns 留在引擎进程内,core 以应答
|
|
2860
|
+
* outcome.content(W2「outcome = 本轮内容」)为增量权威,record.turns 派生不可用。
|
|
2861
|
+
* idle 定时器挂载不在此处——idle 相位帧先于应答帧到达,armChatIdleTimer 已锚定。
|
|
2862
|
+
*/
|
|
2863
|
+
private settleChatRoundFromResponse(record: ExecutionRecord, outcome: AgentOutcome): void {
|
|
2864
|
+
if (record.status !== "running") return; // 终态(cancel/close 抢先)——迟到应答幂等跳过
|
|
2865
|
+
record.round = (record.round ?? 0) + 1;
|
|
2866
|
+
const roundText = outcome.content;
|
|
2867
|
+
record.result = roundText ||
|
|
2868
|
+
(record.lastError ? `round did not complete: ${record.lastError}` : "(no output this round)");
|
|
2869
|
+
// 先送达本轮增量(notify),再推进 base
|
|
2870
|
+
//(notify 后推进:notify 失败时 base 不推进,增量并入下一轮防丢文本)。
|
|
2871
|
+
// [T4①/PS-2] 通知门与 kickOff 回注同款:parent-new/parent-fork 编排性关闭后
|
|
2872
|
+
// 迟到的应答 settle 不注入(可能已切换的)新 session;cancelled 由 cancelBackground
|
|
2873
|
+
// 自行 notify。
|
|
2874
|
+
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2875
|
+
this.collectCoordinator.route(record);
|
|
2876
|
+
}
|
|
2877
|
+
record.roundBaseTurnIndex = record.turnCount;
|
|
2878
|
+
this.store.reportRecordTransition(record);
|
|
2879
|
+
// [M5] closeAfterRound 消费:chatMode 轮次完成的统一汇聚点(优雅关闭在本轮
|
|
2880
|
+
// 完成时兑现终态化)。
|
|
2881
|
+
if (record.closeAfterRound) {
|
|
2882
|
+
record.closeAfterRound = undefined;
|
|
2883
|
+
void this.closeAfterRoundSettled(record);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
/** [W3] chat 域引擎终止意图(协议面):cancel = interact cancel(引擎侧 SIGTERM →
|
|
2888
|
+
* settle 等待 → 杀链升级);close = interact close force(立即杀链收割)。fire-and-forget
|
|
2889
|
+
* ——失败 best-effort 留证(宿主组级收割兜底 = EngineClient killAll,dispose 路径)。 */
|
|
2890
|
+
private terminateChatSession(
|
|
2891
|
+
record: ExecutionRecord,
|
|
2892
|
+
kind: "cancel" | "close",
|
|
2893
|
+
source: string,
|
|
2894
|
+
): void {
|
|
2895
|
+
void (async () => {
|
|
2896
|
+
const engine = this.resolveChatEnginePort();
|
|
2897
|
+
const handle = this.chatHandleFor(record);
|
|
2898
|
+
const result = kind === "cancel"
|
|
2899
|
+
? await engine.interact(handle, { kind: "cancel" })
|
|
2900
|
+
: await engine.interact(handle, { kind: "close", payload: { force: true } });
|
|
2901
|
+
if (!result.ok) {
|
|
2902
|
+
logger.debug(
|
|
2903
|
+
`[subagents] terminateChatSession (${kind}, ${source}) rejected for ${record.id}: ${result.message}`,
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
})().catch((err: unknown) =>
|
|
2907
|
+
bestEffort(err, `terminateChatSession (${kind}, ${source})`, "debug"),
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
/** chat record 的协议 interact handle(engineHandle 缺省时按 recordId 合成——
|
|
2912
|
+
* 引擎侧 interact 定位键 = sessionRef.recordId;sessionFile 有值时一并携带)。 */
|
|
2913
|
+
private chatHandleFor(record: ExecutionRecord): EngineHandle {
|
|
2914
|
+
const known = record.engineHandle;
|
|
2915
|
+
return {
|
|
2916
|
+
data: {
|
|
2917
|
+
v: 1,
|
|
2918
|
+
engineId: record.engine ?? DEFAULT_ENGINE_ID,
|
|
2919
|
+
sessionRef: {
|
|
2920
|
+
recordId: record.id,
|
|
2921
|
+
...(known?.sessionRef["sessionFile"] !== undefined
|
|
2922
|
+
? { sessionFile: known.sessionRef["sessionFile"] }
|
|
2923
|
+
: record.sessionFile !== undefined
|
|
2924
|
+
? { sessionFile: record.sessionFile }
|
|
2925
|
+
: {}),
|
|
2926
|
+
},
|
|
2927
|
+
poolKey: known?.poolKey ?? PI_POOL_KEY,
|
|
2928
|
+
...(known?.journalPath !== undefined ? { journalPath: known.journalPath } : {}),
|
|
2929
|
+
adapterVersion: "subagent-core/host-bridge",
|
|
2930
|
+
},
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
/** [W3] pi 引擎 port 解析(chat 域路由与终止面共用):registry cli 形态 port,
|
|
2935
|
+
* 未注册 = 不可用 stub(engine_not_found,见 pi-host-binding)。 */
|
|
2936
|
+
private resolveChatEnginePort(): EnginePort {
|
|
2937
|
+
return resolveHostPiEnginePort(() => null);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
/** [W3] record 终态化路径的 chat 反向通道路由注销(幂等)。 */
|
|
2941
|
+
private unregisterChatRoundRoute(recordId: string): void {
|
|
2942
|
+
const unregister = this.chatRoundRoutes.get(recordId);
|
|
2943
|
+
if (unregister !== undefined) {
|
|
2944
|
+
this.chatRoundRoutes.delete(recordId);
|
|
2945
|
+
unregister();
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/** 引擎服务面适配器(HostBridgeServiceFace 结构视图):闭包持有本实例的编排面。
|
|
2950
|
+
* [W3] chat 域轮次交接可选面(takeChatRound/runChatRound/resumeChatRound)随
|
|
2951
|
+
* inproc PiEngine 删除移除——引擎经协议 converse,不再回调宿主编排面。 */
|
|
2952
|
+
private piEngineServiceAdapter(): HostBridgeServiceFace {
|
|
2149
2953
|
return {
|
|
2150
2954
|
executeAndAwait: (opts, signal, onEvent, stream) => this.executeAndAwait(opts, signal, onEvent, stream),
|
|
2151
2955
|
getRecordForAction: (id) => this.getRecordForAction(id),
|
|
2152
2956
|
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
2153
2957
|
cancel: (id) => this.cancel(id),
|
|
2154
2958
|
collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
|
|
2155
|
-
takeChatRound: (taskId) => this.takeChatTicket(taskId),
|
|
2156
|
-
runChatRound: (ticket) => this.runTicketRound(ticket),
|
|
2157
|
-
resumeChatRound: (record, text) => this.resumeColdRound(record, text),
|
|
2158
2959
|
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
2159
2960
|
};
|
|
2160
2961
|
}
|
|
2161
2962
|
|
|
2162
|
-
/** [D4 聚合连带]
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2165
|
-
|
|
2166
|
-
get asEngineService(): PiEngineService {
|
|
2963
|
+
/** [D4 聚合连带] 引擎服务面的显式结构视图(SAR 构造 resolveHostPiEnginePort 时
|
|
2964
|
+
* 传入的 getService 兼容位消费——W3 后该面无引擎侧消费方,保留使 SAR 调用点
|
|
2965
|
+
* 零改动,W8 收口时随签名一并清理)。getter 形态:face 视图(惰性构造)。 */
|
|
2966
|
+
get asEngineService(): HostBridgeServiceFace {
|
|
2167
2967
|
return this.piEngineServiceAdapter();
|
|
2168
2968
|
}
|
|
2169
2969
|
|
|
2170
|
-
/** chat 域轮次交接的消费侧(PiEngine.run 回调):一次性取走,防重复消费。私有名与
|
|
2171
|
-
* PiEngineService 可选面成员不同名(历史约束:D4 聚合前的结构化直绑时代,私有同名
|
|
2172
|
-
* 成员会阻断 SubagentService 直绑;聚合后经 asEngineService 显式视图,约束已消失,
|
|
2173
|
-
* 不同名保留为与 adapter 显式映射一致的命名纪律)。 */
|
|
2174
|
-
private takeChatTicket(taskId: string): ChatRoundTicket | undefined {
|
|
2175
|
-
const ticket = this.chatRoundTickets.get(taskId);
|
|
2176
|
-
if (ticket !== undefined) this.chatRoundTickets.delete(taskId);
|
|
2177
|
-
return ticket;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
/** PiEngine chat 分支的执行回调:原 runAndFinalize 直调链的 EnginePort 化落点
|
|
2181
|
-
* (编排归 Service——pool 槽 + runSpawn + 终态迁移,行为零变化)。 */
|
|
2182
|
-
private async runTicketRound(ticket: ChatRoundTicket): Promise<AgentResult> {
|
|
2183
|
-
return this.runAndFinalize(
|
|
2184
|
-
ticket.record,
|
|
2185
|
-
ticket.opts,
|
|
2186
|
-
ticket.ctx,
|
|
2187
|
-
ticket.identity,
|
|
2188
|
-
ticket.signal,
|
|
2189
|
-
ticket.priority,
|
|
2190
|
-
undefined,
|
|
2191
|
-
ticket.stream,
|
|
2192
|
-
ticket.resume,
|
|
2193
|
-
);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
2970
|
/**
|
|
2197
2971
|
* 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
|
|
2198
2972
|
* 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
|
|
@@ -2215,9 +2989,17 @@ export class SubagentService {
|
|
|
2215
2989
|
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
|
|
2216
2990
|
// 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
|
|
2217
2991
|
// 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
|
|
2992
|
+
// [W3] chatMode 的实际终止经协议 interact cancel(引擎进程内 SIGTERM → settle
|
|
2993
|
+
// 等待 → 杀链升级;首轮在途 run 的 abort 帧由 controller.signal 经 RemoteEngine
|
|
2994
|
+
// cancel 分级另行承载,两路幂等)。
|
|
2218
2995
|
killRecordChildWithEscalation(record.id, "cancelBackground");
|
|
2219
2996
|
disarmIdleTimer(record.id);
|
|
2220
2997
|
disarmSettledWatchdog(record.id);
|
|
2998
|
+
disarmRoundFromProtocol(record.id);
|
|
2999
|
+
this.unregisterChatRoundRoute(record.id);
|
|
3000
|
+
if (record.chatMode) {
|
|
3001
|
+
this.terminateChatSession(record, "cancel", "cancelBackground");
|
|
3002
|
+
}
|
|
2221
3003
|
// [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
|
|
2222
3004
|
// record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
|
|
2223
3005
|
// cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
|
|
@@ -2258,7 +3040,7 @@ export class SubagentService {
|
|
|
2258
3040
|
// pending-notifications:cancel 注销(只记 registry 状态)
|
|
2259
3041
|
this.notifyHost.emitPendingUnregister(record.id, "closed");
|
|
2260
3042
|
// cancel 完成通知(与轮次收尾 .then 对称——cancel 抢先时 .then 跳过 notify)
|
|
2261
|
-
this.
|
|
3043
|
+
this.collectCoordinator.route(record);
|
|
2262
3044
|
return true;
|
|
2263
3045
|
}
|
|
2264
3046
|
|
|
@@ -2279,6 +3061,10 @@ export class SubagentService {
|
|
|
2279
3061
|
modelService: this.modelService,
|
|
2280
3062
|
pi: this.pi,
|
|
2281
3063
|
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
3064
|
+
// [F-5 修复] chat record 终态化的路由注销(单一汇聚点钩子,见 FinalizeDeps
|
|
3065
|
+
// .onFinalized)——chatRoundRoutes 仅 chatMode 注册且注销幂等,非 chat record
|
|
3066
|
+
// 经此为 no-op。
|
|
3067
|
+
onFinalized: (id) => this.unregisterChatRoundRoute(id),
|
|
2282
3068
|
sessionDir: this.sessionsDir,
|
|
2283
3069
|
},
|
|
2284
3070
|
record,
|
|
@@ -2312,7 +3098,13 @@ export class SubagentService {
|
|
|
2312
3098
|
|
|
2313
3099
|
/** run() 创建期异常的收尾(H1 修复):createAndConfigureSession 失败会抛,本方法合成 failed
|
|
2314
3100
|
* AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
|
|
2315
|
-
* 继续返回(不 re-throw,swallow 策略)。
|
|
3101
|
+
* 继续返回(不 re-throw,swallow 策略)。
|
|
3102
|
+
* [W3 契约变更⑤(run 期失败清理前置副作用)] kickOffEngineRun 前已建的 worktree
|
|
3103
|
+
* (executeViaEngine 创建点,record.worktreeHandle 已绑定)经本方法 → finalizeRecord
|
|
3104
|
+
* → doFinalizeRecord Step 3b cleanupWorktreeIfBound 清理(manifest 多声明 run 期
|
|
3105
|
+
* 失败 / engine.run prepare 期 reject 共用本收尾链);CAS 没抢锁(cancel 抢先终态)
|
|
3106
|
+
* 时由 cancelBackground 的 worktree cleanup 覆盖。唯一前置副作用 = worktree(并发
|
|
3107
|
+
* 池槽 acquire/release 在 kickOffEngineRun finally 内自回收,journal 是宿主数据不清理)。 */
|
|
2316
3108
|
private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
2317
3109
|
const errMsg = toErrorMessage(err);
|
|
2318
3110
|
// durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
|
|
@@ -2387,43 +3179,6 @@ export class SubagentService {
|
|
|
2387
3179
|
);
|
|
2388
3180
|
}
|
|
2389
3181
|
}
|
|
2390
|
-
|
|
2391
|
-
/** 构造 SessionRunnerContext(spawn 模式:无需 SDK 实例)。 */
|
|
2392
|
-
private buildSessionRunnerContext(overrideCwd?: string): SessionRunnerContext {
|
|
2393
|
-
return {
|
|
2394
|
-
cwd: overrideCwd ?? this.cwd,
|
|
2395
|
-
agentDir: this.modelService.getAgentDir(),
|
|
2396
|
-
// ADR-031 废弃 discovery.json 后,skillDirs 为空。子 session 的 --skill
|
|
2397
|
-
// 由 agent({skill}) 调用方显式传入(resolveSkillPath → opts.skillPath)。
|
|
2398
|
-
skillDirs: [],
|
|
2399
|
-
mainCwd: this.cwd,
|
|
2400
|
-
// mainSessionFile: fork source 解析用,从 session_start 缓存获取。
|
|
2401
|
-
mainSessionFile: this.getMainSessionFile?.() ?? undefined,
|
|
2402
|
-
// worktree pid 回调:session-runner first header 时补全注册表 pid。
|
|
2403
|
-
onWorktreePid: (branch: string, pid: number, sessionFile?: string) => this.worktreeManager.registerPid(branch, pid, sessionFile),
|
|
2404
|
-
uiRequestHandler: this.uiRequestHandler,
|
|
2405
|
-
// SR-4:L2 dialog 队列透传——child close 时 session-runner 据此调 rejectChildDialogs
|
|
2406
|
-
// 清理 L2 pending dialog,防全局死锁。undefined 时 session-runner 跳过 L2 清理。
|
|
2407
|
-
dialogQueue: this.dialogQueue,
|
|
2408
|
-
// 主进程运行模式:session-runner W4 守卫据此决定是否注入 ask_user RPC 提示词。
|
|
2409
|
-
mode: this.uiObservability.getMode(),
|
|
2410
|
-
// [递归可见性] 透传所属根 session(runSpawn 注入为子进程 env PI_SUBAGENT_ROOT_SESSION_ID)。
|
|
2411
|
-
// sessionRootId 在 initSession 设定(根进程=sessionId,子进程=env 贯穿的真 ROOT)。
|
|
2412
|
-
// execute/executeAndAwait 调本方法前必经 initSession,此时 sessionRootId 已非空;
|
|
2413
|
-
// ?? 兑底防类型漂移(运行时不可达)。
|
|
2414
|
-
sessionRootId: this.sessionRootId ?? this.sessionId ?? "",
|
|
2415
|
-
// [MF-3] 透传 ROOT cwd(runSpawn 落盘目录编码键 + 注入子进程 env PI_SUBAGENT_ROOT_CWD)。
|
|
2416
|
-
// worktree 模式下 mainCwd = 本进程 checkout 路径,rootCwd 才是真 ROOT——session 文件
|
|
2417
|
-
// 落盘统一用 rootCwd 编码,ROOT 磁盘重建才扫得到深层 record(与 sessionRootId 同构)。
|
|
2418
|
-
rootCwd: this.rootCwd,
|
|
2419
|
-
// [V2 决策 2] chatMode 首轮闭环:agent_settled 时 session-runner 调本回调。
|
|
2420
|
-
// [D4-②] 业务闭包已拆至 round-settlement.ts(this.settleRound 字段工厂构造,
|
|
2421
|
-
// deps 回调注入 notifyComplete / reportRecordTransition / closeAfterRoundSettled,
|
|
2422
|
-
// 行为逐字节等价)——轻量 idle 化语义(round+=1 + notify 主 agent,不调
|
|
2423
|
-
// doFinalizeRoundToIdle)与增量派生/base 推进/哨兵的机制注释见该文件。
|
|
2424
|
-
onRoundSettled: this.settleRound,
|
|
2425
|
-
};
|
|
2426
|
-
}
|
|
2427
3182
|
}
|
|
2428
3183
|
|
|
2429
3184
|
// ── 进程单例访问器 ────────────────────────────────────
|