@zhushanwen/subagent-core 0.6.0 → 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-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -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 +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- 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__/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-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- 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 +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- 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__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -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__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -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 +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- 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/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- 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 +81 -54
- 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 +7 -2
- 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/finalize-record.ts +25 -0
- 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/record-store.ts +29 -2
- 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-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -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__/execute-agent-call.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/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 -166
- 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
|
|
|
@@ -18,11 +18,12 @@ import { removeAliveMarker } from "./alive-store.ts";
|
|
|
18
18
|
import { bestEffort } from "./best-effort.ts";
|
|
19
19
|
import { CollectCoordinator } from "./collect-coordinator.ts";
|
|
20
20
|
import { DEFAULT_COLLECT_SYNC } from "./config.ts";
|
|
21
|
-
// [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm
|
|
21
|
+
// [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程);
|
|
22
|
+
// idle 相位帧(协议 host/roundLifecycle)= arm 锚点(W3 v1.x——引擎不实现 idle 定时器)。
|
|
22
23
|
// [T4②] DEFAULT_IDLE_TIMEOUT_MS:assertIdleTimeoutMsSafe 错误文案的缺省时长基准。
|
|
23
24
|
// ([M3] hasIdleTimer / hasLiveProcessHandle 的 piAdapter.hasRunningBackground 过滤随通知簇
|
|
24
25
|
// 在 notify-host.ts 消费,不经本文件。)
|
|
25
|
-
import { disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
|
|
26
|
+
import { armIdleTimer, disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
|
|
26
27
|
import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
27
28
|
import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
|
|
28
29
|
import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "./cold-resurrect.ts";
|
|
@@ -38,14 +39,23 @@ import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
|
|
|
38
39
|
import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
|
|
39
40
|
import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
|
|
40
41
|
import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
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";
|
|
44
49
|
import type { EnginePort, RunContext } from "./engine/port.ts";
|
|
45
50
|
import { DEFAULT_ENGINE_ID, getEngine, listEngines } from "./engine/registry.ts";
|
|
46
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
joinEngineModelRef,
|
|
53
|
+
splitEngineModelRef,
|
|
54
|
+
validateModelForEngine,
|
|
55
|
+
withCrossEngineHint,
|
|
56
|
+
} from "./engine/model-validation.ts";
|
|
47
57
|
import { type EngineRouteResult, routeEngineForHost } from "./engine/routing.ts";
|
|
48
|
-
import type { AgentOutcome } from "./engine/types.ts";
|
|
58
|
+
import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
|
|
49
59
|
import { ManifestStore } from "./manifest-store.ts";
|
|
50
60
|
import type { ModelConfigService } from "./model-config-service.ts";
|
|
51
61
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
@@ -54,32 +64,47 @@ import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts
|
|
|
54
64
|
import type { BatchBudgetParams, BgNotifyRecord } from "./notifier.ts";
|
|
55
65
|
import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
|
|
56
66
|
import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
|
|
57
|
-
import { createRoundSettler } from "./round-settlement.ts";
|
|
58
67
|
import type { StatusFilter } from "./record-store.ts";
|
|
59
68
|
import { RecordStore } from "./record-store.ts";
|
|
60
69
|
// [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
|
|
61
70
|
//(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
|
|
62
71
|
import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
|
|
63
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。
|
|
64
76
|
import {
|
|
65
77
|
killAllSpawnedChildren,
|
|
66
78
|
killRecordChildWithEscalation,
|
|
67
79
|
registerSpawnedChildForRecord,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// [u-t2a T2②/T2③] settled watchdog:chatMode 轮 settled 等待固定硬上限(10min),
|
|
73
|
-
// 双挂载点之一在编排层热路径(deliverChatMessage,prompt 发出后),disarm 站点散布
|
|
74
|
-
// 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 尾段)。
|
|
75
84
|
import {
|
|
76
85
|
armMidRoundNoProgress,
|
|
86
|
+
disarmRoundFromProtocol,
|
|
77
87
|
disarmSettledWatchdog,
|
|
88
|
+
noteRoundSettledFromProtocol,
|
|
89
|
+
refreshFromProtocolEvent,
|
|
78
90
|
type SettledWatchdogFireInfo,
|
|
79
91
|
} from "./settled-watchdog.ts";
|
|
80
92
|
import { isIdle, isResumable } from "./lifecycle-predicates.ts";
|
|
81
93
|
import { startIdleGc } from "./idle-gc.ts";
|
|
82
|
-
|
|
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";
|
|
83
108
|
import type { StreamSink, SubagentStream } from "./stream-sink.ts";
|
|
84
109
|
import { createBackgroundStream } from "./stream-sink.ts";
|
|
85
110
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
@@ -142,13 +167,13 @@ export interface SubagentChatActions {
|
|
|
142
167
|
* 子进程彻底 close 前(pi 子进程 trap SIGTERM 做 graceful shutdown,窗口几十~几百 ms),
|
|
143
168
|
* 子进程的 trailing extension_ui_request 仍可能被父进程 pump 解析,调到 A 的 handler 闭包。
|
|
144
169
|
* 若 dispose 不清 uiRequestHandler,旧 handler 闭包仍持有 A 的 ctx,触发
|
|
145
|
-
*
|
|
170
|
+
* inproc UI 请求队列(已删) 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
|
|
146
171
|
* logger.error(看起来像 bug,实际是预期竞态;三层兜底已确保功能正确)。
|
|
147
172
|
*
|
|
148
173
|
* stub 始终返回 {cancelled:true},不调 ctx.ui、不捕获任何 ctx,让 trailing ui_request
|
|
149
174
|
* 干净降级为 cancelled(等价于子进程主动取消)。
|
|
150
175
|
*
|
|
151
|
-
* 不置 undefined —— 那会让 trailing ui_request 走
|
|
176
|
+
* 不置 undefined —— 那会让 trailing ui_request 走 inproc UI 请求队列(已删) 的 handler-missing
|
|
152
177
|
* 分支触发 notifyMissingHandlerGlobal warn,噪声性质从 threw-error 变 missing-handler,
|
|
153
178
|
* 没真正解决。 */
|
|
154
179
|
const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
|
|
@@ -370,22 +395,15 @@ export class SubagentService {
|
|
|
370
395
|
* 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
|
|
371
396
|
private readonly resumesInFlight = new Set<string>();
|
|
372
397
|
|
|
373
|
-
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
/** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
|
|
383
|
-
* 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
|
|
384
|
-
private readonly settleRound = createRoundSettler({
|
|
385
|
-
notifyComplete: (record) => this.collectCoordinator.route(record),
|
|
386
|
-
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
387
|
-
closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record),
|
|
388
|
-
});
|
|
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>();
|
|
389
407
|
|
|
390
408
|
/** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
|
|
391
409
|
* deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
|
|
@@ -399,6 +417,21 @@ export class SubagentService {
|
|
|
399
417
|
getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
|
|
400
418
|
};
|
|
401
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
|
+
|
|
402
435
|
/** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
|
|
403
436
|
* 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
|
|
404
437
|
readonly queries: SubagentQueries = {
|
|
@@ -437,6 +470,9 @@ export class SubagentService {
|
|
|
437
470
|
this.modelService = init.modelService;
|
|
438
471
|
this.getMainSessionFile = init.getMainSessionFile;
|
|
439
472
|
this.uiRequestHandler = init.uiRequestHandler;
|
|
473
|
+
// [W6 R3 MF-A] 壳侧应答端登记:cli 形态引擎经 host/askUser 反向请求消费(discovery
|
|
474
|
+
// portFactory 构造 EngineClient 时读取该登记)。
|
|
475
|
+
setHostUiRequestEndpoint(init.uiRequestHandler);
|
|
440
476
|
this.pool = new DefaultConcurrencyPool(this.modelService.getGlobalConfig().maxConcurrent);
|
|
441
477
|
this.worktreeManager = new WorktreeManager(this.modelService.getAgentDir());
|
|
442
478
|
// [MF-3] worktree 隔离下全树落盘目录统一到 ROOT cwd:子进程(spawn cwd = worktree checkout 路径)
|
|
@@ -526,7 +562,7 @@ export class SubagentService {
|
|
|
526
562
|
}
|
|
527
563
|
},
|
|
528
564
|
});
|
|
529
|
-
// #11:注册进程级 observability 单例——
|
|
565
|
+
// #11:注册进程级 observability 单例——inproc UI 请求队列 handleUiRequest(已删) 经
|
|
530
566
|
// globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
|
|
531
567
|
// warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
|
|
532
568
|
registerGlobalObservability(this.uiObservability);
|
|
@@ -553,6 +589,8 @@ export class SubagentService {
|
|
|
553
589
|
if (init.uiRequestHandler !== undefined) {
|
|
554
590
|
this.uiRequestHandler = init.uiRequestHandler ?? undefined;
|
|
555
591
|
this.uiObservability.resetMissingHandlerWarnings();
|
|
592
|
+
// [W6 R3 MF-A] session 级覆盖同步进壳侧应答端登记(三态:null = 显式清空)。
|
|
593
|
+
setHostUiRequestEndpoint(this.uiRequestHandler);
|
|
556
594
|
}
|
|
557
595
|
// SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
|
|
558
596
|
// 透传 undefined,session-runner onClose 跳过 L2 清理(仅清 L1,保留旧行为)。
|
|
@@ -581,6 +619,21 @@ export class SubagentService {
|
|
|
581
619
|
// 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
|
|
582
620
|
// sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
|
|
583
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
|
+
);
|
|
584
637
|
}
|
|
585
638
|
|
|
586
639
|
/**
|
|
@@ -730,6 +783,9 @@ export class SubagentService {
|
|
|
730
783
|
};
|
|
731
784
|
completeRecord(record, result, "closed", reason);
|
|
732
785
|
this.store.archive(record);
|
|
786
|
+
// [F-5 修复] 本路径不经 doFinalizeRecord(编排性关闭直连 completeRecord+archive),
|
|
787
|
+
// chat 轮路由注销在此补齐(幂等;闭包持 record/stream 引用,防泄漏)。
|
|
788
|
+
this.unregisterChatRoundRoute(record.id);
|
|
733
789
|
// worktree 绑定清理(T3)。cleanup 已 async 化——同步签名(返回计数)不变,
|
|
734
790
|
// 清理 fire-and-forget:失败经 bestEffort 留痕,不阻塞/不影响计数返回。
|
|
735
791
|
if (record.worktreeHandle) {
|
|
@@ -760,10 +816,16 @@ export class SubagentService {
|
|
|
760
816
|
/** SP-4: idle record GC(30 天 TTL,实现抽至 idle-gc.ts)。stop 函数(dispose 调)。 */
|
|
761
817
|
private stopIdleGc: (() => void) | undefined;
|
|
762
818
|
|
|
763
|
-
/** 启动 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 引入的装配错位)。 */
|
|
764
826
|
startGcTimer(): void {
|
|
765
827
|
if (this.stopIdleGc) return;
|
|
766
|
-
this.stopIdleGc = startIdleGc(this.store);
|
|
828
|
+
this.stopIdleGc = startIdleGc(this.store, new FileRunStore({ stateDir: resolvePiWorkflowStateDir() }));
|
|
767
829
|
}
|
|
768
830
|
|
|
769
831
|
/** 停止 idle record GC 定时器(dispose 调用)。 */
|
|
@@ -1050,10 +1112,17 @@ export class SubagentService {
|
|
|
1050
1112
|
// 此处内联其方法体(赋值 + 缺失告警去重重置)。
|
|
1051
1113
|
this.uiRequestHandler = disposedUiRequestStub;
|
|
1052
1114
|
this.uiObservability.resetMissingHandlerWarnings();
|
|
1115
|
+
// [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
|
|
1116
|
+
// cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
|
|
1117
|
+
setHostUiRequestEndpoint(disposedUiRequestStub);
|
|
1053
1118
|
// [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
|
|
1054
1119
|
// abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
|
|
1055
1120
|
this.store.abortRunningControllers();
|
|
1056
1121
|
killAllSpawnedChildren();
|
|
1122
|
+
// [W3] chat 反向通道路由全量注销(inproc 轮次交接包清空的协议形态替位)。
|
|
1123
|
+
for (const recordId of [...this.chatRoundRoutes.keys()]) {
|
|
1124
|
+
this.unregisterChatRoundRoute(recordId);
|
|
1125
|
+
}
|
|
1057
1126
|
// [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
|
|
1058
1127
|
// 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
|
|
1059
1128
|
// running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
|
|
@@ -1065,13 +1134,9 @@ export class SubagentService {
|
|
|
1065
1134
|
// SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
|
|
1066
1135
|
// 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
|
|
1067
1136
|
this.disposeAllRecords("parent-shutdown");
|
|
1068
|
-
// [v4 A-1] EPIPE 连续失败计数器清零(计数器已迁移到 stdin-writer,防跨 session 泄漏)
|
|
1069
|
-
resetAllEpipeFailures();
|
|
1070
1137
|
// [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
|
|
1071
1138
|
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
1072
1139
|
this.resumesInFlight.clear();
|
|
1073
|
-
// chat 轮次交接包清空(正常由 PiEngine.run 消费;此处兜底 kill 后仍挂着的条目)
|
|
1074
|
-
this.chatRoundTickets.clear();
|
|
1075
1140
|
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
1076
1141
|
this.notifyHost.flushPendingNotifications();
|
|
1077
1142
|
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
@@ -1081,6 +1146,9 @@ export class SubagentService {
|
|
|
1081
1146
|
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
1082
1147
|
this.persistUndeliveredNotificationsForReplay();
|
|
1083
1148
|
this.notifyHost.dispose();
|
|
1149
|
+
// [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
|
|
1150
|
+
// 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
|
|
1151
|
+
this.roundSupervisor.dispose();
|
|
1084
1152
|
this.store.dispose();
|
|
1085
1153
|
}
|
|
1086
1154
|
|
|
@@ -1179,7 +1247,9 @@ export class SubagentService {
|
|
|
1179
1247
|
// 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
|
|
1180
1248
|
// 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
|
|
1181
1249
|
// 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
|
|
1182
|
-
// record。pi 请求路径同步短路(routed 非 Promise
|
|
1250
|
+
// record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
|
|
1251
|
+
// 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
|
|
1252
|
+
// 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
|
|
1183
1253
|
// [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
|
|
1184
1254
|
// engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
|
|
1185
1255
|
// 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
|
|
@@ -1195,7 +1265,9 @@ export class SubagentService {
|
|
|
1195
1265
|
taskModel: opts.model,
|
|
1196
1266
|
strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
|
|
1197
1267
|
probe: (engineId) => getEngine(engineId).probe(),
|
|
1198
|
-
|
|
1268
|
+
// [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
|
|
1269
|
+
// 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
|
|
1270
|
+
piEngine: this.resolveChatEnginePort(),
|
|
1199
1271
|
});
|
|
1200
1272
|
const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
|
|
1201
1273
|
return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
|
|
@@ -1255,42 +1327,74 @@ export class SubagentService {
|
|
|
1255
1327
|
// deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
|
|
1256
1328
|
|
|
1257
1329
|
/**
|
|
1258
|
-
* [V2 决策 3] chatMode 统一投递入口(message action 的 Service
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
* 热路径(进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle(F3/F4)。
|
|
1264
|
-
* 冷路径(进程死):冷路径续轮(resumeChatRound)重开 session + prompt(仅崩溃/
|
|
1265
|
-
* timeout kill/跨重启命中)。
|
|
1330
|
+
* [V2 决策 3 → W3 协议形态] chatMode 统一投递入口(message action 的 Service 面)
|
|
1331
|
+
* ——经协议 interact(message action)发往 pi-subagent-cli 引擎进程。协议知识
|
|
1332
|
+
* (stdin prompt 命令 + streamingBehavior 映射 + EPIPE 兜底)在引擎进程内
|
|
1333
|
+
* (chat-session.deliverMessage);编排层按结构化结果分流:
|
|
1266
1334
|
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
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 形态逐字节一致)。
|
|
1269
1340
|
*
|
|
1270
1341
|
* @param record 目标 record(chatMode,running 或 idle)
|
|
1271
1342
|
* @param text 消息正文
|
|
1272
|
-
* @param interrupt true=steer(抢占)/ false=followUp
|
|
1343
|
+
* @param interrupt true=steer(抢占)/ false=followUp(排队)
|
|
1273
1344
|
*/
|
|
1274
1345
|
private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
|
|
1275
1346
|
this.assertReady();
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
+
});
|
|
1282
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
|
+
}
|
|
1283
1383
|
throw new Error(result.message);
|
|
1284
1384
|
}
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
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 终态化即清。
|
|
1294
1398
|
armMidRoundNoProgress(record.id, {
|
|
1295
1399
|
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
1296
1400
|
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
@@ -1385,24 +1489,28 @@ export class SubagentService {
|
|
|
1385
1489
|
// W16 [D4]:冷路径续轮是类外状态写点(不走 register/archive),显式上报迁移。
|
|
1386
1490
|
this.store.reportRecordTransition(record);
|
|
1387
1491
|
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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,
|
|
1393
1502
|
};
|
|
1394
1503
|
|
|
1395
|
-
// 重建 resolved
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
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);
|
|
1401
1509
|
const identity: ResolvedIdentity = {
|
|
1402
1510
|
agent: record.agent,
|
|
1403
1511
|
agentConfig: undefined,
|
|
1404
1512
|
resolved: {
|
|
1405
|
-
model: { id:
|
|
1513
|
+
model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
|
|
1406
1514
|
thinkingLevel: record.thinkingLevel,
|
|
1407
1515
|
},
|
|
1408
1516
|
};
|
|
@@ -1411,15 +1519,17 @@ export class SubagentService {
|
|
|
1411
1519
|
task: text,
|
|
1412
1520
|
slug: record.slug,
|
|
1413
1521
|
worktree: record.worktreeHandle,
|
|
1522
|
+
// 冷续轮同样是 chat 会话形态(run.params.chat 必传 + conversation gate 放行)。
|
|
1523
|
+
conversation: true,
|
|
1414
1524
|
};
|
|
1415
|
-
const ctx = this.buildSessionRunnerContext();
|
|
1416
1525
|
|
|
1417
|
-
// detached 编排:轮次在 background
|
|
1418
|
-
//
|
|
1526
|
+
// detached 编排:轮次在 background 跑(协议 run chat + resume),pool 重新
|
|
1527
|
+
// acquire(轮次间 idle 已 release)。chatMode 轮终 settle 分流自动
|
|
1528
|
+
// finalizeRoundToIdle(record 回 idle、round+1)。
|
|
1419
1529
|
// [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
|
|
1420
|
-
//
|
|
1530
|
+
// (冷路径重入)在守卫处被拒;轮次收尾 finally 统一清除。
|
|
1421
1531
|
this.resumesInFlight.add(record.id);
|
|
1422
|
-
this.kickOffChatRound(record, opts, identity,
|
|
1532
|
+
this.kickOffChatRound(record, opts, identity, record.controller.signal, PRIORITY_BACKGROUND, resume);
|
|
1423
1533
|
}
|
|
1424
1534
|
|
|
1425
1535
|
/**
|
|
@@ -1447,6 +1557,9 @@ export class SubagentService {
|
|
|
1447
1557
|
`terminating (LC-1 wedge recovery)`,
|
|
1448
1558
|
);
|
|
1449
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)");
|
|
1450
1563
|
const failedResult: AgentResult = {
|
|
1451
1564
|
text: "",
|
|
1452
1565
|
turns: record.turnCount,
|
|
@@ -1474,7 +1587,7 @@ export class SubagentService {
|
|
|
1474
1587
|
}
|
|
1475
1588
|
|
|
1476
1589
|
// [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
|
|
1477
|
-
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/
|
|
1590
|
+
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/inproc pi-engine(已删) 的
|
|
1478
1591
|
// rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
|
|
1479
1592
|
// 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
|
|
1480
1593
|
|
|
@@ -1598,13 +1711,16 @@ export class SubagentService {
|
|
|
1598
1711
|
*/
|
|
1599
1712
|
private async closeChatIdle(record: ExecutionRecord): Promise<void> {
|
|
1600
1713
|
// [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
|
|
1601
|
-
// [T2④ / LC-2]
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
//
|
|
1714
|
+
// [T2④ / LC-2] 终止语义收敛:SIGTERM 被无视时 30s 升级 SIGKILL——终态化后
|
|
1715
|
+
// record 已 archive,挂住 = 幽灵进程无任何后续回收通道。同时 disarm settled
|
|
1716
|
+
// watchdog(本路径终态化 = settled 等待窗口终结,防 watchdog 误杀后续同 id 资源)。
|
|
1717
|
+
// [W3] 实际终止在引擎进程内(协议 interact close force → 引擎侧杀链);
|
|
1718
|
+
// killRecordChildWithEscalation 只做镜像置死记账。
|
|
1605
1719
|
disarmIdleTimer(record.id);
|
|
1606
1720
|
disarmSettledWatchdog(record.id);
|
|
1721
|
+
disarmRoundFromProtocol(record.id);
|
|
1607
1722
|
killRecordChildWithEscalation(record.id, "closeChatIdle");
|
|
1723
|
+
this.terminateChatSession(record, "close", "closeChatIdle");
|
|
1608
1724
|
// 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
|
|
1609
1725
|
// `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
|
|
1610
1726
|
// completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
|
|
@@ -1626,6 +1742,8 @@ export class SubagentService {
|
|
|
1626
1742
|
modelService: this.modelService,
|
|
1627
1743
|
pi: this.pi,
|
|
1628
1744
|
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
1745
|
+
// [F-5 修复] 同 finalizeRecord——chat 轮路由注销单一汇聚点钩子。
|
|
1746
|
+
onFinalized: (id) => this.unregisterChatRoundRoute(id),
|
|
1629
1747
|
sessionDir: this.sessionsDir,
|
|
1630
1748
|
},
|
|
1631
1749
|
record,
|
|
@@ -1657,11 +1775,14 @@ export class SubagentService {
|
|
|
1657
1775
|
*/
|
|
1658
1776
|
private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
|
|
1659
1777
|
// 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
|
|
1660
|
-
// [T2④ / LC-2]
|
|
1661
|
-
//
|
|
1778
|
+
// [T2④ / LC-2] 终止语义收敛(30s 升级 SIGKILL,终态化后无后续回收通道);
|
|
1779
|
+
// settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
|
|
1780
|
+
// force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
|
|
1662
1781
|
disarmIdleTimer(record.id);
|
|
1663
1782
|
disarmSettledWatchdog(record.id);
|
|
1783
|
+
disarmRoundFromProtocol(record.id);
|
|
1664
1784
|
killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
|
|
1785
|
+
this.terminateChatSession(record, "close", "closeAfterRoundSettled");
|
|
1665
1786
|
if (!tryTransition(record, "closed", "user-close")) {
|
|
1666
1787
|
return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
|
|
1667
1788
|
}
|
|
@@ -1752,17 +1873,13 @@ export class SubagentService {
|
|
|
1752
1873
|
}
|
|
1753
1874
|
}
|
|
1754
1875
|
|
|
1755
|
-
// ── 步骤 3
|
|
1756
|
-
const ctx = this.buildSessionRunnerContext(opts.cwd);
|
|
1757
|
-
|
|
1758
|
-
// ── 步骤 4: signal 决议 ──
|
|
1876
|
+
// ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
|
|
1759
1877
|
const effectiveSignal = signal ?? record.controller?.signal;
|
|
1760
1878
|
|
|
1761
1879
|
// 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
|
|
1762
1880
|
const result = await this.runAndFinalize(
|
|
1763
1881
|
record,
|
|
1764
1882
|
{ ...opts, worktree: worktreeHandle },
|
|
1765
|
-
ctx,
|
|
1766
1883
|
identity,
|
|
1767
1884
|
effectiveSignal,
|
|
1768
1885
|
PRIORITY_BACKGROUND,
|
|
@@ -1911,19 +2028,21 @@ export class SubagentService {
|
|
|
1911
2028
|
opts: ExecuteOptions,
|
|
1912
2029
|
): ResolvedIdentity {
|
|
1913
2030
|
const canonical = validateModelForEngine(engine, engineModel);
|
|
1914
|
-
// record.model 留痕:canonical
|
|
1915
|
-
//
|
|
1916
|
-
//
|
|
2031
|
+
// record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
|
|
2032
|
+
// 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
|
|
2033
|
+
// 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
|
|
2034
|
+
// 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
|
|
2035
|
+
// 整串进 name,不再落 "<ref>/" 畸形)。
|
|
1917
2036
|
const modelStr = canonical ?? engineModel ?? "";
|
|
1918
|
-
const
|
|
2037
|
+
const model = splitEngineModelRef(modelStr);
|
|
1919
2038
|
return {
|
|
1920
2039
|
agent,
|
|
1921
2040
|
agentConfig,
|
|
1922
2041
|
resolved: {
|
|
1923
2042
|
model: {
|
|
1924
|
-
id:
|
|
1925
|
-
name:
|
|
1926
|
-
provider:
|
|
2043
|
+
id: model.id,
|
|
2044
|
+
name: model.name,
|
|
2045
|
+
provider: model.provider,
|
|
1927
2046
|
reasoning: false,
|
|
1928
2047
|
},
|
|
1929
2048
|
thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
@@ -1953,7 +2072,10 @@ export class SubagentService {
|
|
|
1953
2072
|
|
|
1954
2073
|
const record = createRecord(id, {
|
|
1955
2074
|
agent: identity.agent,
|
|
1956
|
-
model
|
|
2075
|
+
// model 留痕词形与拆分同源(joinEngineModelRef):provider 为空串只写 id——
|
|
2076
|
+
// 契约变更④的无斜杠 ref(provider=""/id=ref)不得落成 "/ref" 或 "ref/" 畸形;
|
|
2077
|
+
// 续聊回读侧 splitEngineModelRef 对无斜杠串还原 provider=""/id=ref,往返自洽。
|
|
2078
|
+
model: joinEngineModelRef(identity.resolved.model),
|
|
1957
2079
|
thinkingLevel: identity.resolved.thinkingLevel,
|
|
1958
2080
|
mode,
|
|
1959
2081
|
task: opts.task,
|
|
@@ -2007,6 +2129,12 @@ export class SubagentService {
|
|
|
2007
2129
|
// 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
|
|
2008
2130
|
// 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
|
|
2009
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 能力位段)。
|
|
2010
2138
|
assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
|
|
2011
2139
|
|
|
2012
2140
|
// identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
|
|
@@ -2077,13 +2205,13 @@ export class SubagentService {
|
|
|
2077
2205
|
}
|
|
2078
2206
|
|
|
2079
2207
|
if (isPiRoute) {
|
|
2080
|
-
// pi:
|
|
2081
|
-
|
|
2208
|
+
// pi:chat 轮次经协议 run(会话形态 chat{recordId})发往 pi-subagent-cli 引擎
|
|
2209
|
+
// 进程(W3——与 run 域 remote-engine 路由同构),编排收尾(notify/终态迁移)
|
|
2210
|
+
// 与旧 pi 主路径语义一致。
|
|
2082
2211
|
this.kickOffChatRound(
|
|
2083
2212
|
record,
|
|
2084
2213
|
{ ...recordOpts, worktree: worktreeHandle },
|
|
2085
2214
|
identity,
|
|
2086
|
-
this.buildSessionRunnerContext(opts.cwd),
|
|
2087
2215
|
record.controller!.signal,
|
|
2088
2216
|
PRIORITY_BACKGROUND,
|
|
2089
2217
|
);
|
|
@@ -2104,6 +2232,8 @@ export class SubagentService {
|
|
|
2104
2232
|
*/
|
|
2105
2233
|
private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
|
|
2106
2234
|
const signal = record.controller?.signal;
|
|
2235
|
+
// [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
|
|
2236
|
+
this.roundSupervisor.noteRunStarted(record.id);
|
|
2107
2237
|
void (async () => {
|
|
2108
2238
|
try {
|
|
2109
2239
|
await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
|
|
@@ -2120,13 +2250,17 @@ export class SubagentService {
|
|
|
2120
2250
|
// DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
|
|
2121
2251
|
// maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
|
|
2122
2252
|
try {
|
|
2123
|
-
|
|
2253
|
+
// [W4] adopted = 走了表 3 行 1 接管分支(record 保持 resumable 交监督器)——
|
|
2254
|
+
// 跳过 bg 完成回注(合并单条通知由监督器 sendMergedFailureNotice 承担,
|
|
2255
|
+
// route 会再发一条 toNotifyRecord 投影通知 = 双通知,正是 R3 要消除的时序窗口)。
|
|
2256
|
+
const adopted = await this.runEngineTask(record, opts, engine, signal);
|
|
2124
2257
|
// cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
|
|
2125
|
-
if (record.closedReason !== "cancelled") {
|
|
2258
|
+
if (!adopted && record.closedReason !== "cancelled") {
|
|
2126
2259
|
this.collectCoordinator.route(record);
|
|
2127
2260
|
}
|
|
2128
2261
|
} finally {
|
|
2129
2262
|
this.pool.release();
|
|
2263
|
+
this.roundSupervisor.noteRunEnded(record.id);
|
|
2130
2264
|
}
|
|
2131
2265
|
})();
|
|
2132
2266
|
}
|
|
@@ -2142,7 +2276,7 @@ export class SubagentService {
|
|
|
2142
2276
|
opts: ExecuteOptions,
|
|
2143
2277
|
engine: EnginePort,
|
|
2144
2278
|
signal: AbortSignal | undefined,
|
|
2145
|
-
): Promise<
|
|
2279
|
+
): Promise<boolean> {
|
|
2146
2280
|
// [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
|
|
2147
2281
|
//(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
|
|
2148
2282
|
// 不传 forwardEvents。
|
|
@@ -2193,15 +2327,49 @@ export class SubagentService {
|
|
|
2193
2327
|
journalPath: journal.path,
|
|
2194
2328
|
};
|
|
2195
2329
|
await journal.close();
|
|
2196
|
-
await this.finalizeEngineOutcome(record, outcome);
|
|
2330
|
+
return await this.finalizeEngineOutcome(record, outcome);
|
|
2197
2331
|
} catch (err) {
|
|
2198
2332
|
// engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
|
|
2199
2333
|
// journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
|
|
2200
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
|
+
}
|
|
2201
2349
|
await this.finalizeFailed(record, err);
|
|
2350
|
+
return false;
|
|
2202
2351
|
}
|
|
2203
2352
|
}
|
|
2204
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
|
+
|
|
2205
2373
|
/**
|
|
2206
2374
|
* 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
|
|
2207
2375
|
* 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
|
|
@@ -2213,43 +2381,50 @@ export class SubagentService {
|
|
|
2213
2381
|
/**
|
|
2214
2382
|
* engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
|
|
2215
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 完成回注)。
|
|
2216
2391
|
*/
|
|
2217
|
-
private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<
|
|
2392
|
+
private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
|
|
2218
2393
|
if (outcome.sessionFile !== undefined) {
|
|
2219
2394
|
record.sessionFile = outcome.sessionFile;
|
|
2220
2395
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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);
|
|
2231
2406
|
if (tryTransition(record, "closed", "gc")) {
|
|
2232
2407
|
await this.finalizeRecord(record, result, "closed", "gc");
|
|
2233
2408
|
}
|
|
2409
|
+
return false;
|
|
2234
2410
|
}
|
|
2235
2411
|
|
|
2236
|
-
// ── 执行内部:run + finalize(sync
|
|
2412
|
+
// ── 执行内部:run + finalize(workflow 域 sync 面;chat 域轮次走 kickOffChatRound)──
|
|
2237
2413
|
|
|
2238
|
-
/**
|
|
2414
|
+
/**
|
|
2415
|
+
* workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
|
|
2416
|
+
* runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
|
|
2417
|
+
* 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
|
|
2418
|
+
*/
|
|
2239
2419
|
private async runAndFinalize(
|
|
2240
2420
|
record: ExecutionRecord,
|
|
2241
2421
|
opts: ExecuteOptions,
|
|
2242
|
-
ctx: SessionRunnerContext,
|
|
2243
2422
|
identity: ResolvedIdentity,
|
|
2244
2423
|
signal: AbortSignal | undefined,
|
|
2245
2424
|
priority: number,
|
|
2246
|
-
|
|
2425
|
+
onEvent?: (event: AgentEvent) => void,
|
|
2247
2426
|
stream?: SubagentStream,
|
|
2248
|
-
/** resume 选项(M2-B1):透传 runSpawn,重开已 idle 的 session 续聊。undefined = 新 session。 */
|
|
2249
|
-
resume?: SpawnResumeOpts,
|
|
2250
2427
|
): Promise<AgentResult> {
|
|
2251
|
-
// [U04 阶段化提取] 主函数只留编排,按「装配(池槽/worktree/深度)→ 执行(ALS 包装
|
|
2252
|
-
// spawn)→ 回收(finally)→ 错误收口 → 终态收口」分段;行为逐字节不变。
|
|
2253
2428
|
const pooled = record.mode === "background";
|
|
2254
2429
|
let acquired = false;
|
|
2255
2430
|
if (pooled) {
|
|
@@ -2257,48 +2432,106 @@ export class SubagentService {
|
|
|
2257
2432
|
if (acquireFailure !== undefined) return acquireFailure;
|
|
2258
2433
|
acquired = true;
|
|
2259
2434
|
}
|
|
2260
|
-
// onEvent 直通(原此处曾有 onUpdate(project(record)) 节流回流包装——生产死路径,
|
|
2261
|
-
// 三调用点恒 onUpdate: undefined、仅测试触达,已按 swf-perf-impl ledger #22 删除
|
|
2262
|
-
// (决策 TC4/IF14,详见 .cw/swf-perf-impl/cleanup-slice-design.json;git 历史可完整恢复)。
|
|
2263
|
-
// ExecuteOptions.onUpdate 字段一并删除,未来误用将编译期失败而非静默无效。
|
|
2264
|
-
const onEvent = rawOnEvent;
|
|
2265
2435
|
|
|
2266
|
-
|
|
2267
|
-
|
|
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 });
|
|
2268
2442
|
|
|
2269
2443
|
let result: AgentResult;
|
|
2270
2444
|
try {
|
|
2271
|
-
|
|
2272
|
-
record,
|
|
2273
|
-
|
|
2274
|
-
|
|
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);
|
|
2275
2465
|
} catch (err) {
|
|
2276
|
-
|
|
2277
|
-
//
|
|
2278
|
-
// swallow(不 re-throw):sync 调用方拿到合成 failed result
|
|
2279
|
-
//
|
|
2466
|
+
await journal.close();
|
|
2467
|
+
// engine.run prepare 期 reject(进程创建前)→ 合成 failed result + 收尾。
|
|
2468
|
+
// swallow(不 re-throw):sync 调用方拿到合成 failed result,避免异常逃逸到
|
|
2469
|
+
// tool 层 + record 卡 running。
|
|
2280
2470
|
if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
|
|
2281
2471
|
return this.finalizeFailed(record, err);
|
|
2282
2472
|
} finally {
|
|
2283
2473
|
this.releaseRoundResources(record, pooled && acquired, stream);
|
|
2284
2474
|
}
|
|
2285
2475
|
|
|
2286
|
-
// [V2 决策 2/3] chatMode 首轮闭环:runSpawn 因 agent_settled 提前 resolve(onRoundSettled
|
|
2287
|
-
// 已设 record.status=idle + round+=1 + notify 主 agent)。进程仍保活(idle timer armed),
|
|
2288
|
-
// 不进下方 chatMode 分流(那是 close 后 done/failed/cancelled 终态化的,走 finalizeRoundToIdle
|
|
2289
|
-
// / finalizeRecord)。tryTransition(idle→done) 天然失败(要求 status==="running"),此处显式
|
|
2290
|
-
// early return 让语义清晰 + 防状态机未来改动。防 double-notify 由 notifier dedup 兜底
|
|
2291
|
-
//(同 id:round 60s 内吞,chat 轮次收尾 .then 的 notify 是 no-op,见 notifier.ts L122)。
|
|
2292
|
-
if (record.chatMode && isIdle(record)) {
|
|
2293
|
-
return result;
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
2476
|
// v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
|
|
2297
|
-
|
|
2298
|
-
await this.settleFinalOutcome(record, result, aborted, this.deriveClosedReason(aborted, result.success));
|
|
2477
|
+
await this.settleOneShotOutcome(record, result, signal?.aborted === true);
|
|
2299
2478
|
return result;
|
|
2300
2479
|
}
|
|
2301
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
|
+
|
|
2302
2535
|
/** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
|
|
2303
2536
|
* 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
|
|
2304
2537
|
* 不触发轮次资源回收——与原控制流逐字节一致)。 */
|
|
@@ -2322,69 +2555,16 @@ export class SubagentService {
|
|
|
2322
2555
|
return typeof opts.worktree === "object" ? opts.worktree : undefined;
|
|
2323
2556
|
}
|
|
2324
2557
|
|
|
2325
|
-
/** [U04 提取·装配] [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。 */
|
|
2326
|
-
private resolveForkDepths(opts: ExecuteOptions): { parentDepth: number; effectiveDepth: number } {
|
|
2327
|
-
const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
|
|
2328
|
-
const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
|
|
2329
|
-
return { parentDepth, effectiveDepth };
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
/** [U04 提取·执行] runSpawn 的两层 ALS 包装(forkDepthAls 外层 + execNesting 内层)。 */
|
|
2333
|
-
private runSpawnNested(
|
|
2334
|
-
record: ExecutionRecord,
|
|
2335
|
-
opts: ExecuteOptions,
|
|
2336
|
-
ctx: SessionRunnerContext,
|
|
2337
|
-
identity: ResolvedIdentity,
|
|
2338
|
-
signal: AbortSignal | undefined,
|
|
2339
|
-
onEvent: ((event: AgentEvent) => void) | undefined,
|
|
2340
|
-
stream: SubagentStream | undefined,
|
|
2341
|
-
resume: SpawnResumeOpts | undefined,
|
|
2342
|
-
worktreeHandle: WorktreeHandle | undefined,
|
|
2343
|
-
parentDepth: number,
|
|
2344
|
-
effectiveDepth: number,
|
|
2345
|
-
): Promise<AgentResult> {
|
|
2346
|
-
// 嵌套上下文包在 forkDepthAls 内层:B run() 期间挂 {recordId:B.id,depth:B.depth},
|
|
2347
|
-
// B 内创建 C 时 createRecordForMode 读到 B → C 挂到 B 名下。两层 ALS 独立但同生命周期。
|
|
2348
|
-
return this.forkDepthAls.run(effectiveDepth, () =>
|
|
2349
|
-
this.execNesting.run(
|
|
2350
|
-
{ recordId: record.id, depth: record.depth },
|
|
2351
|
-
() => runSpawn(record, opts.task, {
|
|
2352
|
-
resolved: identity.resolved,
|
|
2353
|
-
agentConfig: identity.agentConfig,
|
|
2354
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
2355
|
-
skillPath: opts.skillPath,
|
|
2356
|
-
schema: opts.schema,
|
|
2357
|
-
schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
|
|
2358
|
-
maxTurns: opts.maxTurns,
|
|
2359
|
-
graceTurns: opts.graceTurns,
|
|
2360
|
-
signal,
|
|
2361
|
-
onEvent,
|
|
2362
|
-
stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
|
|
2363
|
-
fork: opts.fork,
|
|
2364
|
-
// [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
|
|
2365
|
-
// opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
|
|
2366
|
-
forkSource: opts.forkFromSessionFile,
|
|
2367
|
-
worktree: worktreeHandle,
|
|
2368
|
-
parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
|
|
2369
|
-
}, ctx, resume),
|
|
2370
|
-
),
|
|
2371
|
-
);
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
2558
|
/** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
|
|
2375
|
-
* 成功)、streaming widget
|
|
2559
|
+
* 成功)、streaming widget 清除。 */
|
|
2376
2560
|
private releaseRoundResources(
|
|
2377
|
-
|
|
2561
|
+
_record: ExecutionRecord,
|
|
2378
2562
|
holdSlot: boolean,
|
|
2379
2563
|
stream: SubagentStream | undefined,
|
|
2380
2564
|
): void {
|
|
2381
2565
|
if (holdSlot) this.pool.release();
|
|
2382
2566
|
// 清除 streaming widget(subagent 终态,幂等)
|
|
2383
2567
|
stream?.dispose();
|
|
2384
|
-
// [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
|
|
2385
|
-
// MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
|
|
2386
|
-
// execute() 新建 record 不在集合,delete 是 no-op。
|
|
2387
|
-
this.resumesInFlight.delete(record.id);
|
|
2388
2568
|
}
|
|
2389
2569
|
|
|
2390
2570
|
/** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
|
|
@@ -2401,6 +2581,7 @@ export class SubagentService {
|
|
|
2401
2581
|
sessionId: record.id,
|
|
2402
2582
|
toolCalls: [],
|
|
2403
2583
|
};
|
|
2584
|
+
disarmRoundFromProtocol(record.id);
|
|
2404
2585
|
if (tryTransition(record, "closed", "gc")) {
|
|
2405
2586
|
// 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
|
|
2406
2587
|
await this.finalizeRoundToIdle(record, failedResult);
|
|
@@ -2408,81 +2589,6 @@ export class SubagentService {
|
|
|
2408
2589
|
return failedResult;
|
|
2409
2590
|
}
|
|
2410
2591
|
|
|
2411
|
-
/** [U04 提取·收口派生] closedReason 派生:aborted → cancelled;否则 success → user-close,!success → gc。 */
|
|
2412
|
-
private deriveClosedReason(aborted: boolean, success: boolean): ClosedReason {
|
|
2413
|
-
return aborted ? "cancelled" : success ? "user-close" : "gc";
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
/** [U04 提取·终态收口] CAS 抢锁:抢到则按 chatMode 分流完整收尾;没抢到(cancel 已先设
|
|
2417
|
-
* closed+cancelled)则跳过。 */
|
|
2418
|
-
private async settleFinalOutcome(
|
|
2419
|
-
record: ExecutionRecord,
|
|
2420
|
-
result: AgentResult,
|
|
2421
|
-
aborted: boolean,
|
|
2422
|
-
closedReason: ClosedReason,
|
|
2423
|
-
): Promise<void> {
|
|
2424
|
-
if (!tryTransition(record, "closed", closedReason)) return;
|
|
2425
|
-
if (record.chatMode) {
|
|
2426
|
-
await this.settleChatRoundOutcome(record, result, aborted, closedReason);
|
|
2427
|
-
} else {
|
|
2428
|
-
await this.settleOneShotOutcome(record, result, aborted, closedReason);
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
/** [U04 提取·终态收口] chatMode 轮次分流(原 A/B 分支,De Morgan 等价拆分):
|
|
2433
|
-
* 成功轮(!aborted && success)与失败/取消轮(其补集 !success || aborted)各自处理
|
|
2434
|
-
* closeAfterRound 挂起语义。 */
|
|
2435
|
-
private async settleChatRoundOutcome(
|
|
2436
|
-
record: ExecutionRecord,
|
|
2437
|
-
result: AgentResult,
|
|
2438
|
-
aborted: boolean,
|
|
2439
|
-
closedReason: ClosedReason,
|
|
2440
|
-
): Promise<void> {
|
|
2441
|
-
if (!aborted && result.success) {
|
|
2442
|
-
if (record.closeAfterRound) {
|
|
2443
|
-
// close 优雅关闭(force:false):当前轮完成后终态化为 closed。
|
|
2444
|
-
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2445
|
-
} else {
|
|
2446
|
-
// 对话模式轮次成功完成 → 保持 running(旧 idle 折入 running,finalizeRoundToIdle 设回 running)。
|
|
2447
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2448
|
-
}
|
|
2449
|
-
} else {
|
|
2450
|
-
// MF-6:chatMode 轮次失败/取消不销毁对话——回退 running-resumable(旧 idle,可恢复)。
|
|
2451
|
-
if (record.closeAfterRound) {
|
|
2452
|
-
// [M5] 优雅关闭挂起的失败/取消轮:轮已完成即兑现 close 意图终态化(含本轮 result),
|
|
2453
|
-
// 不再回退 resumable——否则标志残留到下一轮,record 已被 tool 谎报 closed。
|
|
2454
|
-
await this.consumeCloseAfterRound(record, result, closedReason);
|
|
2455
|
-
} else {
|
|
2456
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
/** [U04 提取·终态收口] one-shot(非 chatMode)分流(原 C/D 分支):成功轮消费
|
|
2462
|
-
* closeAfterRound 挂起标志;失败/取消一次性销毁。 */
|
|
2463
|
-
private async settleOneShotOutcome(
|
|
2464
|
-
record: ExecutionRecord,
|
|
2465
|
-
result: AgentResult,
|
|
2466
|
-
aborted: boolean,
|
|
2467
|
-
closedReason: ClosedReason,
|
|
2468
|
-
): Promise<void> {
|
|
2469
|
-
if (!aborted && result.success) {
|
|
2470
|
-
if (record.closeAfterRound) {
|
|
2471
|
-
// [M5] 非 chatMode(one-shot)busy 时 close(force:false) 置的标志在本轮完成时消费
|
|
2472
|
-
// 终态化(对齐 close schema 文案 "release its resources")。旧代码走
|
|
2473
|
-
// finalizeRoundToIdle 不消费——tool 返回 {closed:true} 谎报,record 永久
|
|
2474
|
-
// running-resumable、5min idle timer 杀进程、期间还能继续收 message。
|
|
2475
|
-
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2476
|
-
} else {
|
|
2477
|
-
// [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
|
|
2478
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2479
|
-
}
|
|
2480
|
-
} else {
|
|
2481
|
-
// 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
|
|
2482
|
-
await this.finalizeRecord(record, result, "closed", closedReason);
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
2592
|
/** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
|
|
2487
2593
|
* 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
|
|
2488
2594
|
private async consumeCloseAfterRound(
|
|
@@ -2495,13 +2601,15 @@ export class SubagentService {
|
|
|
2495
2601
|
}
|
|
2496
2602
|
|
|
2497
2603
|
/**
|
|
2498
|
-
* pi chat 域轮次的 detached 编排(
|
|
2499
|
-
*
|
|
2500
|
-
*
|
|
2501
|
-
*
|
|
2502
|
-
*
|
|
2503
|
-
*
|
|
2504
|
-
*
|
|
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 终态化路径注销。
|
|
2505
2613
|
* chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
|
|
2506
2614
|
* 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
|
|
2507
2615
|
*/
|
|
@@ -2509,26 +2617,105 @@ export class SubagentService {
|
|
|
2509
2617
|
record: ExecutionRecord,
|
|
2510
2618
|
opts: ExecuteOptions,
|
|
2511
2619
|
identity: ResolvedIdentity,
|
|
2512
|
-
ctx: SessionRunnerContext,
|
|
2513
2620
|
signal: AbortSignal | undefined,
|
|
2514
2621
|
priority: number,
|
|
2515
|
-
/**
|
|
2516
|
-
resume?:
|
|
2622
|
+
/** 冷续锚点(M2-B1 协议形态):run.params.chat.resume。undefined = 新 session。 */
|
|
2623
|
+
resume?: ResumeAnchor,
|
|
2517
2624
|
): void {
|
|
2518
2625
|
// 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
|
|
2519
2626
|
// 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
|
|
2520
|
-
const stream = createBackgroundStream(record.id, this.streamSink,
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
+
}
|
|
2532
2719
|
// background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
|
|
2533
2720
|
// 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
|
|
2534
2721
|
// 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
|
|
@@ -2536,72 +2723,250 @@ export class SubagentService {
|
|
|
2536
2723
|
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2537
2724
|
this.collectCoordinator.route(record);
|
|
2538
2725
|
}
|
|
2539
|
-
})
|
|
2540
|
-
|
|
2541
|
-
//
|
|
2542
|
-
//
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
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);
|
|
2735
|
+
}
|
|
2548
2736
|
if (err instanceof Error) {
|
|
2549
|
-
logger.debug(`[subagent]
|
|
2737
|
+
logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
|
|
2550
2738
|
}
|
|
2551
|
-
}
|
|
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
|
+
})();
|
|
2749
|
+
}
|
|
2750
|
+
|
|
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
|
+
}
|
|
2552
2803
|
}
|
|
2553
2804
|
|
|
2554
|
-
/**
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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 {
|
|
2558
2953
|
return {
|
|
2559
2954
|
executeAndAwait: (opts, signal, onEvent, stream) => this.executeAndAwait(opts, signal, onEvent, stream),
|
|
2560
2955
|
getRecordForAction: (id) => this.getRecordForAction(id),
|
|
2561
2956
|
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
2562
2957
|
cancel: (id) => this.cancel(id),
|
|
2563
2958
|
collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
|
|
2564
|
-
takeChatRound: (taskId) => this.takeChatTicket(taskId),
|
|
2565
|
-
runChatRound: (ticket) => this.runTicketRound(ticket),
|
|
2566
|
-
resumeChatRound: (record, text) => this.resumeColdRound(record, text),
|
|
2567
2959
|
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
2568
2960
|
};
|
|
2569
2961
|
}
|
|
2570
2962
|
|
|
2571
|
-
/** [D4 聚合连带]
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
|
|
2575
|
-
get asEngineService(): PiEngineService {
|
|
2963
|
+
/** [D4 聚合连带] 引擎服务面的显式结构视图(SAR 构造 resolveHostPiEnginePort 时
|
|
2964
|
+
* 传入的 getService 兼容位消费——W3 后该面无引擎侧消费方,保留使 SAR 调用点
|
|
2965
|
+
* 零改动,W8 收口时随签名一并清理)。getter 形态:face 视图(惰性构造)。 */
|
|
2966
|
+
get asEngineService(): HostBridgeServiceFace {
|
|
2576
2967
|
return this.piEngineServiceAdapter();
|
|
2577
2968
|
}
|
|
2578
2969
|
|
|
2579
|
-
/** chat 域轮次交接的消费侧(PiEngine.run 回调):一次性取走,防重复消费。私有名与
|
|
2580
|
-
* PiEngineService 可选面成员不同名(历史约束:D4 聚合前的结构化直绑时代,私有同名
|
|
2581
|
-
* 成员会阻断 SubagentService 直绑;聚合后经 asEngineService 显式视图,约束已消失,
|
|
2582
|
-
* 不同名保留为与 adapter 显式映射一致的命名纪律)。 */
|
|
2583
|
-
private takeChatTicket(taskId: string): ChatRoundTicket | undefined {
|
|
2584
|
-
const ticket = this.chatRoundTickets.get(taskId);
|
|
2585
|
-
if (ticket !== undefined) this.chatRoundTickets.delete(taskId);
|
|
2586
|
-
return ticket;
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
/** PiEngine chat 分支的执行回调:原 runAndFinalize 直调链的 EnginePort 化落点
|
|
2590
|
-
* (编排归 Service——pool 槽 + runSpawn + 终态迁移,行为零变化)。 */
|
|
2591
|
-
private async runTicketRound(ticket: ChatRoundTicket): Promise<AgentResult> {
|
|
2592
|
-
return this.runAndFinalize(
|
|
2593
|
-
ticket.record,
|
|
2594
|
-
ticket.opts,
|
|
2595
|
-
ticket.ctx,
|
|
2596
|
-
ticket.identity,
|
|
2597
|
-
ticket.signal,
|
|
2598
|
-
ticket.priority,
|
|
2599
|
-
undefined,
|
|
2600
|
-
ticket.stream,
|
|
2601
|
-
ticket.resume,
|
|
2602
|
-
);
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
2970
|
/**
|
|
2606
2971
|
* 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
|
|
2607
2972
|
* 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
|
|
@@ -2624,9 +2989,17 @@ export class SubagentService {
|
|
|
2624
2989
|
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
|
|
2625
2990
|
// 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
|
|
2626
2991
|
// 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
|
|
2992
|
+
// [W3] chatMode 的实际终止经协议 interact cancel(引擎进程内 SIGTERM → settle
|
|
2993
|
+
// 等待 → 杀链升级;首轮在途 run 的 abort 帧由 controller.signal 经 RemoteEngine
|
|
2994
|
+
// cancel 分级另行承载,两路幂等)。
|
|
2627
2995
|
killRecordChildWithEscalation(record.id, "cancelBackground");
|
|
2628
2996
|
disarmIdleTimer(record.id);
|
|
2629
2997
|
disarmSettledWatchdog(record.id);
|
|
2998
|
+
disarmRoundFromProtocol(record.id);
|
|
2999
|
+
this.unregisterChatRoundRoute(record.id);
|
|
3000
|
+
if (record.chatMode) {
|
|
3001
|
+
this.terminateChatSession(record, "cancel", "cancelBackground");
|
|
3002
|
+
}
|
|
2630
3003
|
// [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
|
|
2631
3004
|
// record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
|
|
2632
3005
|
// cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
|
|
@@ -2688,6 +3061,10 @@ export class SubagentService {
|
|
|
2688
3061
|
modelService: this.modelService,
|
|
2689
3062
|
pi: this.pi,
|
|
2690
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),
|
|
2691
3068
|
sessionDir: this.sessionsDir,
|
|
2692
3069
|
},
|
|
2693
3070
|
record,
|
|
@@ -2721,7 +3098,13 @@ export class SubagentService {
|
|
|
2721
3098
|
|
|
2722
3099
|
/** run() 创建期异常的收尾(H1 修复):createAndConfigureSession 失败会抛,本方法合成 failed
|
|
2723
3100
|
* AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
|
|
2724
|
-
* 继续返回(不 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 是宿主数据不清理)。 */
|
|
2725
3108
|
private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
2726
3109
|
const errMsg = toErrorMessage(err);
|
|
2727
3110
|
// durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
|
|
@@ -2796,43 +3179,6 @@ export class SubagentService {
|
|
|
2796
3179
|
);
|
|
2797
3180
|
}
|
|
2798
3181
|
}
|
|
2799
|
-
|
|
2800
|
-
/** 构造 SessionRunnerContext(spawn 模式:无需 SDK 实例)。 */
|
|
2801
|
-
private buildSessionRunnerContext(overrideCwd?: string): SessionRunnerContext {
|
|
2802
|
-
return {
|
|
2803
|
-
cwd: overrideCwd ?? this.cwd,
|
|
2804
|
-
agentDir: this.modelService.getAgentDir(),
|
|
2805
|
-
// ADR-031 废弃 discovery.json 后,skillDirs 为空。子 session 的 --skill
|
|
2806
|
-
// 由 agent({skill}) 调用方显式传入(resolveSkillPath → opts.skillPath)。
|
|
2807
|
-
skillDirs: [],
|
|
2808
|
-
mainCwd: this.cwd,
|
|
2809
|
-
// mainSessionFile: fork source 解析用,从 session_start 缓存获取。
|
|
2810
|
-
mainSessionFile: this.getMainSessionFile?.() ?? undefined,
|
|
2811
|
-
// worktree pid 回调:session-runner first header 时补全注册表 pid。
|
|
2812
|
-
onWorktreePid: (branch: string, pid: number, sessionFile?: string) => this.worktreeManager.registerPid(branch, pid, sessionFile),
|
|
2813
|
-
uiRequestHandler: this.uiRequestHandler,
|
|
2814
|
-
// SR-4:L2 dialog 队列透传——child close 时 session-runner 据此调 rejectChildDialogs
|
|
2815
|
-
// 清理 L2 pending dialog,防全局死锁。undefined 时 session-runner 跳过 L2 清理。
|
|
2816
|
-
dialogQueue: this.dialogQueue,
|
|
2817
|
-
// 主进程运行模式:session-runner W4 守卫据此决定是否注入 ask_user RPC 提示词。
|
|
2818
|
-
mode: this.uiObservability.getMode(),
|
|
2819
|
-
// [递归可见性] 透传所属根 session(runSpawn 注入为子进程 env PI_SUBAGENT_ROOT_SESSION_ID)。
|
|
2820
|
-
// sessionRootId 在 initSession 设定(根进程=sessionId,子进程=env 贯穿的真 ROOT)。
|
|
2821
|
-
// execute/executeAndAwait 调本方法前必经 initSession,此时 sessionRootId 已非空;
|
|
2822
|
-
// ?? 兑底防类型漂移(运行时不可达)。
|
|
2823
|
-
sessionRootId: this.sessionRootId ?? this.sessionId ?? "",
|
|
2824
|
-
// [MF-3] 透传 ROOT cwd(runSpawn 落盘目录编码键 + 注入子进程 env PI_SUBAGENT_ROOT_CWD)。
|
|
2825
|
-
// worktree 模式下 mainCwd = 本进程 checkout 路径,rootCwd 才是真 ROOT——session 文件
|
|
2826
|
-
// 落盘统一用 rootCwd 编码,ROOT 磁盘重建才扫得到深层 record(与 sessionRootId 同构)。
|
|
2827
|
-
rootCwd: this.rootCwd,
|
|
2828
|
-
// [V2 决策 2] chatMode 首轮闭环:agent_settled 时 session-runner 调本回调。
|
|
2829
|
-
// [D4-②] 业务闭包已拆至 round-settlement.ts(this.settleRound 字段工厂构造,
|
|
2830
|
-
// deps 回调注入 notifyComplete / reportRecordTransition / closeAfterRoundSettled,
|
|
2831
|
-
// 行为逐字节等价)——轻量 idle 化语义(round+=1 + notify 主 agent,不调
|
|
2832
|
-
// doFinalizeRoundToIdle)与增量派生/base 推进/哨兵的机制注释见该文件。
|
|
2833
|
-
onRoundSettled: this.settleRound,
|
|
2834
|
-
};
|
|
2835
|
-
}
|
|
2836
3182
|
}
|
|
2837
3183
|
|
|
2838
3184
|
// ── 进程单例访问器 ────────────────────────────────────
|