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