@zhushanwen/subagent-core 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +9799 -7954
- package/dist/index.d.cts +1896 -1360
- package/dist/index.d.ts +1896 -1360
- package/dist/index.js +8952 -7513
- package/dist.bundle/index.cjs +7897 -6054
- package/package.json +5 -5
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/record-store-last-line.test.ts +12 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/core/logger.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/nested-visibility.test.ts +13 -1
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +111 -26
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +26 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/host/spawned-children.ts +1 -1
- package/src/execution/engine/inflight-snapshot.ts +92 -0
- package/src/execution/engine/port.ts +52 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/path-encoding.ts +6 -0
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2955
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +12 -8
- package/src/index.ts +16 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
- package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- package/src/execution/tombstone-store.ts +0 -72
|
@@ -1,182 +1,134 @@
|
|
|
1
1
|
// 执行编排 + 记录领域 Service(D4 按变化轴拆分后的编排核:execute/executeAndAwait 入口、
|
|
2
|
-
// record 生命周期、cancel)。通知簇 → notify-host.ts
|
|
3
|
-
|
|
2
|
+
// record 生命周期、cancel)。通知簇 → notify-host.ts;轮末分流归 ConversationContinuation
|
|
3
|
+
//([H1 U2] §3.4);冷路径查询链 → cold-lookup.ts([H1 U6] 原 cold-resurrect.ts 改名落位)。
|
|
4
4
|
// 上游:subagent-tool(execute/query/cancel)、TUI(onChange/collectRecords)。
|
|
5
5
|
// session_start 时经 initSession 注入 pi;modelRegistry/entries 归 ModelConfigService.initModel。
|
|
6
6
|
|
|
7
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
-
|
|
9
7
|
import { getLogger } from "../core/logger.ts";
|
|
10
8
|
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
14
|
-
import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
|
|
15
|
-
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
16
|
-
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
17
|
-
import { removeAliveMarker } from "./alive-store.ts";
|
|
9
|
+
import type { AgentResult as WorkflowAgentResult, AgentCallOpts } from "../orchestration/models/types.ts";
|
|
18
10
|
import { bestEffort } from "./best-effort.ts";
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
// [V2 决策 3] lifecycle-manager idle timer:chatMode
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import { doFinalizeRecord, doFinalizeRoundToIdle } from "./finalize-record.ts";
|
|
38
|
-
import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
|
|
39
|
-
import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
|
|
40
|
-
import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
|
|
41
|
-
import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
|
|
42
|
-
import type { AgentCallOpts } from "../orchestration/models/types.ts";
|
|
11
|
+
// [R2] 域 #5 聚合转发 getter 返回类型标注(值装配已迁聚合,仅 type 引用)。
|
|
12
|
+
import type { CollectCoordinator } from "./collect-coordinator.ts";
|
|
13
|
+
// [V2 决策 3] lifecycle-manager idle timer:chatMode record 的 disarm 面(终态化/取消
|
|
14
|
+
// 路径防误杀)——[R3] 消费已随终态写面迁 service/record-lifecycle.ts;[R4]
|
|
15
|
+
// DEFAULT_IDLE_TIMEOUT_MS 消费(assertIdleTimeoutMsSafe 错误文案基准)已随 run 域
|
|
16
|
+
// 迁 service/run-orchestration.ts——本文件 lifecycle-manager 零 import。
|
|
17
|
+
import { type ConcurrencyPool, DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
18
|
+
// [R4] execution-record 消费(project/tryTransition/updateFromEvent)已随 run 域迁
|
|
19
|
+
// service/run-orchestration.ts(+ workflow-dispatch.ts 的 updateFromEvent)——壳内零消费。
|
|
20
|
+
// [R4] doFinalizeRoundToIdle(finalizeRoundToIdle wrapper)已随 run 域迁
|
|
21
|
+
// service/run-orchestration.ts——壳内零消费。
|
|
22
|
+
// [R4] ConversationContinuation 装配(continuations 队列 + continuationFor)已随域 #14
|
|
23
|
+
// Continuation 协作面迁 service/run-orchestration.ts——壳内零消费。
|
|
24
|
+
// [R1] 转发 getter 返回类型标注(值构造已迁聚合,仅 type 引用)。
|
|
25
|
+
import type { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
|
|
26
|
+
// [R4] mergeRunSignals / executeOptionsToEngineTaskSpec / SLUG_MAX_LENGTH / journal 接线 /
|
|
27
|
+
// capability 预检等消费已随 R4 域迁 service/run-orchestration.ts +
|
|
28
|
+
// service/workflow-dispatch.ts——壳内零消费(机制注释随消费主体迁移)。
|
|
43
29
|
import { setHostUiRequestEndpoint } from "./engine/host/host-ui-endpoint.ts";
|
|
44
|
-
// [W3 chat 域收口] chat 轮次与 run 域同路:经 pi-host-binding 解析 registry 'pi' 的
|
|
45
|
-
// cli 形态 port(RemoteEngine),协议 run(interact) 发往 pi-subagent-cli 引擎进程。
|
|
46
|
-
// inproc PiEngine(inproc pi 引擎目录)已删——本文件不再持有任何本地引擎实例。
|
|
47
|
-
import { PI_POOL_KEY, resolveHostPiEnginePort } from "./engine/host/pi-host-binding.ts";
|
|
48
30
|
import type { HostBridgeServiceFace } from "./engine/host/host-bridge.ts";
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
|
|
31
|
+
// [W6 宿主面下沉 → W3 纯镜像] killAll / killRecord / register 三函数经
|
|
32
|
+
// spawnedChildren 状态镜像公共面(engine/host/spawned-children.ts)——子进程活在
|
|
33
|
+
// 引擎进程内,本模块只做镜像记账(终止意图位),实际终止经协议 interact cancel/close。
|
|
34
|
+
// [R4] killRecordChildWithEscalation / registerSpawnedChildForRecord 消费已随 run 域迁
|
|
35
|
+
// service/run-orchestration.ts + workflow-dispatch.ts——壳内剩 killAllSpawnedChildren
|
|
36
|
+
//(dispose 收割兜底)。
|
|
37
|
+
import { killAllSpawnedChildren } from "./engine/host/spawned-children.ts";
|
|
38
|
+
// [R4] 引擎路由/registry/model-validation/engine-sdk 消费已随 run 域与 workflow 族迁
|
|
39
|
+
// 两个聚合文件——壳内零消费。
|
|
59
40
|
import { ManifestStore } from "./manifest-store.ts";
|
|
60
41
|
import type { ModelConfigService } from "./model-config-service.ts";
|
|
61
42
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
62
43
|
import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts";
|
|
63
44
|
// [T4④ / PS-5] flush 被门拦时的未投递 pending 落盘账本(persistUndeliveredNotificationsForReplay 消费)
|
|
64
|
-
|
|
45
|
+
// [R2] BatchBudgetParams / BgNotifyRecord 类型引用已随域 #5 聚合迁至 service/sync-collect-domain.ts(壳内零消费)。
|
|
46
|
+
// [H1 U2] notify 门迁 notifier.ts(Continuation 双闸共用),此处 re-export 保持既有
|
|
47
|
+
// import 路径(测试消费面 `from "../subagent-service.ts"` 不变)。
|
|
48
|
+
// [R4] notifyGateAllowsDelivery 的值消费(kickOffChatRound / onOneShotSettledWatchdog
|
|
49
|
+
// Timeout 双闸)已随 run 域迁聚合——壳内零值消费,仅保留 re-export(机制不变)。
|
|
50
|
+
export { notifyGateAllowsDelivery } from "./notifier.ts";
|
|
65
51
|
import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
|
|
66
52
|
import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
|
|
67
53
|
import type { StatusFilter } from "./record-store.ts";
|
|
68
54
|
import { RecordStore } from "./record-store.ts";
|
|
69
|
-
// [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
|
|
70
|
-
//(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
|
|
71
|
-
import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
|
|
72
|
-
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
73
|
-
// [W6 宿主面下沉 → W3 纯镜像] killAll / killRecord / register 三函数经
|
|
74
|
-
// spawnedChildren 状态镜像公共面(engine/host/spawned-children.ts)——子进程活在
|
|
75
|
-
// 引擎进程内,本模块只做镜像记账(终止意图位),实际终止经协议 interact cancel/close。
|
|
76
|
-
import {
|
|
77
|
-
killAllSpawnedChildren,
|
|
78
|
-
killRecordChildWithEscalation,
|
|
79
|
-
registerSpawnedChildForRecord,
|
|
80
|
-
} from "./engine/host/spawned-children.ts";
|
|
81
|
-
// [u-t2a T2②/T2③ + W4 协议事件面] settled watchdog:chatMode 轮 settled 等待两段守护。
|
|
82
|
-
// W3 起事件接线 = 协议事件流(arm 点 = 轮开始;refresh 源 = streamDelta/轮次生命周期
|
|
83
|
-
// 帧;W4 三入口与旧原语逐一同构,见 settled-watchdog.ts 尾段)。
|
|
84
|
-
import {
|
|
85
|
-
armMidRoundNoProgress,
|
|
86
|
-
disarmRoundFromProtocol,
|
|
87
|
-
disarmSettledWatchdog,
|
|
88
|
-
noteRoundSettledFromProtocol,
|
|
89
|
-
refreshFromProtocolEvent,
|
|
90
|
-
type SettledWatchdogFireInfo,
|
|
91
|
-
} from "./settled-watchdog.ts";
|
|
92
|
-
import { isIdle, isResumable } from "./lifecycle-predicates.ts";
|
|
93
|
-
import { startIdleGc } from "./idle-gc.ts";
|
|
94
55
|
// [W4] 轮次活性监督器(D2「等待有主」权威层;机制与注释见 round-supervisor/,
|
|
95
|
-
// 装配绑定面在 service-binding.ts
|
|
56
|
+
// 装配绑定面在 service-binding.ts——变化轴独立)。[B-6/R4] 字段留壳(boot/dispose
|
|
57
|
+
// 时序消费在壳 + C-6 装配闭包经壳转发 late-bound)。
|
|
96
58
|
import type { RoundSupervisor } from "./round-supervisor/index.ts";
|
|
97
59
|
import {
|
|
98
60
|
createRoundSupervisorForService,
|
|
99
61
|
runPendingReconcileSweepForService,
|
|
100
62
|
} from "./round-supervisor/service-binding.ts";
|
|
101
|
-
// [W4] WorkflowRun store 纳入 idle-gc(startedAt 锚归档,宿主无关实现,见 file-run-store.ts)
|
|
102
|
-
import { FileRunStore } from "../orchestration/file-run-store.ts";
|
|
103
|
-
// [F-1 修复] idle-gc 的 WorkflowRun store 读根 = pi 壳 JsonlRunStore 同源布局
|
|
104
|
-
import { resolvePiWorkflowStateDir } from "./workflow-state-root.ts";
|
|
105
|
-
// [W4] 引擎进程死亡分诊(表 3 行 1 判据:EngineSdkError engine_crashed)
|
|
106
|
-
import { EngineSdkError } from "@zhushanwen/subagent-engine-sdk";
|
|
107
|
-
import type { HostRoundLifecycleParams, ProtocolError, ResumeAnchor } from "@zhushanwen/subagent-engine-sdk";
|
|
108
63
|
import type { StreamSink, SubagentStream } from "./stream-sink.ts";
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
64
|
+
// [R4] settled-watchdog 全族消费(arm/disarm/refresh)已随 run 域与 workflow 族迁两个
|
|
65
|
+
// 聚合文件;hasLiveProcessHandle(killStaleChildBeforeDispatch)已迁 run-orchestration。
|
|
66
|
+
// [R4] state-marker(writeRecordBinding)已迁 run-orchestration;EngineSdkError/
|
|
67
|
+
// ResumeAnchor(引擎死亡分诊)已迁 run-orchestration。
|
|
112
68
|
import type {
|
|
113
69
|
AgentEvent,
|
|
114
70
|
AgentResult,
|
|
115
71
|
ClosedReason,
|
|
116
72
|
ExecuteOptions,
|
|
117
73
|
ExecutionHandle,
|
|
118
|
-
ExecutionMode,
|
|
119
74
|
ExecutionRecord,
|
|
120
75
|
RecordSnapshot,
|
|
121
76
|
SubagentRecord,
|
|
122
77
|
} from "./types.ts";
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
import { registerGlobalObservability
|
|
78
|
+
// [R4] ExecutionMode / ForkDepthExceededError / DEFAULT_AGENT_NAME / WorktreeHandle 消费
|
|
79
|
+
// 已随 run 域迁聚合——types import 收窄为转发签名所需类型面。
|
|
80
|
+
import { registerGlobalObservability } from "./ui-request-observability.ts";
|
|
81
|
+
// [R1] 转发 getter 返回类型标注(实例已迁聚合,仅 type 引用)。
|
|
82
|
+
import type { UiRequestObservability } from "./ui-request-observability.ts";
|
|
126
83
|
import { WorktreeManager } from "./worktree-manager.ts";
|
|
127
|
-
|
|
84
|
+
// [H3/R6] 聚合面接口类型声明(queries/chatActions 消费面 + 构造参数)外移支撑文件
|
|
85
|
+
// 后经 type-only import 消费(编译后擦除,与 bootstrap→壳的 SubagentService 值边
|
|
86
|
+
// 不构成值环)。
|
|
87
|
+
import type {
|
|
88
|
+
SubagentChatActions,
|
|
89
|
+
SubagentQueries,
|
|
90
|
+
SubagentServiceInit,
|
|
91
|
+
} from "./service/service-bootstrap.ts";
|
|
92
|
+
// [H3/R6] ENV_SELF_RECORD_ID 常量 SSOT 归位常量叶子文件(D-R3-2 兑现,原随域 #2
|
|
93
|
+
// 聚合声明)——壳 reconcile sweep 装配闭包判据消费。
|
|
94
|
+
import { ENV_SELF_RECORD_ID } from "./service/service-constants.ts";
|
|
95
|
+
// [H3/R1] 域 #2 聚合(session 注入 + ALS/嵌套身份基线)——壳经转发 getter/方法透传,
|
|
96
|
+
// 对外签名零变化。
|
|
97
|
+
import {
|
|
98
|
+
disposedUiRequestStub,
|
|
99
|
+
SessionBaselines,
|
|
100
|
+
type SubagentServiceSessionInit,
|
|
101
|
+
} from "./service/session-baselines.ts";
|
|
102
|
+
// [H3/R2] 域 #5 聚合(sync 批自闭合语义:collectCoordinator 装配 + E9 转账 + E1 恢复)——
|
|
103
|
+
// 检查点① flushBatch 显式依赖注入随聚合落地;C-1/C-2 跨聚合边收敛为其显式接口。
|
|
104
|
+
import { SyncCollectDomain } from "./service/sync-collect-domain.ts";
|
|
105
|
+
// [H3/R3] 域 #3/#4/#8/#10/#11/#13/#17/#18 聚合(record 读建面 + 终态迁移写面)——壳经
|
|
106
|
+
// 转发方法透传,对外签名零变化。[计划变更 D-R3-1] G1 ≤700 与八域体量(833 物理行)
|
|
107
|
+
// 冲突,拆两文件(dev agent 停线报告、主 agent 核验追认):record-access.ts(#3/#8/#10/#13 读建面)+ record-lifecycle.ts
|
|
108
|
+
//(#4/#11/#17/#18 终态写面,D5/H4 落点);两聚合组间零互调零 import,壳分别装配。
|
|
109
|
+
// [R4] ResolvedIdentity 的壳内消费(R3 过渡转发签名)已删——type import 随删转发清零。
|
|
110
|
+
import { RecordAccess } from "./service/record-access.ts";
|
|
111
|
+
import { RecordLifecycle } from "./service/record-lifecycle.ts";
|
|
112
|
+
// [H3/R4] 域 #6/#7/#12/#14/#15 聚合(run 域执行编排 + Continuation 协作面)与
|
|
113
|
+
// [D-R4-1] 拆分的 workflow 族聚合(executeWorkflowAgent 派发链)——两文件组间零互调
|
|
114
|
+
// 零 import(跨文件协作经壳 deps 闭包,G2「经壳编排」形态),壳分别装配、经转发
|
|
115
|
+
// 方法透传,对外签名零变化。
|
|
116
|
+
import { RunOrchestration } from "./service/run-orchestration.ts";
|
|
117
|
+
import { WorkflowDispatch } from "./service/workflow-dispatch.ts";
|
|
128
118
|
|
|
129
119
|
const logger = getLogger("subagents");
|
|
130
120
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/** 按 id 查内存 running record 的只读快照(G3-002 修复)。不存在返回 undefined。 */
|
|
136
|
-
findRecord(id: string): RecordSnapshot | undefined;
|
|
137
|
-
/** [v8.5 A1/B] 全态查找:任意状态 × 任意归属的 record 快照(message 拒绝文案分流
|
|
138
|
-
* 与 fork-from 源解析共用)。id 在内存与磁盘均不存在返回 undefined。 */
|
|
139
|
-
lookupRecordAnyState(id: string): SubagentRecord | undefined;
|
|
140
|
-
/** 合并内存 + 磁盘 record(/subagents list + tool list 消费,按 rootSessionId 过滤)。 */
|
|
141
|
-
collectRecords(limit: number, statusFilter?: StatusFilter): SubagentRecord[];
|
|
142
|
-
/** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。 */
|
|
143
|
-
getFullRecord(id: string): SubagentRecord | undefined;
|
|
144
|
-
/** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
|
|
145
|
-
onChange(listener: () => void): () => void;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** [D4 对话 action 面聚合] chat 域 message/close action 轴(M2-B3,原 Service 同节三方法)
|
|
149
|
-
* ——Service 上的 `service.chatActions` 消费面。变化轴:改对话域归属校验 / close 分流 /
|
|
150
|
-
投递编排,只动 chatActions 组。 */
|
|
151
|
-
export interface SubagentChatActions {
|
|
152
|
-
/** 按 id 查 record 并做归属校验(message/close action 的统一入口)。 */
|
|
153
|
-
getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord;
|
|
154
|
-
/** close action 的统一行为分流(running 子态 × force)。 */
|
|
155
|
-
closeSubagent(record: ExecutionRecord, force: boolean): Promise<void>;
|
|
156
|
-
/** chatMode 统一投递入口(message action,经引擎交互面执行)。 */
|
|
157
|
-
deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void>;
|
|
158
|
-
}
|
|
121
|
+
// [R6] 聚合面接口类型声明(SubagentQueries / SubagentChatActions / SubagentServiceInit)
|
|
122
|
+
// 已外移支撑文件 service/service-bootstrap.ts——壳经 type-only import 消费(编译后
|
|
123
|
+
// 擦除),bootstrap→壳仅存的 SubagentService 值边(createSubagentService 构造依赖)
|
|
124
|
+
// 不构成值环(壳对 bootstrap 零 re-export)。
|
|
159
125
|
|
|
160
126
|
// [v4 A-1] EPIPE 连续失败计数器在 stdin-writer.ts(stdin 错误域,避免 session-runner
|
|
161
127
|
// 反向 import 本文件 helper 产生循环依赖)。同步路径(PiEngine 热路径投递,D2 协议知识
|
|
162
128
|
// 下沉后)与异步路径(session-runner child.stdin.on('error'))共用 stdin-writer 的同一计数器。
|
|
163
129
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
* [背景] Pi 单进程 session 串行接管。session A shutdown 时 SIGTERM 子进程后、
|
|
167
|
-
* 子进程彻底 close 前(pi 子进程 trap SIGTERM 做 graceful shutdown,窗口几十~几百 ms),
|
|
168
|
-
* 子进程的 trailing extension_ui_request 仍可能被父进程 pump 解析,调到 A 的 handler 闭包。
|
|
169
|
-
* 若 dispose 不清 uiRequestHandler,旧 handler 闭包仍持有 A 的 ctx,触发
|
|
170
|
-
* inproc UI 请求队列(已删) 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
|
|
171
|
-
* logger.error(看起来像 bug,实际是预期竞态;三层兜底已确保功能正确)。
|
|
172
|
-
*
|
|
173
|
-
* stub 始终返回 {cancelled:true},不调 ctx.ui、不捕获任何 ctx,让 trailing ui_request
|
|
174
|
-
* 干净降级为 cancelled(等价于子进程主动取消)。
|
|
175
|
-
*
|
|
176
|
-
* 不置 undefined —— 那会让 trailing ui_request 走 inproc UI 请求队列(已删) 的 handler-missing
|
|
177
|
-
* 分支触发 notifyMissingHandlerGlobal warn,噪声性质从 threw-error 变 missing-handler,
|
|
178
|
-
* 没真正解决。 */
|
|
179
|
-
const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
|
|
130
|
+
// [R1] disposedUiRequestStub(dispose 后 stub UI handler,含背景/降级语义注释)已随
|
|
131
|
+
// 域 #2 聚合迁至 service/session-baselines.ts——壳 dispose 应答端替换经顶部 import 消费。
|
|
180
132
|
|
|
181
133
|
/** UI streaming sink 的最小接口(ctx.ui.setWidget 的 duck-typed 子集)。
|
|
182
134
|
* session_start 时从 ctx.ui 注入,background 执行期间用于把合并后的 text_delta
|
|
@@ -186,104 +138,21 @@ export type { StreamSink } from "./stream-sink.ts";
|
|
|
186
138
|
// pi 依赖端口类型 re-export:测试侧 mock PiLike 历来从本模块取(与 StreamSink 同构的门面模式)
|
|
187
139
|
export type { PiLike } from "./notify-host.ts";
|
|
188
140
|
|
|
189
|
-
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
|
|
193
|
-
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
194
|
-
*/
|
|
195
|
-
export interface SubagentServiceInit {
|
|
196
|
-
cwd: string;
|
|
197
|
-
/** 配置/模型域 Service(execute 内部调其 resolveModel)。 */
|
|
198
|
-
modelService: ModelConfigService;
|
|
199
|
-
/** 缓存的主 session file 获取函数(fork source 解析用)。 */
|
|
200
|
-
getMainSessionFile?: () => string | undefined;
|
|
201
|
-
/** W2: UI 请求处理回调(ask_user 扩展)。
|
|
202
|
-
* 签名见 dialog-queue.ts UiRequestHandler:接收 UiRequest,返回 UiResponse。 */
|
|
203
|
-
uiRequestHandler?: UiRequestHandler;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** session_start 注入参数(session 级)。 */
|
|
207
|
-
export interface SubagentServiceSessionInit {
|
|
208
|
-
pi: PiLike;
|
|
209
|
-
sessionId: string;
|
|
210
|
-
/** 主 session 文件路径(session_start 解析后直传)。
|
|
211
|
-
* [E2E 实测] 不能经闭包缓存(getCachedMainSessionFile)读:jiti 多实例分裂下闭包
|
|
212
|
-
* 变量不跨实例共享,恢复逻辑读到的是滞后一个事件的值(读到未 flush 的新 session
|
|
213
|
-
* ENOENT 路径,entry-born 孤儿整段漏判)。 */
|
|
214
|
-
mainSessionFile?: string;
|
|
215
|
-
/** UI streaming sink(ctx.ui.setWidget),用于 background text_delta 转发。 */
|
|
216
|
-
streamSink?: StreamSink;
|
|
217
|
-
/** 主进程运行模式(W4 守卫:headless 不注入 ask_user RPC 提示词)。
|
|
218
|
-
* initSession 读取后存入 this.sessionMode,buildSessionRunnerContext 透传给 session-runner。 */
|
|
219
|
-
mode?: ExtensionMode;
|
|
220
|
-
/** UI 请求 handler(session 级覆盖进程级)。
|
|
221
|
-
* [D4-④ UI 接线外提] 本字段是 handler 的唯一注入入口(原 setUiRequestHandler 方法已删)。
|
|
222
|
-
* 三态语义:undefined = 不动(保留进程级构造/上次值,供不注入 handler 的调用方);
|
|
223
|
-
* null = 显式清空(承载原 setUiRequestHandler(undefined) 语义——headless 的
|
|
224
|
-
* createUiRequestHandlerForMode 返回 undefined 时壳侧传 null);值 = 注入并重置
|
|
225
|
-
* 缺失告警去重。 */
|
|
226
|
-
uiRequestHandler?: UiRequestHandler | null;
|
|
227
|
-
/** L2 跨子进程全局 dialog 串行队列(进程单例)。透传给 session-runner,
|
|
228
|
-
* child close 时调 rejectChildDialogs 清理 pending(SR-4 防全局死锁)。 */
|
|
229
|
-
dialogQueue?: DialogGlobalQueue;
|
|
230
|
-
/** [竞态修复] 主 agent 是否空闲查询(ctx.isIdle),透传给 notifier 的 flush isIdle gate。
|
|
231
|
-
* 避免 background 完成通知在 agent_end→finishRun 窗口里走错 sendMessage 分支丢失。
|
|
232
|
-
* 可选:未注入时 notifier flush 不 gate(原行为)。 */
|
|
233
|
-
isIdle?: () => boolean;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** background 优先级(保留 priority 排序机制,单一值)。 */
|
|
237
|
-
const PRIORITY_BACKGROUND = 1000;
|
|
238
|
-
|
|
239
|
-
/** 时间换算常数(settled watchdog 分钟数展示用;与 session-runner 同名常量同语义)。 */
|
|
240
|
-
const MS_PER_SECOND = 1000;
|
|
241
|
-
const SECONDS_PER_MINUTE = 60;
|
|
141
|
+
/** session_start 注入参数(session 级)。
|
|
142
|
+
* [R1] 接口本体已迁 service/session-baselines.ts(唯一消费者 SessionBaselines.initSession);
|
|
143
|
+
* 此处类型别名 re-export 保持既有导出符号面(外部 `from "./subagent-service.ts"` 消费零改动)。 */
|
|
144
|
+
export type { SubagentServiceSessionInit };
|
|
242
145
|
|
|
243
|
-
/** [T4① / PS-2] notify 门拦截集:disposeAllRecords 因这两类原因关闭的 record,其迟到的
|
|
244
|
-
* 轮次完成回注不得注入新 session——/new、/fork 的决策([v4 A-6])
|
|
245
|
-
* 是「被关 record 的告知改由 list 的 closedReason 表达」,不主动通知;旧门只排除
|
|
246
|
-
* cancelled,parent-new/parent-fork 放行 → 新对话被「Subagent X failed: closed due to
|
|
247
|
-
* parent-new」的僵尸回执 triggerTurn 唤醒,已废弃会话的通知注入新上下文。 */
|
|
248
|
-
const NOTIFY_BLOCKED_CLOSED_REASONS: ReadonlySet<ClosedReason> = new Set(["parent-new", "parent-fork"]);
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* [T4① / PS-2] 轮次完成回注的 notify 门(按 closedReason 白名单放行)。
|
|
252
|
-
*
|
|
253
|
-
* cancelled(cancelBackground 自己 notify)与 parent-new/parent-fork(编排性关闭,
|
|
254
|
-
* 告知由 list 的 closedReason 表达)不放行;其余(undefined = 本路径抢到 CAS 尚未
|
|
255
|
-
* 终态化的迟到回调、user-close/gc 等真实终态)照旧回注。导出供测试与未来调用点复用。
|
|
256
|
-
*/
|
|
257
|
-
export function notifyGateAllowsDelivery(closedReason: ClosedReason | undefined): boolean {
|
|
258
|
-
if (closedReason === undefined) return true;
|
|
259
|
-
if (closedReason === "cancelled") return false;
|
|
260
|
-
return !NOTIFY_BLOCKED_CLOSED_REASONS.has(closedReason);
|
|
261
|
-
}
|
|
262
146
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
|
|
273
|
-
const ENV_DEPTH = "PI_SUBAGENT_DEPTH";
|
|
274
|
-
const ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
|
|
275
|
-
|
|
276
|
-
/** [v2 D4] E1 等待分支 settled 有界重扫上限。无上限重扫 = 泄漏(设计 §3.3 D4 被否
|
|
277
|
-
* 谱系);8 次覆盖重启后主 agent 对 resumable 成员的典型续跑轮次,达限仍有
|
|
278
|
-
* running → disposed,交下次 session_start 收敛。 */
|
|
279
|
-
const SETTLED_RESCAN_LIMIT = 8;
|
|
280
|
-
|
|
281
|
-
/** resolveIdentity 的产物——一次确定、写入 record 后不再变。 */
|
|
282
|
-
interface ResolvedIdentity {
|
|
283
|
-
agent: string;
|
|
284
|
-
agentConfig: AgentConfig | undefined;
|
|
285
|
-
resolved: ResolvedModel;
|
|
286
|
-
}
|
|
147
|
+
// [R1] 跨进程身份贯穿 env 名常量(ENV_ROOT_SESSION_ID / ENV_DEPTH / ENV_ROOT_CWD,
|
|
148
|
+
// 含 [MF-3] 注释)SSOT 已随域 #2 聚合迁至 service/session-baselines.ts;[R6/D-R3-2]
|
|
149
|
+
// ENV_SELF_RECORD_ID 因跨聚合消费(record-access)归位 service/service-constants.ts
|
|
150
|
+
// 常量叶子文件——壳经顶部 import 消费(reconcile sweep 装配闭包的判据)。
|
|
151
|
+
// [R2] SETTLED_RESCAN_LIMIT 常量 SSOT 已随域 #5 聚合迁至 service/sync-collect-domain.ts
|
|
152
|
+
//(唯一消费主体 armSettledRescan)。
|
|
153
|
+
// [R3] ResolvedIdentity 接口本体(resolveIdentity 产物)已随读建面迁
|
|
154
|
+
// service/record-access.ts——壳经顶部 type import 消费(R4 领地 execute/executeAndAwait/
|
|
155
|
+
// workflow 派发链的局部类型标注不变)。
|
|
287
156
|
|
|
288
157
|
/**
|
|
289
158
|
* 执行编排 Service。进程级单例。
|
|
@@ -302,51 +171,18 @@ interface ResolvedIdentity {
|
|
|
302
171
|
* @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
|
|
303
172
|
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
304
173
|
*/
|
|
174
|
+
|
|
305
175
|
export class SubagentService {
|
|
176
|
+
|
|
177
|
+
// ── 域 #1 依赖装配(壳组合根;R1-R4 抽取后留壳)(R0 重排)──
|
|
178
|
+
|
|
306
179
|
private readonly pool: ConcurrencyPool;
|
|
307
180
|
private readonly store: RecordStore;
|
|
308
181
|
private readonly modelService: ModelConfigService;
|
|
309
182
|
private readonly cwd: string;
|
|
310
183
|
private readonly worktreeManager: WorktreeManager;
|
|
311
184
|
private readonly getMainSessionFile: (() => string | undefined) | undefined;
|
|
312
|
-
|
|
313
|
-
private uiRequestHandler: SubagentServiceInit["uiRequestHandler"];
|
|
314
|
-
/** L2 dialog 串行队列(进程级)。SR-4:child close 时 session-runner 调 rejectChildDialogs 清理。 */
|
|
315
|
-
private dialogQueue: DialogGlobalQueue | undefined;
|
|
316
|
-
/** UI 请求可观测性(sessionMode + handler 缺失告警去重,提取自本类降低行数)。 */
|
|
317
|
-
private readonly uiObservability = new UiRequestObservability();
|
|
318
|
-
private pi: PiLike | null = null;
|
|
319
|
-
/** 当前 Pi session ID(本进程 pi session,事件路由等用;record 过滤不用它)。initSession 时注入。 */
|
|
320
|
-
private sessionId: string | null = null;
|
|
321
|
-
/** 主 session 文件(initSession 按值直传——jiti 多实例下闭包缓存不可靠,见 SessionInit 注释)。 */
|
|
322
|
-
private mainSessionFile: string | undefined;
|
|
323
|
-
/** 所属根 session ID(record 归属过滤用)。根进程 = sessionId(自己是 root);
|
|
324
|
-
* 子进程 = env PI_SUBAGENT_ROOT_SESSION_ID 贯穿的真 ROOT(initSession 读取)。
|
|
325
|
-
* 与 sessionId 正交:sessionId 是本进程 pi session(事件路由等),sessionRootId 是所属根
|
|
326
|
-
* (collectRecords filter 用,与 createRecordForMode 的 rootSessionId 盖章同源——子进程
|
|
327
|
-
* 因此看到整棵 ROOT 树)。设计见 recursive-subagent-visibility.md 决策 3。 */
|
|
328
|
-
private sessionRootId: string | null = null;
|
|
329
|
-
/**
|
|
330
|
-
* [D3-⑤ 嵌套防护合一] 进程内执行嵌套上下文(原 execCtxAls 私有字段下沉公共层
|
|
331
|
-
* common/nesting-guard.ts ExecutionNestingContext——机制注释含 ALS 断裂基线兜底)。
|
|
332
|
-
* 实例 per-Service:基线随宿主进程身份而异(initSession 从 env 建立)。
|
|
333
|
-
*/
|
|
334
|
-
private readonly execNesting = new ExecutionNestingContext();
|
|
335
|
-
/** fork 深度基线(同 ALS 断裂问题:forkDepthAls.getStore() 兜底用)。根进程=0。 */
|
|
336
|
-
private forkDepthBaseline = 0;
|
|
337
|
-
/** [MF-3] 所属根进程 cwd(sessions/records 落盘目录编码键)。
|
|
338
|
-
* 根进程=自身 cwd(构造时 init.cwd);子进程=env PI_SUBAGENT_ROOT_CWD 贯穿的真 ROOT cwd。
|
|
339
|
-
* worktree 模式下子进程 this.cwd 是 checkout 路径,若按它编码目录,深层 record 落到
|
|
340
|
-
* enc(worktree) 段、ROOT 扫描不到 → 全树可见性深度 ≥ 2 断裂(与 sessionRootId 同构)。 */
|
|
341
|
-
private rootCwd: string;
|
|
342
|
-
/** UI streaming sink(ctx.ui.setWidget)。workflow 域经 getStreamSink() 取用。 */
|
|
343
|
-
private streamSink: StreamSink | null = null;
|
|
344
|
-
/** [竞态修复] 主 agent isIdle 查询(ctx.isIdle)。notifier flush gate 用。
|
|
345
|
-
* initSession 注入,piAdapter 透传给 NotifierHost。 */
|
|
346
|
-
private isIdleFn: (() => boolean) | undefined;
|
|
347
|
-
getStreamSink(): StreamSink | null { return this.streamSink; }
|
|
348
|
-
private _disposed = false;
|
|
349
|
-
private _seq = 0;
|
|
185
|
+
|
|
350
186
|
/** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
|
|
351
187
|
* 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
|
|
352
188
|
* deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
|
|
@@ -359,97 +195,6 @@ export class SubagentService {
|
|
|
359
195
|
getIsIdle: () => this.isIdleFn,
|
|
360
196
|
});
|
|
361
197
|
|
|
362
|
-
/** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
|
|
363
|
-
private readonly collectCoordinator: CollectCoordinator;
|
|
364
|
-
/** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
|
|
365
|
-
* 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
|
|
366
|
-
* 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
|
|
367
|
-
* sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
|
|
368
|
-
* 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
|
|
369
|
-
private readonly e9ConvertedIds = new Set<string>();
|
|
370
|
-
/** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
|
|
371
|
-
* 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
|
|
372
|
-
* 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
|
|
373
|
-
* pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
|
|
374
|
-
* 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
|
|
375
|
-
* 的文件域);dispose 的惰化处置见 dispose()。 */
|
|
376
|
-
private settledRescanState: { disposed: boolean; scans: number } | null = null;
|
|
377
|
-
/** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
|
|
378
|
-
* 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
|
|
379
|
-
* parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
|
|
380
|
-
* [MF#2] 旧实现用单实例字段跨执行链共享 → 并发下 A 还原深度后 B 读到被压低值 → 护栏失效。 */
|
|
381
|
-
private readonly forkDepthAls = new AsyncLocalStorage<number>();
|
|
382
|
-
|
|
383
|
-
// [D3-⑤] subagent 执行上下文(record 身份 + 递归深度)的 ALS 传递已下沉公共层
|
|
384
|
-
// (execNesting 字段,common/nesting-guard.ts)——「B run() 期间挂身份,B 内创建 C
|
|
385
|
-
// 时读到 B」的机制与 ALS 断裂基线兜底注释见该文件。与 forkDepthAls 独立:后者只数
|
|
386
|
-
// fork 链(fork=true 才递增),嵌套上下文数所有 subagent 嵌套。
|
|
387
|
-
|
|
388
|
-
/** [review MF1] record 级在途 resume 守卫。冷路径续轮(resumeChatRound)全部守卫通过后
|
|
389
|
-
* add,runAndFinalize 结束(finally,覆盖轮次完成 / MF-6 失败回退 / abort / 终态化所有
|
|
390
|
-
* 分支)时 delete(幂等:execute() 新建 record 不在集合,no-op)。窗口 = resume 发起
|
|
391
|
-
* (含 pool.acquire 排队)→ 本轮 runAndFinalize 收尾。窗口内同 record 再次到达续轮
|
|
392
|
-
* (冷路径重入 / EPIPE 兜底)直接 throw——防两个 pi 子进程以 --session 同一 JSONL 双写 +
|
|
393
|
-
* 前一个脱离 kill 记账成孤儿(冷路径的 acquireActivateLock 只覆盖续轮同步段,锁释放
|
|
394
|
-
* 在子进程注册(session-runner spawnedChildren.set)之前,锁空洞由此守卫兜住;EPIPE
|
|
395
|
-
* 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
|
|
396
|
-
private readonly resumesInFlight = new Set<string>();
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* [W3 chat 域收口] chat 轮次反向通道路由表(recordId 键):首轮 run chat 注册、
|
|
400
|
-
* interact 续聊轮复用——host/streamDelta(recordId 分支)与 host/roundLifecycle
|
|
401
|
-
* (recordId 键)的分发目标(EngineClient.recordRoutes 经
|
|
402
|
-
* EnginePort.registerChatRoundRoute 注册)。record 终态化路径注销([F-5 修复]
|
|
403
|
-
* 汇聚点 = doFinalizeRecord 的 onFinalized 钩子 + cancelBackground /
|
|
404
|
-
* disposeAllRecords 两处直连终态化路径显式注销)。
|
|
405
|
-
*/
|
|
406
|
-
private readonly chatRoundRoutes = new Map<string, () => void>();
|
|
407
|
-
|
|
408
|
-
/** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
|
|
409
|
-
* deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
|
|
410
|
-
private readonly coldResurrectDeps: ColdResurrectDeps = {
|
|
411
|
-
findLightById: (id) => this.store.findLightById(id),
|
|
412
|
-
collectRecords: (limit, statusFilter, rootFilter) =>
|
|
413
|
-
this.store.collectRecords(limit, statusFilter, rootFilter),
|
|
414
|
-
register: (record) => this.store.register(record),
|
|
415
|
-
reportRecordTransition: (record) => this.store.reportRecordTransition(record),
|
|
416
|
-
getSessionRootId: () => this.sessionRootId,
|
|
417
|
-
getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* [W4] 轮次活性监督器(D2「等待有主」权威层):死亡事件纳管(run failed / 引擎
|
|
422
|
-
* exited → adoptOnProcessDeath)+ boot 分区重认领(initSession)+ 三态判定
|
|
423
|
-
* (record 级视图)+ 该放弃(终态化 failed + 注销 + 终止通知)。装配绑定面
|
|
424
|
-
* (deps/giveUp 编排/sweep 挂点)在 round-supervisor/service-binding.ts——变化轴
|
|
425
|
-
* 独立(通知文案 / 终态化编排 / store 读侧判据只动该文件),本字段只持实例。
|
|
426
|
-
*/
|
|
427
|
-
private readonly roundSupervisor: RoundSupervisor = createRoundSupervisorForService({
|
|
428
|
-
getStore: () => this.store,
|
|
429
|
-
getPi: () => this.pi,
|
|
430
|
-
getSessionRootId: () => this.sessionRootId,
|
|
431
|
-
getMainSessionFile: () => this.mainSessionFile,
|
|
432
|
-
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
/** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
|
|
436
|
-
* 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
|
|
437
|
-
readonly queries: SubagentQueries = {
|
|
438
|
-
findRecord: (id) => this.findRecord(id),
|
|
439
|
-
lookupRecordAnyState: (id) => this.lookupRecordAnyState(id),
|
|
440
|
-
collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
|
|
441
|
-
getFullRecord: (id) => this.getFullRecord(id),
|
|
442
|
-
onChange: (listener) => this.onChange(listener),
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
/** [D4 对话 action 面聚合] chat 域 message/close 消费面(壳 subagent-actions 经此访问;
|
|
446
|
-
* 纯委托同上。PiEngineService 适配器不经此——引擎边界走 piEngineServiceAdapter)。 */
|
|
447
|
-
readonly chatActions: SubagentChatActions = {
|
|
448
|
-
getRecordForAction: (id, opts) => this.getRecordForAction(id, opts),
|
|
449
|
-
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
450
|
-
deliverChatMessage: (record, text, interrupt) => this.deliverChatMessage(record, text, interrupt),
|
|
451
|
-
};
|
|
452
|
-
|
|
453
198
|
private readonly manifestStore: ManifestStore;
|
|
454
199
|
|
|
455
200
|
/**
|
|
@@ -469,7 +214,43 @@ export class SubagentService {
|
|
|
469
214
|
this.cwd = init.cwd;
|
|
470
215
|
this.modelService = init.modelService;
|
|
471
216
|
this.getMainSessionFile = init.getMainSessionFile;
|
|
472
|
-
|
|
217
|
+
// [R1] 域 #2 基线聚合:进程级初值(uiRequestHandler / rootCwd env 推导)在聚合构造期
|
|
218
|
+
// 落位;deps 全晚绑定闭包(构造期零求值——store/notifyHost 等后续才构造的依赖经闭包
|
|
219
|
+
// 现读,形态先例 = D4 late-bound getter 与 notifyHost 装配闭包)。
|
|
220
|
+
this.baselines = new SessionBaselines(
|
|
221
|
+
{ cwd: init.cwd, uiRequestHandler: init.uiRequestHandler },
|
|
222
|
+
{
|
|
223
|
+
// [D4 late-bound getter `() => ({pi, disposed})`] assertReady 断言状态现读:
|
|
224
|
+
// pi(initSession 时点注入,经壳 getter 透传聚合)+ disposed(壳旗标)。
|
|
225
|
+
readAssertState: () => ({ pi: this.pi, disposed: this._disposed }),
|
|
226
|
+
// session 复活(initSession 内 revive 步骤回调)——旗标写点留壳(断言面同源)。
|
|
227
|
+
reviveDisposed: () => {
|
|
228
|
+
this._disposed = false;
|
|
229
|
+
},
|
|
230
|
+
// [C-2 显式回调 → R2 已对接] #5 SyncCollect 的 settledRescanState 复活重置改指
|
|
231
|
+
// SyncCollectDomain 显式接口(resetSettledRescan)——聚合间零直写(G2)。晚绑定
|
|
232
|
+
// 闭包构造期零求值,syncCollect 后置构造安全(R1 打样时预留的对接点)。
|
|
233
|
+
resetSettledRescan: () => {
|
|
234
|
+
this.syncCollect.resetSettledRescan();
|
|
235
|
+
},
|
|
236
|
+
getStore: () => this.store,
|
|
237
|
+
getNotifyHost: () => this.notifyHost,
|
|
238
|
+
// [跨域编排回调] initSession 复活后编排(R3/R4 域方法;抽取后改指聚合显式接口)。
|
|
239
|
+
recoverOrphans: () => this.recoverOrphansIfRootProcess(),
|
|
240
|
+
bootRoundSupervisor: () => this.roundSupervisor.bootPartition(),
|
|
241
|
+
runPendingReconcileSweep: () =>
|
|
242
|
+
runPendingReconcileSweepForService(
|
|
243
|
+
{
|
|
244
|
+
getStore: () => this.store,
|
|
245
|
+
getPi: () => this.pi,
|
|
246
|
+
getSessionRootId: () => this.sessionRootId,
|
|
247
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
248
|
+
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
249
|
+
},
|
|
250
|
+
(process.env[ENV_SELF_RECORD_ID] ?? "") !== "",
|
|
251
|
+
),
|
|
252
|
+
},
|
|
253
|
+
);
|
|
473
254
|
// [W6 R3 MF-A] 壳侧应答端登记:cli 形态引擎经 host/askUser 反向请求消费(discovery
|
|
474
255
|
// portFactory 构造 EngineClient 时读取该登记)。
|
|
475
256
|
setHostUiRequestEndpoint(init.uiRequestHandler);
|
|
@@ -477,723 +258,430 @@ export class SubagentService {
|
|
|
477
258
|
this.worktreeManager = new WorktreeManager(this.modelService.getAgentDir());
|
|
478
259
|
// [MF-3] worktree 隔离下全树落盘目录统一到 ROOT cwd:子进程(spawn cwd = worktree checkout 路径)
|
|
479
260
|
// 若按自身 cwd 编码目录,深层 record 写到 enc(worktree) 段,ROOT 磁盘重建扫不到。
|
|
480
|
-
//
|
|
481
|
-
//
|
|
261
|
+
// rootCwd 的 env 推导随域 #2 聚合(session-baselines.ts 构造器);sessions 与 records 两套
|
|
262
|
+
// 目录必须同源(同一 rootCwd),否则 enc 段不变量断裂(只改其一会让同 record 的
|
|
482
263
|
// session 文件与 manifest 分落两段,GC/重建互相找不到)。
|
|
483
|
-
const envRootCwd = process.env[ENV_ROOT_CWD];
|
|
484
|
-
this.rootCwd = envRootCwd && envRootCwd !== "" ? envRootCwd : init.cwd;
|
|
485
264
|
const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
486
265
|
const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
487
266
|
this.sessionsDir = sessionsDir;
|
|
488
267
|
this.recordsDir = recordsDir;
|
|
489
268
|
this.manifestStore = new ManifestStore(recordsDir);
|
|
490
|
-
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
// batchFinalized 纯落标(设计 §3.1.3 两出口之一:批闭合 flush 写账成功后)。
|
|
551
|
-
// collectMode/batchFinalized 显式覆写在 appendBatchFinalizedEntry 内(防非
|
|
552
|
-
// entry 源重建丢标记);末条 entry 带标记 → E1 重建扫描(collectMode=sync 且
|
|
553
|
-
// 无标记才收)据此排除,防双重通知。源序「写账先于落标」不变(v1 幂等窗口
|
|
554
|
-
// 语义);manifest 已在屏障提前写,幂等窗口内崩溃时索引更早已就位。
|
|
555
|
-
for (const full of fulls) {
|
|
556
|
-
this.appendBatchFinalizedEntry(full);
|
|
557
|
-
}
|
|
558
|
-
// [S11] miss 成员兜底落标(缓冲快照 → 最小标记 entry,映射见 sync-rebuild.ts),
|
|
559
|
-
// 与 fulls 同出口①语义(写账成功后统一补标)。
|
|
560
|
-
for (const m of fullMissed) {
|
|
561
|
-
this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? undefined));
|
|
562
|
-
}
|
|
269
|
+
// [U1 偏差 3 收尾 / D8 v7] 第 4 参 manifestDir 接线(与 manifestStore 同源同一
|
|
270
|
+
// recordsDir,构造点同语句保证不漂移):终态原语(markFinalized/markCancelled/
|
|
271
|
+
// markBatchFinalized)的 manifest 面走 writeAtomicFileSync 同步落盘——停机窗
|
|
272
|
+
// fire-and-forget 竞态构造性消灭;生产构造点恒传 manifestDir(见下方构造
|
|
273
|
+
// 调用),缺省降级异步分支仅纯内存测试形态可达。
|
|
274
|
+
this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? undefined, recordsDir);
|
|
275
|
+
// [W4 发射点② / U5 收口项②] markRoundIdle 簿记⑧的 pending 注销闭包接线(唯一
|
|
276
|
+
// 装配点——轮终注销由 store 统一发射,调用方薄壳不再双轨重复发)。
|
|
277
|
+
this.store.setPendingUnregister((id, status) => this.notifyHost.emitPendingUnregister(id, status));
|
|
278
|
+
// [R2] 域 #5 聚合:sync 批自闭合语义(collectCoordinator 装配 + E9 dispose 转账 +
|
|
279
|
+
// E1 崩溃恢复 + settled 有界重扫 + collectSync 配置读取)。deps 全晚绑定闭包(构造期
|
|
280
|
+
// 零求值——store/notifyHost/baselines 基线字段等运行时可变态经闭包
|
|
281
|
+
// 现读,形态先例 = D4 late-bound getter 与 R1 装配);[检查点①] flushBatch 显式依赖
|
|
282
|
+
// 注入随聚合落地:CollectCoordinator 在聚合构造器内装配,flushBatch 闭包的外部状态
|
|
283
|
+
// 经 deps getter 现读,service 整实例零注入。
|
|
284
|
+
this.syncCollect = new SyncCollectDomain({
|
|
285
|
+
getStore: () => this.store,
|
|
286
|
+
getNotifyHost: () => this.notifyHost,
|
|
287
|
+
getPi: () => this.pi,
|
|
288
|
+
getSessionRootId: () => this.sessionRootId,
|
|
289
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
290
|
+
getCollectSyncSection: () => this.modelService.getGlobalConfig().collectSync,
|
|
291
|
+
});
|
|
292
|
+
// [R3] 域 #3/#8/#10/#13 聚合(record 读建面:孤儿恢复/查询投影/action 网关/身份解析
|
|
293
|
+
// 与 record 创建)。deps 全晚绑定闭包(构造期零求值——store/manifestStore/modelService
|
|
294
|
+
// 为 #1 留壳共享依赖经 getter 现读同一实例;sessionRootId/sessionId/mainSessionFile/
|
|
295
|
+
// execNesting 为 #2 基线字段运行时可变态现读壳 getter);assertReady 经壳转发方法
|
|
296
|
+
// 现读(D4 下沉形态)。与 recordLifecycle 组间零 import(G2),壳分别装配。
|
|
297
|
+
this.recordAccess = new RecordAccess({
|
|
298
|
+
assertReady: () => this.assertReady(),
|
|
299
|
+
getStore: () => this.store,
|
|
300
|
+
getManifestStore: () => this.manifestStore,
|
|
301
|
+
getModelService: () => this.modelService,
|
|
302
|
+
getSessionRootId: () => this.sessionRootId,
|
|
303
|
+
getSessionId: () => this.sessionId,
|
|
304
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
305
|
+
getExecNesting: () => this.execNesting,
|
|
306
|
+
});
|
|
307
|
+
// [R3] 域 #4/#11/#17/#18 聚合(record 终态迁移写面:dispose 批量回收/close 三路/
|
|
308
|
+
// cancel/finalize 簇——D5「store 与终态迁移入口的唯一宿主」,H4 落点)。跨聚合边
|
|
309
|
+
// 收敛:C-5(onRecordFinalizedCleanup 汇聚点 + Continuation 队列清空,本体在壳
|
|
310
|
+
// #14 协作面)经 deps 回调;cancelBackground 的 collectCoordinator.route 经 #5
|
|
311
|
+
// 显式 getter 投影现读(聚合间零私有互调)。C-6(roundSupervisor/reconcile sweep
|
|
312
|
+
// 装配闭包调 finalizeRecord)经壳转发方法 late-bound 读取,装配点零改动。
|
|
313
|
+
this.recordLifecycle = new RecordLifecycle({
|
|
314
|
+
assertReady: () => this.assertReady(),
|
|
315
|
+
getStore: () => this.store,
|
|
316
|
+
getWorktreeManager: () => this.worktreeManager,
|
|
317
|
+
getModelService: () => this.modelService,
|
|
318
|
+
getNotifyHost: () => this.notifyHost,
|
|
319
|
+
getSessionsDir: () => this.sessionsDir,
|
|
320
|
+
getPi: () => this.pi,
|
|
321
|
+
getCollectCoordinator: () => this.collectCoordinator,
|
|
322
|
+
// [R4 / C-5 兑现] Continuation 协作面本体(continuations 队列 +
|
|
323
|
+
// onRecordFinalizedCleanup + abortAndClearQueue)已迁 RunOrchestration 聚合——
|
|
324
|
+
// R3 装配时指向壳闭包的两个回调改指聚合显式接口(r0-inventory 清单① C-5
|
|
325
|
+
// 收敛完成;聚合间零直写,G2)。
|
|
326
|
+
onRecordFinalizedCleanup: (id) => this.runOrchestration.onRecordFinalizedCleanup(id),
|
|
327
|
+
abortContinuationQueue: (id) => {
|
|
328
|
+
this.runOrchestration.abortContinuationQueue(id);
|
|
563
329
|
},
|
|
564
330
|
});
|
|
331
|
+
// [R4] 域 #6/#7/#12/#14/#15 聚合(run 域执行编排 + Continuation 协作面 + pool/
|
|
332
|
+
// worktree 资源)。deps 全晚绑定闭包(构造期零求值——#1 留壳共享依赖经 getter
|
|
333
|
+
// 现读同一实例;R3 聚合显式接口直指 recordAccess/recordLifecycle,聚合间零私有
|
|
334
|
+
// 互调 G2)。[B-6] roundSupervisor 留壳(boot/dispose 时序消费在壳 + C-6 装配
|
|
335
|
+
// 闭包经壳转发 late-bound 天然兼容),聚合经 getter 现读。
|
|
336
|
+
this.runOrchestration = new RunOrchestration({
|
|
337
|
+
assertReady: () => this.assertReady(),
|
|
338
|
+
getStore: () => this.store,
|
|
339
|
+
getModelService: () => this.modelService,
|
|
340
|
+
getCwd: () => this.cwd,
|
|
341
|
+
getWorktreeManager: () => this.worktreeManager,
|
|
342
|
+
getNotifyHost: () => this.notifyHost,
|
|
343
|
+
getPool: () => this.pool,
|
|
344
|
+
getPi: () => this.pi,
|
|
345
|
+
getSessionRootId: () => this.sessionRootId,
|
|
346
|
+
getStreamSink: () => this.streamSink,
|
|
347
|
+
getUiObservability: () => this.uiObservability,
|
|
348
|
+
getExecNesting: () => this.execNesting,
|
|
349
|
+
getRoundSupervisor: () => this.roundSupervisor,
|
|
350
|
+
getCollectCoordinator: () => this.collectCoordinator,
|
|
351
|
+
resolveIdentity: (opts, pre) => this.recordAccess.resolveIdentity(opts, pre),
|
|
352
|
+
resolveIdentityForEngine: (engine, engineModel, agent, agentConfig, opts) =>
|
|
353
|
+
this.recordAccess.resolveIdentityForEngine(engine, engineModel, agent, agentConfig, opts),
|
|
354
|
+
createRecordForMode: (identity, opts, mode, originFields) =>
|
|
355
|
+
this.recordAccess.createRecordForMode(identity, opts, mode, originFields),
|
|
356
|
+
buildEarlyFailedHandle: (record) => this.recordAccess.buildEarlyFailedHandle(record),
|
|
357
|
+
finalizeRecord: (record, result, status, closedReason) =>
|
|
358
|
+
this.recordLifecycle.finalizeRecord(record, result, status, closedReason),
|
|
359
|
+
finalizeFailed: (record, err) => this.recordLifecycle.finalizeFailed(record, err),
|
|
360
|
+
finalizeAborted: (record) => this.recordLifecycle.finalizeAborted(record),
|
|
361
|
+
closeChatIdle: (record) => this.recordLifecycle.closeChatIdle(record),
|
|
362
|
+
});
|
|
363
|
+
// [R4 / D-R4-1 拆分] workflow 族聚合(executeWorkflowAgent + runWorkflowEngineTask
|
|
364
|
+
// + 类外派发 helper)——与 RunOrchestration 组间零互调零 import,跨文件协作
|
|
365
|
+
//(池槽/终态收口/outcome 映射/资源回收/port 解析/入口校验)经本装配闭包指回
|
|
366
|
+
// runOrchestration 实例方法(G2「经壳编排」形态)。
|
|
367
|
+
this.workflowDispatch = new WorkflowDispatch({
|
|
368
|
+
assertReady: () => this.assertReady(),
|
|
369
|
+
assertIdleTimeoutMsSafe: (opts) => this.runOrchestration.assertIdleTimeoutMsSafe(opts),
|
|
370
|
+
getExecNesting: () => this.execNesting,
|
|
371
|
+
getModelService: () => this.modelService,
|
|
372
|
+
resolveChatEnginePort: () => this.runOrchestration.resolveChatEnginePort(),
|
|
373
|
+
resolveIdentity: (opts) => this.recordAccess.resolveIdentity(opts),
|
|
374
|
+
resolveIdentityForEngine: (engine, engineModel, agent, agentConfig, opts) =>
|
|
375
|
+
this.recordAccess.resolveIdentityForEngine(engine, engineModel, agent, agentConfig, opts),
|
|
376
|
+
createRecordForMode: (identity, opts, mode, originFields) =>
|
|
377
|
+
this.recordAccess.createRecordForMode(identity, opts, mode, originFields),
|
|
378
|
+
getNotifyHost: () => this.notifyHost,
|
|
379
|
+
getStreamSink: () => this.streamSink,
|
|
380
|
+
getUiObservability: () => this.uiObservability,
|
|
381
|
+
getSessionRootId: () => this.sessionRootId,
|
|
382
|
+
getRoundSupervisor: () => this.roundSupervisor,
|
|
383
|
+
acquirePoolOrFinalize: (record, signal, priority) =>
|
|
384
|
+
this.runOrchestration.acquirePoolOrFinalize(record, signal, priority),
|
|
385
|
+
outcomeToAgentResult: (record, outcome) =>
|
|
386
|
+
this.runOrchestration.outcomeToAgentResult(record, outcome),
|
|
387
|
+
settleOneShotOutcome: (record, result, aborted) =>
|
|
388
|
+
this.runOrchestration.settleOneShotOutcome(record, result, aborted),
|
|
389
|
+
finalizeFailed: (record, err) => this.recordLifecycle.finalizeFailed(record, err),
|
|
390
|
+
releaseRoundResources: (record, holdSlot, stream) =>
|
|
391
|
+
this.runOrchestration.releaseRoundResources(record, holdSlot, stream),
|
|
392
|
+
});
|
|
565
393
|
// #11:注册进程级 observability 单例——inproc UI 请求队列 handleUiRequest(已删) 经
|
|
566
394
|
// globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
|
|
567
395
|
// warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
|
|
568
396
|
registerGlobalObservability(this.uiObservability);
|
|
569
397
|
}
|
|
570
398
|
|
|
571
|
-
//
|
|
399
|
+
// [D4-①] 通知簇四方法(notifyComplete / notifyClosed / piAdapter / toNotifyRecord)
|
|
400
|
+
// 与 emitPendingRegister / emitPendingUnregister 模块函数已整体搬移至 notify-host.ts
|
|
401
|
+
//(本类经 this.notifyHost 消费;行为逐字节等价,搬移 + 依赖注入)。
|
|
402
|
+
|
|
403
|
+
// ── 对外聚合面(域 #0,壳终态保留 D3)(R0 重排)──
|
|
404
|
+
|
|
405
|
+
/** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
|
|
406
|
+
* 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
|
|
407
|
+
readonly queries: SubagentQueries = {
|
|
408
|
+
findRecord: (id) => this.findRecord(id),
|
|
409
|
+
lookupRecordAnyState: (id) => this.lookupRecordAnyState(id),
|
|
410
|
+
collectRecords: (limit, statusFilter, includeWorkflow) =>
|
|
411
|
+
this.collectRecords(limit, statusFilter, includeWorkflow),
|
|
412
|
+
// [H2 W1] parentRunId 查询入口:rootSessionFilter 口径与 private collectRecords 一致
|
|
413
|
+
//(sessionRootId → sessionId → undefined 兜底),委托 store(不过滤 origin)。
|
|
414
|
+
collectRecordsByParentRunId: (parentRunId, limit) =>
|
|
415
|
+
this.store.collectRecordsByParentRunId(
|
|
416
|
+
parentRunId,
|
|
417
|
+
limit,
|
|
418
|
+
this.sessionRootId ?? this.sessionId ?? undefined,
|
|
419
|
+
),
|
|
420
|
+
getFullRecord: (id) => this.getFullRecord(id),
|
|
421
|
+
onChange: (listener) => this.onChange(listener),
|
|
422
|
+
};
|
|
572
423
|
|
|
573
|
-
/**
|
|
424
|
+
/** [D4 对话 action 面聚合] chat 域 message/close 消费面(壳 subagent-actions 经此访问;
|
|
425
|
+
* 纯委托同上。PiEngineService 适配器不经此——引擎边界走 piEngineServiceAdapter)。 */
|
|
426
|
+
readonly chatActions: SubagentChatActions = {
|
|
427
|
+
getRecordForAction: (id, opts) => this.getRecordForAction(id, opts),
|
|
428
|
+
closeSubagent: (record, force) => this.closeSubagent(record, force),
|
|
429
|
+
deliverChatMessage: (record, text) => this.deliverChatMessage(record, text),
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// ── 域 #2 SessionBaselines 聚合转发(R1 抽取;本体 execution/service/session-baselines.ts)──
|
|
433
|
+
|
|
434
|
+
/** [R1] 域 #2 聚合实例:session 级基线状态(pi / session 身份 / fork ALS / 嵌套 ALS /
|
|
435
|
+
* UI handler 面)的唯一宿主与唯一写者(r0-inventory 清单① #13-#25 共 13 字段)。
|
|
436
|
+
* deps 全晚绑定闭包(装配见构造器),壳经下方 getter/方法透传,对外签名零变化。 */
|
|
437
|
+
private readonly baselines: SessionBaselines;
|
|
438
|
+
|
|
439
|
+
// 基线字段读路径透传(R1 打样模式 2·strangler 转发壳):壳内既有 this.X 引用点零改动;
|
|
440
|
+
// 写点已收口聚合方法(D1 单写者),只 getter 无 setter = 壳侧只读。
|
|
441
|
+
private get pi(): PiLike | null { return this.baselines.pi; }
|
|
442
|
+
private get sessionId(): string | null { return this.baselines.sessionId; }
|
|
443
|
+
private get mainSessionFile(): string | undefined { return this.baselines.mainSessionFile; }
|
|
444
|
+
private get sessionRootId(): string | null { return this.baselines.sessionRootId; }
|
|
445
|
+
private get streamSink(): StreamSink | null { return this.baselines.streamSink; }
|
|
446
|
+
private get isIdleFn(): (() => boolean) | undefined { return this.baselines.isIdleFn; }
|
|
447
|
+
private get execNesting(): ExecutionNestingContext { return this.baselines.execNesting; }
|
|
448
|
+
private get rootCwd(): string { return this.baselines.rootCwd; }
|
|
449
|
+
private get uiObservability(): UiRequestObservability { return this.baselines.uiObservability; }
|
|
450
|
+
|
|
451
|
+
/** session_start 注入 pi + revive(modelRegistry/entries 归 ModelConfigService.initModel)。
|
|
452
|
+
* 本体已迁 SessionBaselines.initSession(fork/exec 嵌套基线建立、复活与跨域编排时序
|
|
453
|
+
* 逐行等价随迁);壳纯转发,对外签名不变。 */
|
|
574
454
|
initSession(init: SubagentServiceSessionInit): void {
|
|
575
|
-
this.
|
|
576
|
-
// 同步注入 pi 到 RecordStore(构造时 this.pi 为 null,session_start 后才有真实 handle)。
|
|
577
|
-
// RecordStore 跳过损坏 manifest 时调 appendEntry 上报用户可见——若不重新注入,
|
|
578
|
-
// 上报通道永远是 no-op,事故排查依然静默。
|
|
579
|
-
this.store.setPi(this.pi);
|
|
580
|
-
this.sessionId = init.sessionId;
|
|
581
|
-
// 主 session 文件按值直传(jiti 多实例下闭包缓存不可靠,见接口注释)。
|
|
582
|
-
this.mainSessionFile = init.mainSessionFile;
|
|
583
|
-
this.streamSink = init.streamSink ?? null;
|
|
584
|
-
this.isIdleFn = init.isIdle;
|
|
585
|
-
// 读取 mode(W4 守卫透传给 session-runner)+ session 级 handler 覆盖
|
|
586
|
-
//([D4-④] initSession.uiRequestHandler 是唯一注入入口;三态语义见接口注释——
|
|
587
|
-
//null = 显式清空,承载原 setUiRequestHandler(undefined) 语义)。
|
|
588
|
-
this.uiObservability.setMode(init.mode);
|
|
589
|
-
if (init.uiRequestHandler !== undefined) {
|
|
590
|
-
this.uiRequestHandler = init.uiRequestHandler ?? undefined;
|
|
591
|
-
this.uiObservability.resetMissingHandlerWarnings();
|
|
592
|
-
// [W6 R3 MF-A] session 级覆盖同步进壳侧应答端登记(三态:null = 显式清空)。
|
|
593
|
-
setHostUiRequestEndpoint(this.uiRequestHandler);
|
|
594
|
-
}
|
|
595
|
-
// SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
|
|
596
|
-
// 透传 undefined,session-runner onClose 跳过 L2 清理(仅清 L1,保留旧行为)。
|
|
597
|
-
if (init.dialogQueue !== undefined) {
|
|
598
|
-
this.dialogQueue = init.dialogQueue;
|
|
599
|
-
}
|
|
600
|
-
this.initForkDepthBaseline();
|
|
601
|
-
// [递归可见性] 跨进程身份贯穿(设计 recursive-subagent-visibility.md)。
|
|
602
|
-
// 父进程 spawn 时注入 env 描述「子进程自己的身份」(rootSessionId / selfRecordId /
|
|
603
|
-
// depth / rootCwd),语义与基线建立见 initExecContextBaseline。根进程无 env →
|
|
604
|
-
// sessionRootId = init.sessionId(自己是 root),execCtxAls 不 enterWith(顶层)。
|
|
605
|
-
const envRoot = process.env[ENV_ROOT_SESSION_ID];
|
|
606
|
-
this.sessionRootId = envRoot ?? init.sessionId;
|
|
607
|
-
this.initExecContextBaseline(envRoot, init.sessionId);
|
|
608
|
-
// revive(dispose 的逆操作:/resume /fork /new 后复活)
|
|
609
|
-
this._disposed = false;
|
|
610
|
-
// [v2 D4] settled 重扫状态随 revive 重置:新 session 的 E1 若再判「仍有 running」
|
|
611
|
-
// 可重新注册。旧 handler 闭包捕获旧 state:正常时序(session_shutdown →
|
|
612
|
-
// session_start)下已随 dispose() 惰化;未经 dispose 的时序残留仍会在 settled
|
|
613
|
-
// 边沿执行——其扫描 this.mainSessionFile 当前值(非注册时的旧文件),行为等价于
|
|
614
|
-
// 新 session 多注册一次扫描,由账本 sync-batch:<hash> 幂等 + batchFinalized 候选
|
|
615
|
-
// 过滤收敛,无跨 session 污染面。
|
|
616
|
-
this.settledRescanState = null;
|
|
617
|
-
this.store.revive();
|
|
618
|
-
this.notifyHost.revive();
|
|
619
|
-
// 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
|
|
620
|
-
// sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
|
|
621
|
-
this.recoverOrphansIfRootProcess();
|
|
622
|
-
// [W4] boot 分区 + 注册对账 sweep(须在孤儿恢复之后——依赖关系见两方法注释:
|
|
623
|
-
// 孤儿恢复把「重启前在途」record 直断 closed、把 resumable 形态保留 running 落
|
|
624
|
-
// entry,监督器重认领消费后者;sweep 再对终态 record 补发注销落盘——表 3 行 2
|
|
625
|
-
// 「注销经对账 sweep 保证落盘」的编排点)。
|
|
626
|
-
this.roundSupervisor.bootPartition();
|
|
627
|
-
runPendingReconcileSweepForService(
|
|
628
|
-
{
|
|
629
|
-
getStore: () => this.store,
|
|
630
|
-
getPi: () => this.pi,
|
|
631
|
-
getSessionRootId: () => this.sessionRootId,
|
|
632
|
-
getMainSessionFile: () => this.mainSessionFile,
|
|
633
|
-
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
634
|
-
},
|
|
635
|
-
(process.env[ENV_SELF_RECORD_ID] ?? "") !== "",
|
|
636
|
-
);
|
|
455
|
+
this.baselines.initSession(init);
|
|
637
456
|
}
|
|
638
457
|
|
|
639
458
|
/**
|
|
640
|
-
* [
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* session)→ 基线 0。enterWith 贯穿整个 session 生命周期。
|
|
459
|
+
* [F6] 当前根 session id 的只读访问——引擎接线方(SAR 等)构造 RunContext 注入
|
|
460
|
+
* `ctx.sessionRootId`(pi 引擎 relay 归属键 SESSION_ID 权威源)。initSession 后有值
|
|
461
|
+
* (根进程 = 本 session id;嵌套 = env 贯穿的真 ROOT)。[D3+] 壳终态保留面。
|
|
644
462
|
*/
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (envDepth !== undefined && envDepth !== "") {
|
|
648
|
-
const base = Number.parseInt(envDepth, 10);
|
|
649
|
-
if (!Number.isNaN(base) && base > 0) {
|
|
650
|
-
this.forkDepthAls.enterWith(base);
|
|
651
|
-
this.forkDepthBaseline = base;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
463
|
+
getSessionRootId(): string | null {
|
|
464
|
+
return this.baselines.getSessionRootId();
|
|
654
465
|
}
|
|
655
466
|
|
|
656
|
-
/**
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
*/
|
|
661
|
-
private
|
|
662
|
-
|
|
663
|
-
if (envSelfRecord !== undefined && envSelfRecord !== "") {
|
|
664
|
-
const envNestingDepth = Number.parseInt(process.env[ENV_DEPTH] ?? "0", 10);
|
|
665
|
-
const nestingDepth = Number.isNaN(envNestingDepth) ? 0 : envNestingDepth;
|
|
666
|
-
// [ALS 断裂修复] 基线兜底:enterWith 在 pi 事件回调模型下不可靠(机制注释见
|
|
667
|
-
// common/nesting-guard.ts ExecutionNestingContext),基线是 createRecordForMode /
|
|
668
|
-
// 护栏读 ALS store 失败时的权威回退。
|
|
669
|
-
this.execNesting.setBaseline({ recordId: envSelfRecord, depth: nestingDepth });
|
|
670
|
-
this.execNesting.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
|
|
671
|
-
if (process.env.XYZ_AGENT_DEBUG) {
|
|
672
|
-
logger.debug(
|
|
673
|
-
`[subagents] execNesting initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? sessionId}`,
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
467
|
+
/** UI streaming sink 只读访问(workflow 域消费)。[D3+] 壳终态保留面。 */
|
|
468
|
+
getStreamSink(): StreamSink | null { return this.baselines.getStreamSink(); }
|
|
469
|
+
|
|
470
|
+
/** [D4 下沉] 就绪断言本体已迁聚合(late-bound getter 现读 pi/disposed,错误文案不变);
|
|
471
|
+
* 壳 9 个调用点经此转发。 */
|
|
472
|
+
private assertReady(): void {
|
|
473
|
+
this.baselines.assertReady();
|
|
677
474
|
}
|
|
475
|
+
// ── 域 #5 SyncCollectDomain 聚合转发(R2 抽取;本体 execution/service/sync-collect-domain.ts)──
|
|
678
476
|
|
|
679
|
-
/**
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
* 非空)——与 execNesting 基线同源。根进程恢复语义不变。
|
|
691
|
-
*/
|
|
692
|
-
private recoverOrphansIfRootProcess(): void {
|
|
693
|
-
const isChildProcess = (process.env[ENV_SELF_RECORD_ID] ?? "") !== "";
|
|
694
|
-
if (!isChildProcess) {
|
|
695
|
-
this.recoverOrphanRecords();
|
|
696
|
-
} else if (process.env.XYZ_AGENT_DEBUG) {
|
|
697
|
-
logger.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
|
|
698
|
-
}
|
|
477
|
+
/** [R2] 域 #5 聚合实例:sync 批自闭合语义(collectCoordinator 装配 + E9 dispose 转账 +
|
|
478
|
+
* E1 崩溃恢复 + settled 有界重扫 + collectSync 配置读取)的唯一宿主与唯一写者
|
|
479
|
+
* (r0-inventory 清单① #26-#28)。deps 全晚绑定闭包(装配见构造器;检查点①
|
|
480
|
+
* flushBatch 显式依赖注入随聚合落地),壳经下方 getter/方法透传,对外签名零变化。 */
|
|
481
|
+
private readonly syncCollect: SyncCollectDomain;
|
|
482
|
+
|
|
483
|
+
// collectCoordinator 读路径透传(R1 打样模式 2·strangler 转发壳):壳内既有 route
|
|
484
|
+
// 调用点(#11 cancel / #14 引擎编排 / Continuation 装配)零改动;无写路径(构造期
|
|
485
|
+
// readonly 装配,聚合单写者)。
|
|
486
|
+
private get collectCoordinator(): CollectCoordinator {
|
|
487
|
+
return this.syncCollect.collectCoordinator;
|
|
699
488
|
}
|
|
700
489
|
|
|
701
|
-
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
490
|
+
/** [E1] sync 批崩溃恢复(index.ts session_start 恢复编排处调用)。本体已迁
|
|
491
|
+
* SyncCollectDomain(扫描/补发/落标/settled 重扫时序逐行等价随迁);壳纯转发,
|
|
492
|
+
* 对外签名不变。 */
|
|
493
|
+
recoverSyncCollectBatch(): Promise<void> {
|
|
494
|
+
return this.syncCollect.recoverSyncCollectBatch();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** collectSync.default 当前生效值(startHandler 缺省 collect 解析用;本体与配置读取
|
|
498
|
+
* 链已迁聚合)。[D3+] 壳终态保留面(subagent-actions-core 消费)。 */
|
|
499
|
+
getCollectSyncDefault(): "async" | "sync" {
|
|
500
|
+
return this.syncCollect.getCollectSyncDefault();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── 域 #3/#4/#8/#10/#11/#13/#17/#18 RecordAccess + RecordLifecycle 聚合转发(R3 抽取;
|
|
504
|
+
// 本体 execution/service/record-access.ts + record-lifecycle.ts)──
|
|
505
|
+
|
|
506
|
+
/** [R3] 读建面聚合实例(域 #3 孤儿恢复 / #8 查询面 / #10 action 网关 / #13 身份解析与
|
|
507
|
+
* record 创建的唯一宿主)。deps 全晚绑定闭包(装配见构造器),壳经下方同名方法
|
|
508
|
+
* 透传,对外签名零变化。[D-R3-1] 与 RecordLifecycle 拆分的计划变更见聚合文件头。 */
|
|
509
|
+
private readonly recordAccess: RecordAccess;
|
|
510
|
+
|
|
511
|
+
/** [R3] 终态迁移写面聚合实例(域 #4 回收面 / #11 close 三路 / #17 cancel / #18
|
|
512
|
+
* finalize 簇的唯一宿主——D5「store 与终态迁移入口的唯一宿主」,H4 落点)。
|
|
513
|
+
* deps 全晚绑定闭包(装配见构造器;C-5 跨域汇聚经回调、collectCoordinator 经 #5
|
|
514
|
+
* 显式投影现读),壳经下方同名方法透传,对外签名零变化。 */
|
|
515
|
+
private readonly recordLifecycle: RecordLifecycle;
|
|
516
|
+
|
|
517
|
+
// ── 域 #3 孤儿/manifest 恢复 聚合转发(R3 抽取;本体 execution/service/record-access.ts)──
|
|
518
|
+
|
|
519
|
+
/** [R3] 孤儿终态恢复编排入口(baselines deps.recoverOrphans 回调消费点——initSession
|
|
520
|
+
* 复活后编排,时序逐行等价随迁读建面);壳纯转发,签名不变。 */
|
|
521
|
+
private recoverOrphansIfRootProcess(): void {
|
|
522
|
+
this.recordAccess.recoverOrphansIfRootProcess();
|
|
723
523
|
}
|
|
724
524
|
|
|
725
|
-
/**
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
525
|
+
/** 启动恢复:manifest tmp 残留清扫(ADR-035;index.ts session_start 每次都调)。
|
|
526
|
+
* [U4c / D6] tmp 恢复已退役为**静默删除**——manifest 现为可丢可重建缓存(权威
|
|
527
|
+
* = `.state`,重建 = rebuildIndexes),promote 半写 tmp 的恢复语义失效;本入口
|
|
528
|
+
* 只清残留(含 0 字节/半写形态)。本体已迁 RecordAccess(行为随 ManifestStore
|
|
529
|
+
* 退役语义);壳纯转发,对外签名不变。 */
|
|
729
530
|
async recoverManifestTmpFiles(): Promise<{ deleted: number; recovered: number }> {
|
|
730
|
-
|
|
731
|
-
return await this.manifestStore.recoverTmpFiles();
|
|
732
|
-
} catch (err) {
|
|
733
|
-
bestEffort(err, "recoverManifestTmpFiles", "error");
|
|
734
|
-
return { deleted: 0, recovered: 0 };
|
|
735
|
-
}
|
|
531
|
+
return this.recordAccess.recoverManifestTmpFiles();
|
|
736
532
|
}
|
|
737
533
|
|
|
738
|
-
/**
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* [T2⑥ / PS-1] 补齐三回收面(对照 dispose() 的既有形态,消除同文件双标):
|
|
744
|
-
* controller.abort + kill(收敛到 killChildWithEscalation)+ disarmIdleTimer +
|
|
745
|
-
* disarmSettledWatchdog。旧实现只关 record 不中止执行——「record 已关」≠「执行已
|
|
746
|
-
* 处置」:在途子进程继续跑且无任何用户可及的取消通道(cancel 只查内存 running,
|
|
747
|
-
* archive 后恒 false),若挂死唯一上界是默认关闭的 spawn watchdog → 泄漏至宿主退出。
|
|
748
|
-
* abort/kill/timer 三面对已终态/已死 record 均幂等 no-op,dispose() 先行的
|
|
749
|
-
* abortRunningControllers + killAllSpawnedChildren 不受影响(parent-shutdown 路径
|
|
750
|
-
* 双保险)。
|
|
751
|
-
*
|
|
752
|
-
* [v4 A-6] 旧实现的 recentlyCascaded 收集(供已删除的 before_agent_start 注入告知)
|
|
753
|
-
* 与 drainCascaded 已一并移除——被关 record 的告知改由 list 的 closedReason 表达。
|
|
754
|
-
*
|
|
755
|
-
* @param reason 关闭原因(parent-fork / parent-new / parent-shutdown)
|
|
756
|
-
* @returns 被关闭的 record 数量
|
|
534
|
+
/**
|
|
535
|
+
* [U4c / G1] 缓存降级重建通道(boot 全量腿):manifest 与 sessions-index 均为
|
|
536
|
+
* 可丢缓存,本入口在 boot revive 完成后(initSession 的孤儿恢复/重物化已收敛
|
|
537
|
+
* 磁盘态)全量重建——幂等补缺(幸存 manifest 不覆写),失败静默降级不抛。
|
|
538
|
+
* 语义细节见 RecordStore.rebuildIndexes(D5 三要素)。壳纯转发。
|
|
757
539
|
*/
|
|
540
|
+
rebuildIndexes(): number {
|
|
541
|
+
return this.store.rebuildIndexes();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── 域 #4 回收面 聚合转发(R3 抽取;本体 execution/service/record-lifecycle.ts)──
|
|
545
|
+
// [被否谱系 #3] 回收面显式归 RecordLifecycle——disposeAllRecords/onParentFork/
|
|
546
|
+
// onParentNew 编排性关闭 + GC timer 随终态写面迁移;壳只保留时序编排(检查点③:
|
|
547
|
+
// dispose 调用顺序与现状逐行等价,E9 先于批量 archive)。promoteSessionFileFromEngine
|
|
548
|
+
// Handle(A 通道写点,清单②#1)/stopIdleGc/stopGcTimer 为聚合内部互调成员,壳内零
|
|
549
|
+
// 消费 → 零转发(R2 打样模式 2)。
|
|
550
|
+
|
|
551
|
+
/** SP-4: 关闭所有活跃 record(dispose 编排 parent-shutdown 路径 + D3 对外面)。
|
|
552
|
+
* 本体已迁 RecordLifecycle(三回收面 + manifest 补写 + 锚点提升时序逐行等价随迁);
|
|
553
|
+
* 壳纯转发,对外签名不变。 */
|
|
758
554
|
disposeAllRecords(reason: ClosedReason): number {
|
|
759
|
-
|
|
760
|
-
let count = 0;
|
|
761
|
-
for (const record of activeRecords) {
|
|
762
|
-
// [T2⑥/PS-1] 回收面 i:abort 在途 controller(排队的 acquire / 在途 signal listener
|
|
763
|
-
// 立即感知取消)。幂等:已 aborted 的 controller.abort() 是 no-op。
|
|
764
|
-
record.controller?.abort();
|
|
765
|
-
// 回收面 ii:回收子进程(SIGTERM + 30s SIGKILL 升级,收敛 T2④同款)。
|
|
766
|
-
// 回收面 iii:disarm idle timer + settled watchdog(进程回收后 timer 只会误触发)。
|
|
767
|
-
killRecordChildWithEscalation(record.id, `disposeAllRecords (${reason})`);
|
|
768
|
-
disarmIdleTimer(record.id);
|
|
769
|
-
disarmSettledWatchdog(record.id);
|
|
770
|
-
// tryTransition 只对 running 生效;idle 需要直接 completeRecord(无 CAS 保护)。
|
|
771
|
-
// 与 closeChatIdle 对称:idle 无在途 AgentResult,构造合成 result。
|
|
772
|
-
if (record.status === "running") {
|
|
773
|
-
if (!tryTransition(record, "closed", reason)) continue;
|
|
774
|
-
}
|
|
775
|
-
const result: AgentResult = {
|
|
776
|
-
text: "",
|
|
777
|
-
turns: record.turnCount,
|
|
778
|
-
durationMs: Date.now() - record.startedAt,
|
|
779
|
-
success: false,
|
|
780
|
-
error: `closed due to ${reason}`,
|
|
781
|
-
sessionId: record.id,
|
|
782
|
-
toolCalls: [],
|
|
783
|
-
};
|
|
784
|
-
completeRecord(record, result, "closed", reason);
|
|
785
|
-
this.store.archive(record);
|
|
786
|
-
// [F-5 修复] 本路径不经 doFinalizeRecord(编排性关闭直连 completeRecord+archive),
|
|
787
|
-
// chat 轮路由注销在此补齐(幂等;闭包持 record/stream 引用,防泄漏)。
|
|
788
|
-
this.unregisterChatRoundRoute(record.id);
|
|
789
|
-
// worktree 绑定清理(T3)。cleanup 已 async 化——同步签名(返回计数)不变,
|
|
790
|
-
// 清理 fire-and-forget:失败经 bestEffort 留痕,不阻塞/不影响计数返回。
|
|
791
|
-
if (record.worktreeHandle) {
|
|
792
|
-
void this.worktreeManager.cleanup(record.worktreeHandle).catch((err: unknown) => {
|
|
793
|
-
bestEffort(err, `worktree cleanup (${reason})`);
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
// pending-notifications 注销
|
|
797
|
-
this.notifyHost.emitPendingUnregister(record.id, "closed");
|
|
798
|
-
count++;
|
|
799
|
-
}
|
|
800
|
-
return count;
|
|
555
|
+
return this.recordLifecycle.disposeAllRecords(reason);
|
|
801
556
|
}
|
|
802
557
|
|
|
803
|
-
/** SP-4: /fork 新 session 时清理旧 record
|
|
804
|
-
*
|
|
558
|
+
/** SP-4: /fork 新 session 时清理旧 record(index.ts session_before_fork handler 触发)。
|
|
559
|
+
* 本体已迁 RecordLifecycle;壳纯转发,对外签名不变。 */
|
|
805
560
|
onParentFork(): number {
|
|
806
|
-
return this.
|
|
561
|
+
return this.recordLifecycle.onParentFork();
|
|
807
562
|
}
|
|
808
563
|
|
|
809
|
-
/** SP-4: /new 创建全新 session 时清理旧 record
|
|
810
|
-
*
|
|
811
|
-
* (reason==="new")handler 触发。 */
|
|
564
|
+
/** SP-4: /new 创建全新 session 时清理旧 record(index.ts session_before_switch 触发)。
|
|
565
|
+
* 本体已迁 RecordLifecycle;壳纯转发,对外签名不变。 */
|
|
812
566
|
onParentNew(): number {
|
|
813
|
-
return this.
|
|
567
|
+
return this.recordLifecycle.onParentNew();
|
|
814
568
|
}
|
|
815
569
|
|
|
816
|
-
/**
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
/** 启动 idle record GC 定时器(session_start 调用,幂等)。
|
|
820
|
-
* [W4] WorkflowRun store(FileRunStore)同批纳入:running 且 startedAt 超 30 天
|
|
821
|
-
* 锚窗的 run 终态化归档(只终态化不补注销,见 idle-gc.ts 头注)。宿主未
|
|
822
|
-
* configureCore 时 loadAll 抛错由 idle-gc 内部吞掉(单轮跳过)。
|
|
823
|
-
* [F-1 修复] stateDir 与 pi 壳 JsonlRunStore 落盘布局同源
|
|
824
|
-
* (resolvePiWorkflowStateDir → <sessionDir>/workflow-state/)——缺省 dataRoot 根
|
|
825
|
-
* 与 pi 生产落盘不相交,WorkflowRun GC 曾恒空转(W4 引入的装配错位)。 */
|
|
570
|
+
/** 启动 idle record GC 定时器(session_start 调用,幂等)。本体已迁 RecordLifecycle
|
|
571
|
+
* (stopIdleGc 句柄为聚合唯一写者字段);壳纯转发,对外签名不变。 */
|
|
826
572
|
startGcTimer(): void {
|
|
827
|
-
|
|
828
|
-
this.stopIdleGc = startIdleGc(this.store, new FileRunStore({ stateDir: resolvePiWorkflowStateDir() }));
|
|
573
|
+
this.recordLifecycle.startGcTimer();
|
|
829
574
|
}
|
|
830
575
|
|
|
831
|
-
/** 停止 idle record GC 定时器(dispose
|
|
576
|
+
/** 停止 idle record GC 定时器(dispose 编排消费点)。壳纯转发。 */
|
|
832
577
|
private stopGcTimer(): void {
|
|
833
|
-
this.
|
|
834
|
-
this.stopIdleGc = undefined;
|
|
578
|
+
this.recordLifecycle.stopGcTimer();
|
|
835
579
|
}
|
|
836
580
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
* 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
|
|
843
|
-
/** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
|
|
844
|
-
* 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
|
|
845
|
-
* → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
|
|
846
|
-
* sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
|
|
847
|
-
private getCollectSyncBudget(): BatchBudgetParams | undefined {
|
|
848
|
-
const cs = this.collectSyncSection();
|
|
849
|
-
return cs !== undefined ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : undefined;
|
|
850
|
-
}
|
|
581
|
+
// ── 域 #13 身份解析/record 创建(R3 抽取;本体 execution/service/record-access.ts)──
|
|
582
|
+
// [R4] resolveIdentity / resolveIdentityForEngine / createRecordForMode /
|
|
583
|
+
// buildEarlyFailedHandle 四转发已删(R3 过渡期消费方 = R4 域派发链,已随域迁
|
|
584
|
+
// RunOrchestration/WorkflowDispatch,经 deps 装配闭包直指 recordAccess 聚合——
|
|
585
|
+
// 壳内与全仓零剩余消费,删转发判据满足)。
|
|
851
586
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
*
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
* (详见 flushBatch 闭包注释)。 */
|
|
861
|
-
private markMembersBatchFinalized(memberIds: readonly string[]): void {
|
|
862
|
-
for (const id of memberIds) {
|
|
863
|
-
const full = this.store.getFullRecord(id);
|
|
864
|
-
if (!full) continue;
|
|
865
|
-
this.appendBatchFinalizedEntry(full);
|
|
866
|
-
// 反查索引缺失只影响指针行反查(session-reader 错误文案已指引绝对路径兜底),
|
|
867
|
-
// 不构成落标失败(与屏障路径的 best-effort 语义同源,仅无时序保证)。
|
|
868
|
-
void this.writeBatchMemberManifest(full).catch((err: unknown) => {
|
|
869
|
-
logger.debug(
|
|
870
|
-
`[subagents] batch-finalized manifest write failed (record=${full.id})`,
|
|
871
|
-
{ reason: err instanceof Error ? err.message : String(err) },
|
|
872
|
-
);
|
|
873
|
-
});
|
|
874
|
-
}
|
|
587
|
+
// ── 域 #8 查询面 聚合转发(R3 抽取;本体 execution/service/record-access.ts)──
|
|
588
|
+
// 消费方 = queries 聚合面(D3 壳保留)+ Continuation handlers(R4 领地),壳保留
|
|
589
|
+
// 转发方法 → 调用点零改动。
|
|
590
|
+
|
|
591
|
+
/** 按 id 查内存 running record 的只读快照(G3-002 修复)。本体已迁 RecordAccess
|
|
592
|
+
* (assertReady 就绪门经 deps 现读);壳纯转发,签名不变。 */
|
|
593
|
+
private findRecord(id: string): RecordSnapshot | undefined {
|
|
594
|
+
return this.recordAccess.findRecord(id);
|
|
875
595
|
}
|
|
876
596
|
|
|
877
|
-
/**
|
|
878
|
-
*
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
*
|
|
882
|
-
* [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
|
|
883
|
-
* 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
|
|
884
|
-
* records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
|
|
885
|
-
* 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
|
|
886
|
-
* await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
|
|
887
|
-
* fire-and-forget(async 单条通知无指针行消费,无时序要求)。
|
|
888
|
-
* rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
|
|
889
|
-
* 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
|
|
890
|
-
* task/slug/parentRecordId 等可选 undefined 自然缺省。 */
|
|
891
|
-
private appendBatchFinalizedEntry(rec: SubagentRecord): void {
|
|
892
|
-
this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
|
|
597
|
+
/** [v8.5 A1/B] 全态查找(message 拒绝文案分流 + fork-from 源解析共用)。本体已迁
|
|
598
|
+
* RecordAccess;壳纯转发。 */
|
|
599
|
+
private lookupRecordAnyState(id: string): SubagentRecord | undefined {
|
|
600
|
+
return this.recordAccess.lookupRecordAnyState(id);
|
|
893
601
|
}
|
|
894
602
|
|
|
895
|
-
/**
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
* 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
|
|
899
|
-
* writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
|
|
900
|
-
* fire-and-forget catch(debug 不阻断落标)。 */
|
|
901
|
-
private writeBatchMemberManifest(rec: SubagentRecord): Promise<void> {
|
|
902
|
-
return this.manifestStore.writeManifest({
|
|
903
|
-
id: rec.id,
|
|
904
|
-
rootSessionId: rec.rootSessionId ?? "",
|
|
905
|
-
parentRecordId: rec.parentRecordId,
|
|
906
|
-
agentName: rec.agent,
|
|
907
|
-
status: rec.status,
|
|
908
|
-
createdAt: rec.startedAt,
|
|
909
|
-
completedAt: rec.endedAt,
|
|
910
|
-
sessionFile: rec.sessionFile,
|
|
911
|
-
task: rec.task,
|
|
912
|
-
slug: rec.slug,
|
|
913
|
-
model: rec.model,
|
|
914
|
-
});
|
|
603
|
+
/** 订阅 store 变更(widget/list requestRender)。本体已迁 RecordAccess;壳纯转发。 */
|
|
604
|
+
private onChange(listener: () => void): () => void {
|
|
605
|
+
return this.recordAccess.onChange(listener);
|
|
915
606
|
}
|
|
916
607
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
*
|
|
922
|
-
private
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
`[subagents] batch-finalized manifest write failed (record=${recs[i]!.id}, manifest=${this.recordsDir}/${recs[i]!.id}.json)`,
|
|
929
|
-
{ reason: result.reason instanceof Error ? result.reason.message : String(result.reason) },
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
608
|
+
// [D4] listRunning 已删除:零生产调用方(TUI 计数经 collectRecords / notify-host 的
|
|
609
|
+
// piAdapter 直调 store.listRunning 覆盖),唯一消费是初始空态单测——保留 store 层方法。
|
|
610
|
+
|
|
611
|
+
/** 合并内存 + 磁盘 record(/subagents list + tool list 消费,按 rootSessionId 过滤)。
|
|
612
|
+
* 本体已迁 RecordAccess;壳纯转发(默认参数在聚合侧保持同值)。 */
|
|
613
|
+
private collectRecords(
|
|
614
|
+
limit: number,
|
|
615
|
+
statusFilter: StatusFilter = "all",
|
|
616
|
+
includeWorkflow: boolean = false,
|
|
617
|
+
): SubagentRecord[] {
|
|
618
|
+
return this.recordAccess.collectRecords(limit, statusFilter, includeWorkflow);
|
|
933
619
|
}
|
|
934
620
|
|
|
935
|
-
/** [
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
* 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
|
|
939
|
-
* 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
|
|
940
|
-
* 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
|
|
941
|
-
private convertPendingSyncBufferToAsync(): void {
|
|
942
|
-
// [U8 拆批修复·E9 交互] 挂起的合批排程先取消——dispose 已选「放弃攒批转 async」
|
|
943
|
-
// 语义(最简语义 = 取消而非同步 flush):放任排程触发会让 flushBatch(批 hash
|
|
944
|
-
// 通道)与下方逐条 notify(async id 通道)双通道并发写账 → 同成员双投递,且触发
|
|
945
|
-
// 点可能落在 notifier/store dispose 之后。取消后缓冲原样保留,本函数既有单通路
|
|
946
|
-
// 完整接管(协调器侧幂等:无排程时 no-op)。
|
|
947
|
-
this.collectCoordinator.cancelScheduledFlush();
|
|
948
|
-
const members = this.collectCoordinator.pendingMembers();
|
|
949
|
-
if (members.length === 0) return;
|
|
950
|
-
for (const member of members) {
|
|
951
|
-
this.e9ConvertedIds.add(member.id);
|
|
952
|
-
this.notifyHost.notify(member);
|
|
953
|
-
}
|
|
954
|
-
this.markMembersBatchFinalized(members.map((m) => m.id));
|
|
955
|
-
logger.warn(
|
|
956
|
-
`[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
|
|
957
|
-
{ ids: members.map((m) => m.id) },
|
|
958
|
-
);
|
|
621
|
+
/** [perf] 单 record 详情懒加载。本体已迁 RecordAccess;壳纯转发。 */
|
|
622
|
+
private getFullRecord(id: string): SubagentRecord | undefined {
|
|
623
|
+
return this.recordAccess.getFullRecord(id);
|
|
959
624
|
}
|
|
960
625
|
|
|
626
|
+
// ── 域 #10 action 网关 聚合转发(R3 抽取;本体 execution/service/record-access.ts)──
|
|
627
|
+
// coldLookupDeps 字段(清单① #30,唯一消费方 getRecordForAction 冷查分支)为聚合
|
|
628
|
+
// 内部成员,壳内零消费 → 零转发;壳只保留 getRecordForAction 转发(chatActions 面 +
|
|
629
|
+
// piEngineServiceAdapter + Continuation handlers 消费点)。
|
|
630
|
+
|
|
961
631
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
* - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
|
|
966
|
-
* collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
|
|
967
|
-
* 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
|
|
968
|
-
* - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
|
|
969
|
-
* 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
|
|
970
|
-
* - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
|
|
971
|
-
* 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
|
|
972
|
-
* entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
|
|
973
|
-
* 「已处理」);
|
|
974
|
-
* - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
|
|
975
|
-
* 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
|
|
976
|
-
* 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
|
|
977
|
-
* running 口径与协调器同构
|
|
978
|
-
* (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
|
|
979
|
-
* - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
|
|
980
|
-
* 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
|
|
981
|
-
* 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
|
|
982
|
-
*
|
|
983
|
-
* async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
|
|
984
|
-
* session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
|
|
985
|
-
* 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
|
|
986
|
-
*
|
|
987
|
-
* [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
|
|
988
|
-
* dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
|
|
989
|
-
* 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
|
|
632
|
+
* 按 id 查 record 并做归属校验(message/close action 的统一入口;含 SP-2 冷查复活
|
|
633
|
+
* 与 v4 A-5 直接父校验)。本体已迁 RecordAccess;壳纯转发,签名不变。
|
|
990
634
|
*/
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const { outcome } = await this.runSyncCollectRecoveryScan();
|
|
994
|
-
// [v2 D4] 断链 4:等待分支不再死等——挂 settled 有界重扫(幂等单注册)。
|
|
995
|
-
if (outcome === "waiting") {
|
|
996
|
-
this.armSettledRescan();
|
|
997
|
-
}
|
|
998
|
-
} catch (err) {
|
|
999
|
-
// 与 index.ts 调用点原 try/catch 的 warn 容错同语义(该处 catch 对 async 化后
|
|
1000
|
-
// 的 promise 异常兑现不到,容错收敛到本方法内部)。
|
|
1001
|
-
logger.warn("[subagents] sync collect batch recovery failed", {
|
|
1002
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
635
|
+
private getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord {
|
|
636
|
+
return this.recordAccess.getRecordForAction(id, opts);
|
|
1005
637
|
}
|
|
1006
638
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
outcome: "idle" | "waiting" | "dispatched";
|
|
1014
|
-
waitingIds: string[];
|
|
1015
|
-
}> {
|
|
1016
|
-
const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
|
|
1017
|
-
if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
1018
|
-
const rootFilter = this.sessionRootId;
|
|
1019
|
-
const candidates = lastRecords.filter(
|
|
1020
|
-
(r) =>
|
|
1021
|
-
r.collectMode === "sync" &&
|
|
1022
|
-
r.batchFinalized !== true &&
|
|
1023
|
-
(rootFilter === undefined || r.rootSessionId === rootFilter),
|
|
1024
|
-
);
|
|
1025
|
-
if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
|
|
1026
|
-
// [v2 D3] 与协调器 hasRunningSync 同构口径(collect-coordinator.ts):running+
|
|
1027
|
-
// resumable 视为已完成、不阻止补发——成功成员崩溃时的末条 entry 恒为轮终
|
|
1028
|
-
// running+resumable(SP-5 有意语义),旧口径只看 status !== "closed" 会把主场景
|
|
1029
|
-
// (批内含成功成员)顶死在「等自然终态」永不补发(v2 §2.3 断链 3)。
|
|
1030
|
-
const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
|
|
1031
|
-
if (running.length > 0) {
|
|
1032
|
-
logger.debug(
|
|
1033
|
-
`[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
|
|
1034
|
-
{ ids: running.map((r) => r.id) },
|
|
1035
|
-
);
|
|
1036
|
-
return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
|
|
1037
|
-
}
|
|
1038
|
-
// [时序屏障] manifest 先于写账 await 全部落盘——E1 补发同样是批通知(指针行消费
|
|
1039
|
-
// 依赖反查索引),「通知可达 ⇒ 索引就位」的构造性保证与 flushBatch 同款。
|
|
1040
|
-
await this.writeSyncBatchManifestBarrier(candidates);
|
|
1041
|
-
// 全员终态:单条批补发(budget 热读与 flushBatch 同源);账本同 hash 幂等拒绝也算
|
|
1042
|
-
// 已投递(批已在账/已销账,重放由账本承接)——两种结局统一补标。
|
|
1043
|
-
const members = candidates.map((r) => syncRebuildToNotifyMember(r));
|
|
1044
|
-
const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
|
|
1045
|
-
for (const rec of candidates) {
|
|
1046
|
-
this.appendBatchFinalizedEntry(rec);
|
|
1047
|
-
}
|
|
1048
|
-
logger.warn(
|
|
1049
|
-
`[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
|
|
1050
|
-
{ ids: members.map((m) => m.id) },
|
|
1051
|
-
);
|
|
1052
|
-
return { outcome: "dispatched", waitingIds: [] };
|
|
1053
|
-
}
|
|
639
|
+
// ── 域 #11 close 三路 聚合转发(R3 抽取;本体 execution/service/record-lifecycle.ts)──
|
|
640
|
+
// 消费点:closeSubagent(chatActions 面 + piEngineServiceAdapter + Continuation
|
|
641
|
+
// handlers 装配回调)。cancelBackground / closeChatIdle 为聚合内部互调成员或已随
|
|
642
|
+
// R4 域迁走(closeChatIdle 的 R3 过渡转发已删——消费方 continuationFor closeNow
|
|
643
|
+
// 回调随域迁 RunOrchestration,经 deps 直指 recordLifecycle;壳内与全仓零剩余
|
|
644
|
+
// 消费,删转发判据满足)。
|
|
1054
645
|
|
|
1055
|
-
/**
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
* pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
|
|
1063
|
-
* index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
|
|
1064
|
-
* pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
|
|
1065
|
-
* extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
|
|
1066
|
-
* extension 的全部 handler 逐一 await;故本注册与 ledger host 经
|
|
1067
|
-
* piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
|
|
1068
|
-
private armSettledRescan(): void {
|
|
1069
|
-
if (this.settledRescanState !== null) return;
|
|
1070
|
-
const state = { disposed: false, scans: 0 };
|
|
1071
|
-
this.settledRescanState = state;
|
|
1072
|
-
this.pi?.on?.("agent_settled", async () => {
|
|
1073
|
-
if (state.disposed) return;
|
|
1074
|
-
state.scans += 1;
|
|
1075
|
-
// await 完整补发序列(manifest 屏障 → 写账 → 落标):pi emit 对 handler 逐一
|
|
1076
|
-
// await(P-settled 定谳,0.84.4 dist runner.js),async 化不改变分发语义。
|
|
1077
|
-
const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
|
|
1078
|
-
if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
|
|
1079
|
-
state.disposed = true;
|
|
1080
|
-
if (outcome === "waiting") {
|
|
1081
|
-
// D6 #7b / SC-2:warn + 滞留成员 id(debug 级排障不可见——达限即批通知挂起至下次 session_start)
|
|
1082
|
-
logger.warn(
|
|
1083
|
-
`[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
|
|
1084
|
-
{ ids: waitingIds },
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
646
|
+
/**
|
|
647
|
+
* close action 的统一行为分流(running 子态 × force:chatMode abort+清队+立即终态 /
|
|
648
|
+
* one-shot closeAfterRound 挂起 / force 走 cancelBackground)。本体已迁 RecordLifecycle;
|
|
649
|
+
* 壳纯转发,签名不变。
|
|
650
|
+
*/
|
|
651
|
+
private closeSubagent(record: ExecutionRecord, force: boolean): Promise<void> {
|
|
652
|
+
return this.recordLifecycle.closeSubagent(record, force);
|
|
1088
653
|
}
|
|
1089
654
|
|
|
1090
|
-
// [
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
// patchFile 透传。调用点:runSyncCollectRecoveryScan。
|
|
655
|
+
// [H1 U6] closeAfterRoundSettled([M5] chat 域「轮完成时终态化」消费面)已随 chat 域
|
|
656
|
+
// closeAfterRound 挂起标志退役删除:D4 close = abort 在途 + 清空队列 + 立即终态化
|
|
657
|
+
//(closeChatIdle),不等轮终;one-shot 域的 closeAfterRound 消费走 consumeCloseAfterRound
|
|
658
|
+
//(settleOneShotOutcome,照旧)。
|
|
1095
659
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
// 且不写账,而 E1 dispatched 段不判 accepted 仍统一落标 → 批被标 batchFinalized
|
|
1103
|
-
// 而通知从未写账(永久丢失,不可逆)。惰化后通知由下次重启的 E1 首扫兑现
|
|
1104
|
-
// (成员无标记,候选可达)。与 initSession revive 重置不冲突:dispose 是终态置
|
|
1105
|
-
// disposed,revive 置 null 是新 session 的重新注册,旧 state 对象随旧 handler
|
|
1106
|
-
// 闭包保持 disposed 永久惰化。
|
|
1107
|
-
if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
|
|
1108
|
-
// [dispose stub] 第一时间换 stub,防 trailing ui_request 调到 stale handler 闭包
|
|
1109
|
-
// (仍持有 disposed session 的 ctx)产生误导性 console.error。stub 干净降级为 cancelled。
|
|
1110
|
-
// 必须在 emit/abort 之前——这些步骤可能同步触发 trailing pump。
|
|
1111
|
-
// [D4-④] 原 setUiRequestHandler 方法已删(initSession 参数为唯一注入入口),
|
|
1112
|
-
// 此处内联其方法体(赋值 + 缺失告警去重重置)。
|
|
1113
|
-
this.uiRequestHandler = disposedUiRequestStub;
|
|
1114
|
-
this.uiObservability.resetMissingHandlerWarnings();
|
|
1115
|
-
// [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
|
|
1116
|
-
// cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
|
|
1117
|
-
setHostUiRequestEndpoint(disposedUiRequestStub);
|
|
1118
|
-
// [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
|
|
1119
|
-
// abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
|
|
1120
|
-
this.store.abortRunningControllers();
|
|
1121
|
-
killAllSpawnedChildren();
|
|
1122
|
-
// [W3] chat 反向通道路由全量注销(inproc 轮次交接包清空的协议形态替位)。
|
|
1123
|
-
for (const recordId of [...this.chatRoundRoutes.keys()]) {
|
|
1124
|
-
this.unregisterChatRoundRoute(recordId);
|
|
1125
|
-
}
|
|
1126
|
-
// [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
|
|
1127
|
-
// 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
|
|
1128
|
-
// running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
|
|
1129
|
-
// 冷 idToFile(无目录扫描则 miss → 跳过落标 → E1 重建误收已转换成员);先转换取
|
|
1130
|
-
// 内存命中,与 flushBatch 出口①同款通路。同样在 flushPendingNotifications 之前
|
|
1131
|
-
//(转换条目加入本次 flush)与 notifier/store dispose 之前(写账与 appendEntry
|
|
1132
|
-
// 通道仍可用)。仍在跑成员不在此处理——后续 disposeAllRecords 按现有退出路径关闭。
|
|
1133
|
-
this.convertPendingSyncBufferToAsync();
|
|
1134
|
-
// SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
|
|
1135
|
-
// 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
|
|
1136
|
-
this.disposeAllRecords("parent-shutdown");
|
|
1137
|
-
// [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
|
|
1138
|
-
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
1139
|
-
this.resumesInFlight.clear();
|
|
1140
|
-
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
1141
|
-
this.notifyHost.flushPendingNotifications();
|
|
1142
|
-
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
1143
|
-
// 复查失败时 pending 挂在 ledger 内存态,随后 dispose 销毁——旧注释「flush 后 dispose
|
|
1144
|
-
//(防丢失)」的承诺与行为不符。现把未投递 pending 经 ledger entry 通道复写落盘
|
|
1145
|
-
// (notifyId 幂等,重复 entry 由恢复扫描后写覆盖吸收),重启 recoverFromSession 按
|
|
1146
|
-
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
1147
|
-
this.persistUndeliveredNotificationsForReplay();
|
|
1148
|
-
this.notifyHost.dispose();
|
|
1149
|
-
// [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
|
|
1150
|
-
// 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
|
|
1151
|
-
this.roundSupervisor.dispose();
|
|
1152
|
-
this.store.dispose();
|
|
660
|
+
// ── 域 #17 取消 聚合转发(R3 抽取;本体 execution/service/record-lifecycle.ts)──
|
|
661
|
+
|
|
662
|
+
/** 取消 background record(tryTransition CAS 抢锁防重复副作用)。本体已迁
|
|
663
|
+
* RecordLifecycle;壳纯转发,对外签名不变(D3 壳终态保留面)。 */
|
|
664
|
+
cancel(id: string): boolean {
|
|
665
|
+
return this.recordLifecycle.cancel(id);
|
|
1153
666
|
}
|
|
1154
667
|
|
|
668
|
+
// ── 域 #6/#7/#12/#14/#15 RunOrchestration + WorkflowDispatch 聚合转发(R4 抽取;
|
|
669
|
+
// 本体 execution/service/run-orchestration.ts + workflow-dispatch.ts,[D-R4-1] 拆分
|
|
670
|
+
// 边界与偏差登记见聚合文件头)──
|
|
671
|
+
|
|
672
|
+
/** [R4] 域 #6-#15 核心编排聚合实例:run 域执行编排(execute/executeAndAwait 入口、
|
|
673
|
+
* 引擎编排 + adopt 分诊、settleOneShotOutcome 终态收口、Continuation 协作面 +
|
|
674
|
+
* continuations 队列(C-4/C-5 宿主)、pool/worktree 资源)。deps 全晚绑定闭包
|
|
675
|
+
* (装配见构造器),壳经下方同名方法透传,对外签名零变化。 */
|
|
676
|
+
private readonly runOrchestration: RunOrchestration;
|
|
677
|
+
|
|
678
|
+
/** [R4 / D-R4-1] workflow 族聚合实例(executeWorkflowAgent 派发链 + 类外 helper)。
|
|
679
|
+
* 与 runOrchestration 组间零互调零 import(跨文件协作经壳 deps 闭包)。 */
|
|
680
|
+
private readonly workflowDispatch: WorkflowDispatch;
|
|
681
|
+
|
|
1155
682
|
/**
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
* 触发条件:flushPendingNotifications 后 ledger 仍有 pending(isIdle 门拦 / sendDelivery
|
|
1159
|
-
* 受理失败的残留)且主 agent 非 idle——即本次 shutdown 注定投不出去。落盘动作 =
|
|
1160
|
-
* pi.appendEntry 重写 NOTIFY_LEDGER_CUSTOM_TYPE entry(与 ledger.record 同通道同 schema,
|
|
1161
|
-
* notifyId 幂等:恢复扫描按后写覆盖 + ack/abandoned 差集去重,重复账面不产生重复投递)。
|
|
1162
|
-
* 主 agent idle 时 flush 已投出,无需复写(零开销)。
|
|
1163
|
-
*/
|
|
1164
|
-
private persistUndeliveredNotificationsForReplay(): void {
|
|
1165
|
-
try {
|
|
1166
|
-
const ledger = getBoundNotifyLedger();
|
|
1167
|
-
const pending = ledger?.pendingEntries() ?? [];
|
|
1168
|
-
if (pending.length === 0) return;
|
|
1169
|
-
if (this.isIdleFn?.() !== false) return; // idle:flush 已投出(或无 gate 场景照旧)
|
|
1170
|
-
for (const item of pending) {
|
|
1171
|
-
this.pi?.appendEntry(NOTIFY_LEDGER_CUSTOM_TYPE, {
|
|
1172
|
-
v: 1,
|
|
1173
|
-
notifyId: item.notifyId,
|
|
1174
|
-
content: item.content,
|
|
1175
|
-
record: item.record,
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
logger.warn(
|
|
1179
|
-
`[subagents] shutdown flush blocked by busy main agent: ${pending.length} pending notification(s) persisted to ledger for replay on next session_start`,
|
|
1180
|
-
{ count: pending.length },
|
|
1181
|
-
);
|
|
1182
|
-
} catch (err) {
|
|
1183
|
-
// 落盘复写是防丢失增强,失败不阻断 dispose(账本原 entry 仍随 pi flush 落盘)。
|
|
1184
|
-
bestEffort(err, "persistUndeliveredNotificationsForReplay", "error");
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// ── 执行(subagent-tool 调)────────────────────────────
|
|
1189
|
-
|
|
1190
|
-
// [D4-①] 通知簇四方法(notifyComplete / notifyClosed / piAdapter / toNotifyRecord)
|
|
1191
|
-
// 与 emitPendingRegister / emitPendingUnregister 模块函数已整体搬移至 notify-host.ts
|
|
1192
|
-
//(本类经 this.notifyHost 消费;行为逐字节等价,搬移 + 依赖注入)。
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* 预解析 model(renderCall 标题行用,同步)。代理 modelService.resolveModel。
|
|
1196
|
-
* 仅解析 override/agentConfig 路径;ctxModel 缺失时拋错,调用方 catch 降级。
|
|
683
|
+
* 预解析 model(renderCall 标题行用,同步)。本体已迁 RunOrchestration;壳纯转发,
|
|
684
|
+
* 对外签名不变(subagent-actions-core 消费)。
|
|
1197
685
|
*/
|
|
1198
686
|
resolveModel(
|
|
1199
687
|
agent: string,
|
|
@@ -1201,1750 +689,96 @@ export class SubagentService {
|
|
|
1201
689
|
ctxModel?: ModelInfo,
|
|
1202
690
|
agentConfig?: AgentConfig,
|
|
1203
691
|
): ResolvedModel {
|
|
1204
|
-
return this.
|
|
692
|
+
return this.runOrchestration.resolveModel(agent, override, ctxModel, agentConfig);
|
|
1205
693
|
}
|
|
1206
694
|
|
|
1207
695
|
/**
|
|
1208
|
-
* 统一执行入口。mode 固定 background(sync
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
* @param opts.ctxModel 主 agent 当前模型(模型解析第三层兼底)。undefined 时仅依赖 override/agentConfig。
|
|
1212
|
-
*/
|
|
696
|
+
* 统一执行入口。mode 固定 background(sync 已删除)。本体已迁 RunOrchestration
|
|
697
|
+
* (路由 → identity → record 创建 → worktree → 引擎 run 编排逐行等价随迁);
|
|
698
|
+
* 壳纯转发,对外签名不变(subagent-tool / subagent-actions 消费)。 */
|
|
1213
699
|
async execute(opts: ExecuteOptions): Promise<ExecutionHandle> {
|
|
1214
|
-
this.
|
|
1215
|
-
// [T4② / PS-4] idleTimeoutMs 配置错误在首个副作用前同步 fail-fast(错误含合法范围)。
|
|
1216
|
-
this.assertIdleTimeoutMsSafe(opts);
|
|
1217
|
-
|
|
1218
|
-
// 通用嵌套深度护栏(D-033):嵌套上下文([D3-⑤] 公共层 ExecutionNestingContext)
|
|
1219
|
-
// 记录所有 subagent 嵌套层级(fork + 非 fork),每层 +1。MAX_FORK_DEPTH 同时限
|
|
1220
|
-
// fork 链与通用嵌套——非 fork 递归虽不累积 session 体积,但耗资源且 LLM 易陷入
|
|
1221
|
-
// 「委派→再委派」死循环。在所有副作用之前拦截,错误直达调用方。
|
|
1222
|
-
// 计数基准:顶层 nestingDepth=0,nestingDepth>MAX 被拒。与 fork 体积护栏(parentForkDepth 检查)
|
|
1223
|
-
// 互补:本护栏更严(计所有嵌套),混合链下先生效;两者共享 MAX_FORK_DEPTH 上限不漂移。
|
|
1224
|
-
// [ALS 断裂修复] current() 内含基线兜底(pi 事件回调模型下 enterWith 不贯穿)。
|
|
1225
|
-
const parentNesting = this.execNesting.current();
|
|
1226
|
-
const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
|
|
1227
|
-
if (nestingDepth > MAX_FORK_DEPTH) {
|
|
1228
|
-
throw new ForkDepthExceededError(
|
|
1229
|
-
`subagent nesting depth ${nestingDepth} > ${MAX_FORK_DEPTH} (max recursion), refusing to spawn deeper`,
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// mode 固定 background(sync 模式已删除)
|
|
1234
|
-
const mode: ExecutionMode = "background";
|
|
1235
|
-
|
|
1236
|
-
// ── 1. IDENTITY 前置解析:agentConfig(agent .md 加载)保持在最前 ──
|
|
1237
|
-
// [u-h2 D2-1] 路由先行:model 解析从「路由之前」移到「路由之后、按目标引擎分支」
|
|
1238
|
-
// (修 F2-A/B 时序根因——曾 :850 先解析 model 再 :867 路由)。agentConfig 是路由
|
|
1239
|
-
// 第二层输入(frontmatter engine)必须先解析;显式 agent ref 校验语义不变。
|
|
1240
|
-
const agent = opts.agent ?? DEFAULT_AGENT_NAME;
|
|
1241
|
-
const agentConfig = opts.agent
|
|
1242
|
-
? this.modelService.getRequiredAgentConfig(opts.agent)
|
|
1243
|
-
: undefined;
|
|
1244
|
-
|
|
1245
|
-
// ── 1.5 引擎路由(D2 单轨 + D3-② 路由单点:统一经 routeEngineForHost)──
|
|
1246
|
-
// 唯一实现在 engine/routing.ts(pi 同步短路 + registry 注入 + 兜底回本地 pi 实例
|
|
1247
|
-
// 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
|
|
1248
|
-
// 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
|
|
1249
|
-
// 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
|
|
1250
|
-
// record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
|
|
1251
|
-
// 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
|
|
1252
|
-
// 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
|
|
1253
|
-
// [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
|
|
1254
|
-
// engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
|
|
1255
|
-
// 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
|
|
1256
|
-
const routingInput = {
|
|
1257
|
-
callEngine: opts.engine,
|
|
1258
|
-
agentEngine: agentConfig?.engine,
|
|
1259
|
-
globalDefaultEngine: this.modelService.getGlobalConfig().defaultEngine,
|
|
1260
|
-
};
|
|
1261
|
-
const routed = routeEngineForHost({
|
|
1262
|
-
routing: routingInput,
|
|
1263
|
-
// 守卫 c 判据只看调用方显式指定的 model(resolved model 含 ctxModel 兼底,
|
|
1264
|
-
// 恒非空会把一切兜底误判为 model 绑定命中)
|
|
1265
|
-
taskModel: opts.model,
|
|
1266
|
-
strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
|
|
1267
|
-
probe: (engineId) => getEngine(engineId).probe(),
|
|
1268
|
-
// [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
|
|
1269
|
-
// 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
|
|
1270
|
-
piEngine: this.resolveChatEnginePort(),
|
|
1271
|
-
});
|
|
1272
|
-
const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
|
|
1273
|
-
return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
/**
|
|
1277
|
-
* 按 id 查内存 running record 的只读快照(G3-002 修复)。
|
|
1278
|
-
* 不从 session.jsonl 重建(cancel/list 单点查询只关心内存 running record)。
|
|
1279
|
-
* 供 tool 层 cancelHandler 翻译 throw 用(id 不存在 / mode / 终态三种错误)。
|
|
1280
|
-
* 不存在返回 undefined。
|
|
1281
|
-
*/
|
|
1282
|
-
private findRecord(id: string): RecordSnapshot | undefined {
|
|
1283
|
-
this.assertReady();
|
|
1284
|
-
const record = this.store.getMutable(id);
|
|
1285
|
-
return record ? snapshot(record) : undefined;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
/** 取消 background record(tryTransition CAS 抢锁防重复副作用)。 */
|
|
1289
|
-
cancel(id: string): boolean {
|
|
1290
|
-
this.assertReady();
|
|
1291
|
-
const record = this.store.getMutable(id);
|
|
1292
|
-
if (!record) return false;
|
|
1293
|
-
return this.cancelBackground(record);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
/**
|
|
1297
|
-
* [v8.5 A1/B] 全态查找:任意状态(running/closed)× 任意归属(含异 root session)的
|
|
1298
|
-
* record 快照。供 message 拒绝文案分流(A1)与 fork-from 源解析(B)共用。
|
|
1299
|
-
*
|
|
1300
|
-
* 与 getRecordForAction 的差异:不做归属/直接父校验、不重建可变 record 入内存,
|
|
1301
|
-
* 只读快照(light 形态可能缺详情重数据,身份/sidecar 状态字段齐全)。查询顺序与
|
|
1302
|
-
* getRecordForAction 冷路径同款(idToFile 索引直查 → collectRecords 全扫兑底),
|
|
1303
|
-
* 不限 status——终态(sidecar closed)记录也能查到。
|
|
1304
|
-
*
|
|
1305
|
-
* 返回 undefined:id 在内存与磁盘均不存在。
|
|
1306
|
-
*/
|
|
1307
|
-
private lookupRecordAnyState(id: string): SubagentRecord | undefined {
|
|
1308
|
-
try {
|
|
1309
|
-
this.assertReady();
|
|
1310
|
-
} catch {
|
|
1311
|
-
return undefined; // 未初始化/disposed 时按「不存在」处理(文案分流无需区分)
|
|
1312
|
-
}
|
|
1313
|
-
const direct = this.store.findLightById(id);
|
|
1314
|
-
if (direct) return direct;
|
|
1315
|
-
return this.store.collectRecords(COLD_LOOKUP_SCAN_LIMIT, "all", undefined).find((r) => r.id === id);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// ── 对话模式投递(M2-B3 message action 调用)──────────────
|
|
1319
|
-
|
|
1320
|
-
// [review 修复] 已删除 deliverToRunning(busy follow_up/steer 投递 + pendingMessages
|
|
1321
|
-
// 消费确认制):SP-5 upgrade 后所有 running record 走 chatMode 分支 → 统一投递
|
|
1322
|
-
//(热路径 prompt+streamingBehavior / 冷路径 resume),该方法无生产调用方,
|
|
1323
|
-
// 其配套三段消费链(push / message_start shift / redeliverPending 补投)全部不可达,
|
|
1324
|
-
// 一并移除(详见各文件同步删除)。
|
|
1325
|
-
// [D2 单轨] 投递的 pi RPC stdin 协议知识(stdin prompt 命令直调 + streamingBehavior
|
|
1326
|
-
// 映射 + EPIPE 兜底 + 冷路径分流)已下沉 PiEngine.deliverPrompt——本层经
|
|
1327
|
-
// deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
|
|
1328
|
-
|
|
1329
|
-
/**
|
|
1330
|
-
* [V2 决策 3 → W3 协议形态] chatMode 统一投递入口(message action 的 Service 面)
|
|
1331
|
-
* ——经协议 interact(message action)发往 pi-subagent-cli 引擎进程。协议知识
|
|
1332
|
-
* (stdin prompt 命令 + streamingBehavior 映射 + EPIPE 兜底)在引擎进程内
|
|
1333
|
-
* (chat-session.deliverMessage);编排层按结构化结果分流:
|
|
1334
|
-
*
|
|
1335
|
-
* 受理(引擎侧进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle。
|
|
1336
|
-
* 冷路径(engine_session_not_resumable = 进程死/无活进程):冷路径续轮
|
|
1337
|
-
* (resumeColdRound → run chat + resume 锚点接续,仅崩溃/timeout kill/跨重启命中)。
|
|
1338
|
-
* 其他拒绝(EPIPE 兜底耗尽 / engine_interact_failed):业务拒绝原样 throw
|
|
1339
|
-
* (文案自带行动语言——与 inproc 形态逐字节一致)。
|
|
1340
|
-
*
|
|
1341
|
-
* @param record 目标 record(chatMode,running 或 idle)
|
|
1342
|
-
* @param text 消息正文
|
|
1343
|
-
* @param interrupt true=steer(抢占)/ false=followUp(排队)
|
|
1344
|
-
*/
|
|
1345
|
-
private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
|
|
1346
|
-
this.assertReady();
|
|
1347
|
-
// 终态守卫(MF-4):closed record 不可续聊(对齐原冷路径守卫前置——状态检查
|
|
1348
|
-
// 先于引擎交互,避免对已终态对话产生引擎侧副作用)。
|
|
1349
|
-
if (record.status !== "running") {
|
|
1350
|
-
throw new Error(
|
|
1351
|
-
`subagent ${record.id} is not ready for a new message (current state: ${record.status}). ` +
|
|
1352
|
-
`Recovery: use action:'list' to confirm state; wait for the current round to finish, or send the message again once it is idle.`,
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
// 新 turn 开跑先 disarm idle timer(防 turn 期间误杀活进程——V2 决策 4,
|
|
1356
|
-
// 原 PiEngine.deliverPrompt 入口语义,编排侧承接)。
|
|
1357
|
-
disarmIdleTimer(record.id);
|
|
1358
|
-
const engine = this.resolveChatEnginePort();
|
|
1359
|
-
// recordId 键路由兜底注册(首轮已注册时幂等跳过——kickOffChatRound 注册面):
|
|
1360
|
-
// 续聊轮 delta/生命周期帧的分发目标。stream 缺省(首轮流已 dispose——widget 清除后
|
|
1361
|
-
// delta 为 no-op,与 inproc 形态一致),守护刷新与相位消费不受影响。
|
|
1362
|
-
if (!this.chatRoundRoutes.has(record.id)) {
|
|
1363
|
-
this.chatRoundRoutes.set(
|
|
1364
|
-
record.id,
|
|
1365
|
-
engine.registerChatRoundRoute?.(record.id, {
|
|
1366
|
-
onStreamDelta: (_delta) => refreshFromProtocolEvent(record.id),
|
|
1367
|
-
onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
|
|
1368
|
-
}) ?? (() => {}),
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
const result = await engine.interact(this.chatHandleFor(record), {
|
|
1372
|
-
kind: "message",
|
|
1373
|
-
payload: text,
|
|
1374
|
-
interrupt,
|
|
1375
|
-
});
|
|
1376
|
-
if (!result.ok) {
|
|
1377
|
-
// 冷路径分流:引擎无活进程 → 冷路径续轮(resume spawn 接续原 session——
|
|
1378
|
-
// W2 契约「引擎不自发 resume,宿主经 run chat+resume 接续」)。
|
|
1379
|
-
if (result.code === "engine_session_not_resumable") {
|
|
1380
|
-
this.resumeColdRound(record, text);
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
throw new Error(result.message);
|
|
1384
|
-
}
|
|
1385
|
-
// 受理成功:轮始执行态信号清除 + 迁移上报(原 deliverPrompt 热路径同步段语义——
|
|
1386
|
-
// 清上一轮 result(§5.4 isStreaming 公式要求 result undefined 才显示 streaming)
|
|
1387
|
-
// 与 resumable,appendEntry 让 runtime 派生缓存失效、GUI 从 waiting 切回 spinner)。
|
|
1388
|
-
record.status = "running";
|
|
1389
|
-
record.result = undefined;
|
|
1390
|
-
record.resumable = undefined;
|
|
1391
|
-
this.store.reportRecordTransition(record);
|
|
1392
|
-
// [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点;首轮/
|
|
1393
|
-
// 冷续轮调用点在 kickOffChatRound 的 run 派发前——[F-2 修复] 该点此前声称存在
|
|
1394
|
-
// 实则缺失,被删的 inproc stdout-pump 是首轮唯一中段守护)。prompt 发出后挂
|
|
1395
|
-
// **中段**无进展检测,刷新源 = 协议事件(recordId 键 streamDelta / roundLifecycle
|
|
1396
|
-
// 帧,见 chatRoundRoutes;首轮 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle);
|
|
1397
|
-
// settled 相位交棒(noteRoundSettledFromProtocol)、idle/close/cancel 终态化即清。
|
|
1398
|
-
armMidRoundNoProgress(record.id, {
|
|
1399
|
-
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
1400
|
-
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* 冷路径续轮(PiEngine.deliverPrompt 的编排回调,D2 下沉后的归属):resume spawn
|
|
1406
|
-
* 开启新一轮对话(设计决策 6 idle 分支)。仅进程死(idle timer reap / 崩溃 / 跨重启
|
|
1407
|
-
* / EPIPE 兜底)时经引擎到达。
|
|
1408
|
-
*
|
|
1409
|
-
* record 必须 idle-resumable(轮次完成、进程已回收、record 留内存)。手动把 status
|
|
1410
|
-
* 设回 "running"(M2-A 边界:idle→running 是恢复非终态,绕过 tryTransition——
|
|
1411
|
-
* tryTransition 要求当前态 running 才 CAS)。
|
|
1412
|
-
*
|
|
1413
|
-
* resume 参数从 record identity 读(防多轮对话模型漂移,探针 P-10):sessionFile、
|
|
1414
|
-
* model、thinkingLevel 均为 record 身份字段(创建时确定、不可变)。maxTurns/schema 等
|
|
1415
|
-
* 执行约束第一版不恢复(设计 §5 拆分 1 待验证检查点),agentConfig 用 undefined
|
|
1416
|
-
*(pi --session 续写保留上下文,agent 行为由 session 内 messages 决定;M2-B3 messageHandler 可完善)。
|
|
1417
|
-
*
|
|
1418
|
-
* detached 编排(kickOffChatRound,经 EnginePort 交接):不 await,轮次在 background 跑。
|
|
1419
|
-
* chatMode + done 时轮次收尾的 M2-A 分流自动把 record 回退 idle-resumable。并发槽在
|
|
1420
|
-
* 轮次执行内重新 acquire(轮次间 idle 已 release);pool.acquire 是排队模型,
|
|
1421
|
-
* 池满时排队等待槽位而非 throw(与 execute 一致)。
|
|
1422
|
-
*
|
|
1423
|
-
* @param record 目标 record(必须 idle-resumable)
|
|
1424
|
-
* @param text 新一轮消息正文
|
|
1425
|
-
* @throws Error record 非 running / 无 sessionFile / 无 controller / worktree 绑定丢失 / 续轮在途
|
|
1426
|
-
*/
|
|
1427
|
-
private resumeColdRound(record: ExecutionRecord, text: string): void {
|
|
1428
|
-
this.assertReady();
|
|
1429
|
-
// [CL-b1-cas-coupling / v4 B-1] v4 把旧 idle 折入 running 后此守卫对 idle-resumable
|
|
1430
|
-
// record 恒放行(idle 本来就是 running),`status = "running"`(下方)是幂等写——
|
|
1431
|
-
// 旧「idle→running CAS」的单写者语义已消失。单写者守卫由 resumesInFlight 承担
|
|
1432
|
-
//([review MF1],见字段注释);本守卫保留拦截终态(closed)record。
|
|
1433
|
-
if (record.status !== "running") {
|
|
1434
|
-
// MF-4:行动语言(spec §3.1),不暴露 resume/controller 等内部词汇。
|
|
1435
|
-
throw new Error(
|
|
1436
|
-
`subagent ${record.id} is not ready for a new message (current state: ${record.status}). ` +
|
|
1437
|
-
`Recovery: use action:'list' to confirm state; wait for the current round to finish, or send the message again once it is idle.`,
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
// [review MF1] 在途 resume 守卫:上一条消息发起的 resume 仍在途(spawn 尚未注册 /
|
|
1441
|
-
// 本轮轮次未收尾)时,再次到达(冷路径重入 / EPIPE 兜底)直接拒绝。
|
|
1442
|
-
// 触发链:pi 对同一 assistant message 的 tool calls 顺序执行(sequential),tool1 的
|
|
1443
|
-
// 投递在冷路径续轮返回即 resolve(早于 spawn 注册完成),tool2 立即执行 →
|
|
1444
|
-
// getChildByRecord 仍 undefined → 再次冷路径。无此守卫 → 两次 kickOff →
|
|
1445
|
-
// runSpawn 2 次 → 两 pi 子进程双写同一 session JSONL + 第一个脱离 kill 记账成孤儿。
|
|
1446
|
-
if (this.resumesInFlight.has(record.id)) {
|
|
1447
|
-
// MF-4:行动语言。
|
|
1448
|
-
throw new Error(
|
|
1449
|
-
`subagent ${record.id} is already starting a new round (a previous message is still resuming). ` +
|
|
1450
|
-
`Recovery: wait for the round to start (check with action:'list'), then send the message again; ` +
|
|
1451
|
-
`or use action:'close' if this subagent is no longer needed.`,
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
if (!record.sessionFile) {
|
|
1455
|
-
// MF-4:session 损坏 → canonical 文案(spec §3.1 失败表)。
|
|
1456
|
-
throw new Error(
|
|
1457
|
-
`subagent ${record.id} session unavailable (session file missing or unreadable). ` +
|
|
1458
|
-
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
if (!record.controller) {
|
|
1462
|
-
// chatMode background record 创建时一定有 controller;兜底防御性检查。MF-4 行动语言。
|
|
1463
|
-
throw new Error(
|
|
1464
|
-
`subagent ${record.id} is not ready for a new message (internal state error). ` +
|
|
1465
|
-
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
1466
|
-
);
|
|
1467
|
-
}
|
|
1468
|
-
// [review round2] 跨重启 worktree 绑定丢失守卫:原 record 曾用 worktree 隔离
|
|
1469
|
-
//(hadWorktree 由 getRecordForAction 磁盘重建时从 session entry 恢复),但 handle
|
|
1470
|
-
// 不可序列化、跨重启后无法 reattach(reattach 也无 checkout 可用——reaper 在
|
|
1471
|
-
// session_start 已按 pid 死活清理孤儿 worktree)。此时 resume 的 spawn cwd 会静默
|
|
1472
|
-
// 回落主 repo,子 agent 直接编辑主仓库——正是 worktree 隔离要防的场景。拒绝续聊。
|
|
1473
|
-
if (record.hadWorktree === true && !record.worktreeHandle) {
|
|
1474
|
-
// MF-4:行动语言。
|
|
1475
|
-
throw new Error(
|
|
1476
|
-
`subagent ${record.id} was created with worktree isolation, but that binding was lost when the parent process restarted; ` +
|
|
1477
|
-
`resuming it now would run in the main repository and bypass the isolation. ` +
|
|
1478
|
-
`Recovery: use action:'close' to release this subagent, then action:'start' a new one with worktree isolation.`,
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// 手动设回 running(M2-A 边界:绕过 tryTransition,idle→running 恢复非终态 CAS)。
|
|
1483
|
-
record.status = "running";
|
|
1484
|
-
// 执行态信号清除(residual-fixes U3 补全):新一轮开跑 = 无轮终信号——resumable
|
|
1485
|
-
// 与上一轮 result 都要清(§5.4 isStreaming 公式要求 result undefined 才显示
|
|
1486
|
-
// streaming,不清则续轮流仍显示 waiting、spinner 无法恢复)。
|
|
1487
|
-
record.resumable = undefined;
|
|
1488
|
-
record.result = undefined;
|
|
1489
|
-
// W16 [D4]:冷路径续轮是类外状态写点(不走 register/archive),显式上报迁移。
|
|
1490
|
-
this.store.reportRecordTransition(record);
|
|
1491
|
-
|
|
1492
|
-
// [W3 协议形态] 冷续锚点 = record identity(防多轮对话模型漂移,探针 P-10):
|
|
1493
|
-
// sessionFile 经 resume.sessionRef 携带(引擎 --session 续写原文件);model/
|
|
1494
|
-
// thinkingLevel 属 record 身份字段(锚点轮引擎侧覆盖解析)。原 SpawnResumeOpts
|
|
1495
|
-
// 形态随 inproc pi 引擎目录 删除,锚点载体 = 协议 ResumeAnchor(W1 SDK 契约)。
|
|
1496
|
-
const resume: ResumeAnchor = {
|
|
1497
|
-
sessionRef: {
|
|
1498
|
-
recordId: record.id,
|
|
1499
|
-
...(record.sessionFile !== undefined ? { sessionFile: record.sessionFile } : {}),
|
|
1500
|
-
},
|
|
1501
|
-
poolKey: PI_POOL_KEY,
|
|
1502
|
-
};
|
|
1503
|
-
|
|
1504
|
-
// 重建 resolved:锚点轮的模型解析兜底(ctxModel 第三层)。从 record.model
|
|
1505
|
-
// (createRecordForMode 写入的 "provider/id" 或无斜杠 ref——契约变更④)拆分构造
|
|
1506
|
-
// 最小 ModelInfo;拆分与写入侧同源(splitEngineModelRef):无斜杠 ref 的 provider
|
|
1507
|
-
// 为空串(旧行为 "unknown" 会给续轮注入虚构 provider),整串进 name。
|
|
1508
|
-
const model = splitEngineModelRef(record.model);
|
|
1509
|
-
const identity: ResolvedIdentity = {
|
|
1510
|
-
agent: record.agent,
|
|
1511
|
-
agentConfig: undefined,
|
|
1512
|
-
resolved: {
|
|
1513
|
-
model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
|
|
1514
|
-
thinkingLevel: record.thinkingLevel,
|
|
1515
|
-
},
|
|
1516
|
-
};
|
|
1517
|
-
|
|
1518
|
-
const opts: ExecuteOptions = {
|
|
1519
|
-
task: text,
|
|
1520
|
-
slug: record.slug,
|
|
1521
|
-
worktree: record.worktreeHandle,
|
|
1522
|
-
// 冷续轮同样是 chat 会话形态(run.params.chat 必传 + conversation gate 放行)。
|
|
1523
|
-
conversation: true,
|
|
1524
|
-
};
|
|
1525
|
-
|
|
1526
|
-
// detached 编排:轮次在 background 跑(协议 run chat + resume),pool 重新
|
|
1527
|
-
// acquire(轮次间 idle 已 release)。chatMode 轮终 settle 分流自动
|
|
1528
|
-
// finalizeRoundToIdle(record 回 idle、round+1)。
|
|
1529
|
-
// [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
|
|
1530
|
-
// (冷路径重入)在守卫处被拒;轮次收尾 finally 统一清除。
|
|
1531
|
-
this.resumesInFlight.add(record.id);
|
|
1532
|
-
this.kickOffChatRound(record, opts, identity, record.controller.signal, PRIORITY_BACKGROUND, resume);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
/**
|
|
1536
|
-
* [T2③] 热路径轮 settled watchdog 到期处置(对齐 u-t2a 首轮形态:kill + 该轮失败
|
|
1537
|
-
* 终态化 + 失败通知,error 含 'settled watchdog' 标记与恢复指引)。
|
|
1538
|
-
*
|
|
1539
|
-
* [D9 两段式] 两段(mid-round 无进展 / settled 收尾段上界)共用本处置,fire 信息
|
|
1540
|
-
*(段 + 窗长)由原语注入,失败文案按段分叉窗长语义。
|
|
1541
|
-
*
|
|
1542
|
-
* 与首轮的差异:runSpawn 已返回(无收尾链路承接 settledWatchdogFired 标记),失败
|
|
1543
|
-
* 终态化在本回调内完成。chatMode 按 MF-6 语义回退 running-resumable(与首轮 watchdog
|
|
1544
|
-
* 经 runAndFinalize 失败分支的最终形态一致——对话可冷路径复活);非 chatMode 终态
|
|
1545
|
-
* 销毁。CAS(tryTransition closed+gc)防与 cancel/dispose 双收尾,抢锁失败即跳过。
|
|
1546
|
-
*
|
|
1547
|
-
* 回调在 timer 触发的同步上下文执行:同步段只做 kill + CAS(不抛),异步收尾
|
|
1548
|
-
* fire-and-forget 且 catch 归 bestEffort——错误逃出回调 = uncaughtException 崩宿主。
|
|
1549
|
-
*/
|
|
1550
|
-
private onHotPathSettledWatchdogTimeout(record: ExecutionRecord, fire: SettledWatchdogFireInfo): void {
|
|
1551
|
-
const windowDesc =
|
|
1552
|
-
fire.phase === "mid-round"
|
|
1553
|
-
? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min after prompt (mid-round no-progress)`
|
|
1554
|
-
: `no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end (settled phase)`;
|
|
1555
|
-
logger.warn(
|
|
1556
|
-
`[subagents] settled watchdog (${fire.phase}) fired for ${record.id}: ${windowDesc}, ` +
|
|
1557
|
-
`terminating (LC-1 wedge recovery)`,
|
|
1558
|
-
);
|
|
1559
|
-
killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
|
|
1560
|
-
// [W3] 子进程在引擎进程内——终止经协议 interact cancel(SIGTERM → 引擎侧
|
|
1561
|
-
// settle 等待 → 杀链升级);镜像置死位由上方 killRecordChildWithEscalation 记账。
|
|
1562
|
-
this.terminateChatSession(record, "cancel", "settled watchdog (hot path)");
|
|
1563
|
-
const failedResult: AgentResult = {
|
|
1564
|
-
text: "",
|
|
1565
|
-
turns: record.turnCount,
|
|
1566
|
-
durationMs: Date.now() - record.startedAt,
|
|
1567
|
-
success: false,
|
|
1568
|
-
error:
|
|
1569
|
-
`subagent did not reach agent_settled (${windowDesc}; settled watchdog); ` +
|
|
1570
|
-
`the process was terminated to bound the wait. ` +
|
|
1571
|
-
`Recovery: check state with subagents action:'list', then re-send your message to continue.`,
|
|
1572
|
-
sessionId: record.id,
|
|
1573
|
-
toolCalls: [],
|
|
1574
|
-
};
|
|
1575
|
-
if (!tryTransition(record, "closed", "gc")) {
|
|
1576
|
-
return; // 已被 cancel/dispose 抢先终态化——不重复收尾(watchdog disarm 由对方承接)
|
|
1577
|
-
}
|
|
1578
|
-
const finalize = record.chatMode
|
|
1579
|
-
? this.finalizeRoundToIdle(record, failedResult)
|
|
1580
|
-
: this.finalizeRecord(record, failedResult, "closed", "gc");
|
|
1581
|
-
void finalize
|
|
1582
|
-
// [D4-①] 通知簇搬移 notify-host 后的遗留调用点修正:this.notifyComplete 方法已
|
|
1583
|
-
// 不存在(其余三处调用点均 this.notifyHost.notifyComplete),旧引用 throw
|
|
1584
|
-
// TypeError 被本 .catch 吞掉 → settled watchdog 失败通知静默丢失
|
|
1585
|
-
.then(() => this.collectCoordinator.route(record))
|
|
1586
|
-
.catch((err: unknown) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
// [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
|
|
1590
|
-
// 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/inproc pi-engine(已删) 的
|
|
1591
|
-
// rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
|
|
1592
|
-
// 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
|
|
1593
|
-
|
|
1594
|
-
// ── 对话模式 message/close action 支持(M2-B3)──────────────
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* 按 id 查 record 并做归属校验(message/close action 的统一入口)。
|
|
1598
|
-
*
|
|
1599
|
-
* 设计决策 3(归属守卫):校验 record.rootSessionId 必须等于当前 session 的根 id
|
|
1600
|
-
*(this.sessionRootId)。不匹配 / 不存在统一抛「not found or not owned」——不区分
|
|
1601
|
-
* 两种失败,防信息泄露(无法通过错误消息探测其他 session 的 subagent id)。
|
|
1602
|
-
*
|
|
1603
|
-
* 同进程内 running + idle record 都在内存(getMutable);终态 record 已 archive。
|
|
1604
|
-
* 跨重启(SP-2)内存空时,从磁盘 collectRecords 重建 idle record 并 register 进内存。
|
|
1605
|
-
* reconstructAll 已将跨重启 record(无 sidecar marker + pid 死)标记为 running(v4 B-1 跨重启可续聊语义,record-store buildRecord 分支 4),
|
|
1606
|
-
* collectRecords 返回的 SubagentRecord 可直接转为可变 ExecutionRecord 供续操作。
|
|
1607
|
-
*
|
|
1608
|
-
* @param id subagent record id
|
|
1609
|
-
* @param opts.allowReconnect [v8.5 D] message 专属:冷查额外接受「可重连」的 closed 记录
|
|
1610
|
-
* (死因∈ RECONNECTABLE_FINAL_REASONS,A 档真实死因 sidecar 是唯一准入门),经四重守卫后
|
|
1611
|
-
* resurrectClosed 回边为 running 并续写原 session 文件。仅 message 开启;close/cancel 维持单向终态语义。
|
|
1612
|
-
* @returns 可变 ExecutionRecord(message/close handler 直接操作)
|
|
1613
|
-
* @throws Error record 不存在 / 非本 session 所有(含恢复指引)
|
|
1614
|
-
* @throws ResurrectDeniedError 命中可重连集但被 worktree/异进程活实例守卫拦截(自带完整行动语言)
|
|
1615
|
-
*/
|
|
1616
|
-
private getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord {
|
|
1617
|
-
this.assertReady();
|
|
1618
|
-
let record = this.store.getMutable(id);
|
|
1619
|
-
// SP-2 跨重启恢复:内存未命中时,从磁盘 collectRecords 重建 idle record。
|
|
1620
|
-
// reconstructAll 已将跨重启 record(无 sidecar + pid 死)标记为 running(v4 B-1 可续聊语义,非 crashed),
|
|
1621
|
-
// 直接转为可变 ExecutionRecord register 进内存,供 message/close action 续操作。
|
|
1622
|
-
if (!record) {
|
|
1623
|
-
// [D4-③] 冷查/复活链整体搬移至 cold-resurrect.ts(行为逐字节等价)。
|
|
1624
|
-
record = coldLookupForAction(this.coldResurrectDeps, id, opts?.allowReconnect === true);
|
|
1625
|
-
}
|
|
1626
|
-
if (!record || record.rootSessionId !== this.sessionRootId) {
|
|
1627
|
-
throw new Error(
|
|
1628
|
-
`subagent not found or not owned: ${id}. Recovery: use action:'list' to confirm the id; ` +
|
|
1629
|
-
`ended subagents cannot be messaged — start a new one; only subagents owned by the current session can be operated on.`,
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
1632
|
-
// [v4 A-5 / P7] 直接父校验:rootSessionId 已确认 record 属于本 session 树,但递归场景下
|
|
1633
|
-
// 孙级 record(parentRecordId = 某子进程的 self recordId)的子进程句柄只存在于其直接父
|
|
1634
|
-
// 进程内存。主进程(基线 null)若仅凭 rootSessionId 通过就 message 孙级,会走
|
|
1635
|
-
// 冷路径重新 spawn → 双写同一 session 文件(P7 双写者窗口)。统一用 baseline recordId 校验:
|
|
1636
|
-
// - 主进程 baseline=undefined → 只能操作 parentRecordId=undefined 的根层 record
|
|
1637
|
-
// - 子进程 baseline="sa-X" → 只能操作 parentRecordId="sa-X" 的直接孩子
|
|
1638
|
-
// record.parentRecordId===undefined 视作根层,仅主进程可操作(身份缺省的旧/异常 record 归此)。
|
|
1639
|
-
const baselineRecordId = this.execNesting.baseline()?.recordId ?? undefined;
|
|
1640
|
-
if (record.parentRecordId !== baselineRecordId) {
|
|
1641
|
-
throw new Error(
|
|
1642
|
-
`subagent ${id} is owned by its direct parent; message it through that parent ` +
|
|
1643
|
-
`(see /subagents list, parent=${record.parentRecordId ?? "(root layer)"}). [v4 A-5] cross-layer ` +
|
|
1644
|
-
`ownership guard: this process's baseline=${baselineRecordId ?? "(root)"} is not the direct parent of ${id}; ` +
|
|
1645
|
-
`operating here would race the owning child process's handle and double-write the session file.`,
|
|
1646
|
-
);
|
|
1647
|
-
}
|
|
1648
|
-
return record;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// [D4-③] 冷路径复活链(findColdLookupCandidate / assertReconnectAllowed /
|
|
1652
|
-
// resurrectColdRecord / coldLookupForAction + isReconnectableClosed 判定)已整体
|
|
1653
|
-
// 搬移至 cold-resurrect.ts(本类经 coldResurrectDeps 注入,行为逐字节等价)。
|
|
1654
|
-
// SP-2 冷路径 [perf] 语义不变:idToFile 索引直查 → collectRecords 全扫兜底。
|
|
1655
|
-
// [T5③ / PS-7b] running 候选异进程活实例守卫(findForeignLiveInstance 探针,
|
|
1656
|
-
// ResurrectDeniedError)已随迁 cold-resurrect.ts 的 findColdLookupCandidate
|
|
1657
|
-
//(与 closed 候选守卫 assertReconnectAllowed 对称)。
|
|
1658
|
-
|
|
1659
|
-
/**
|
|
1660
|
-
* close action 的统一行为分流(running 子态 × force)。
|
|
1661
|
-
*
|
|
1662
|
-
* running + force:true → cancelBackground(显式 SIGTERM + closed+cancelled 终态)
|
|
1663
|
-
* running + force:false + 无在跑轮 → closeChatIdle(立即终态化 done + 回收保活进程 + disarm timer)
|
|
1664
|
-
* (isIdle timer armed 或 isResumable 无活进程)
|
|
1665
|
-
* running + force:false + 有活进程在跑轮 → 置 closeAfterRound=true(轮完成时终态化:
|
|
1666
|
-
* chatMode 消费点在 onRoundSettled,非 chatMode 在 runAndFinalize CAS 分支)
|
|
1667
|
-
* 其他终态 → 幂等 no-op(已结束)
|
|
1668
|
-
*
|
|
1669
|
-
* 与设计决策 5 一致:close = 正式终态(走 finalize),force 只影响 running 时机。
|
|
1670
|
-
*
|
|
1671
|
-
* @param record 目标 record(getRecordForAction 已校验归属)
|
|
1672
|
-
* @param force true=立即终止(running 时 SIGTERM)/ false=优雅关闭(running 时等轮完)
|
|
1673
|
-
*/
|
|
1674
|
-
private async closeSubagent(record: ExecutionRecord, force: boolean): Promise<void> {
|
|
1675
|
-
this.assertReady();
|
|
1676
|
-
if (record.status === "running") {
|
|
1677
|
-
if (force) {
|
|
1678
|
-
// 立即终止:cancelBackground(controller.abort + tryTransition closed+cancelled + finalize)
|
|
1679
|
-
this.cancelBackground(record);
|
|
1680
|
-
} else if (isIdle(record) || isResumable(record)) {
|
|
1681
|
-
// [M5] 无在跑轮:Path A(isIdle timer armed、进程保活等待续聊)/ Path B(isResumable
|
|
1682
|
-
// 无活进程)→ 立即终态化 done(closeChatIdle 内回收保活进程 + disarm timer)。
|
|
1683
|
-
// 旧代码 Path A 走 else 置 closeAfterRound,但唯一消费点(runAndFinalize CAS 分支)
|
|
1684
|
-
// 对 chatMode 不可达——agent_settled 恒 arm idle timer → runAndFinalize 恒命中
|
|
1685
|
-
// early return(isIdle 恒 true),标志置了无人消费,tool 却返回 {closed:true}(谎报)。
|
|
1686
|
-
await this.closeChatIdle(record);
|
|
1687
|
-
} else {
|
|
1688
|
-
// 优雅关闭:正在执行(有活进程在跑轮),标记 closeAfterRound,轮完成时终态化
|
|
1689
|
-
//(chatMode 消费点在 onRoundSettled,非 chatMode 在 runAndFinalize CAS 分支)
|
|
1690
|
-
record.closeAfterRound = true;
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
// 其他终态(closed):幂等 no-op
|
|
700
|
+
return this.runOrchestration.execute(opts);
|
|
1694
701
|
}
|
|
1695
702
|
|
|
1696
703
|
/**
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
* 无在途 AgentResult(轮次完成时 record 未冻结,turns[] 保留运行时状态),
|
|
1700
|
-
* 构造合成 done result(对齐 cancelBackground 的 cancelledResult 模式)。
|
|
1701
|
-
* 走 doFinalizeRecord 的完整终态化路径(completeRecord + archive + finalized + worktree
|
|
1702
|
-
* cleanup + alive marker + manifest)。
|
|
1703
|
-
*
|
|
1704
|
-
* [M5] 覆盖两路:Path B(无活进程,同旧行为)与 Path A(idle timer armed、进程保活等待
|
|
1705
|
-
* 续聊)。Path A 必须先显式回收进程 + disarm timer——否则 record 已终态化但保活进程
|
|
1706
|
-
* 继续驻留(终态后无人再杀它:closeSubagent 不再来、idle timer 已 disarm、runSpawn
|
|
1707
|
-
* promise 早已 resolve),直到宿主进程退出。
|
|
1708
|
-
*
|
|
1709
|
-
* 不走 tryTransition(v4 B-1 此态 status=running,但由 doFinalizeRecord 内部的
|
|
1710
|
-
* completeRecord 直接覆盖 status,与 cancelBackground 对 record 的处理同构)。
|
|
1711
|
-
*/
|
|
1712
|
-
private async closeChatIdle(record: ExecutionRecord): Promise<void> {
|
|
1713
|
-
// [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
|
|
1714
|
-
// [T2④ / LC-2] 终止语义收敛:SIGTERM 被无视时 30s 升级 SIGKILL——终态化后
|
|
1715
|
-
// record 已 archive,挂住 = 幽灵进程无任何后续回收通道。同时 disarm settled
|
|
1716
|
-
// watchdog(本路径终态化 = settled 等待窗口终结,防 watchdog 误杀后续同 id 资源)。
|
|
1717
|
-
// [W3] 实际终止在引擎进程内(协议 interact close force → 引擎侧杀链);
|
|
1718
|
-
// killRecordChildWithEscalation 只做镜像置死记账。
|
|
1719
|
-
disarmIdleTimer(record.id);
|
|
1720
|
-
disarmSettledWatchdog(record.id);
|
|
1721
|
-
disarmRoundFromProtocol(record.id);
|
|
1722
|
-
killRecordChildWithEscalation(record.id, "closeChatIdle");
|
|
1723
|
-
this.terminateChatSession(record, "close", "closeChatIdle");
|
|
1724
|
-
// 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
|
|
1725
|
-
// `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
|
|
1726
|
-
// completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
|
|
1727
|
-
// archive 落的 close 终态 subagent-record entry(D4 重建源)随之失真——重开
|
|
1728
|
-
// session 后 result 回退空串。
|
|
1729
|
-
const doneResult: AgentResult = {
|
|
1730
|
-
text: record.result ?? "",
|
|
1731
|
-
turns: record.turnCount,
|
|
1732
|
-
durationMs: Date.now() - record.startedAt,
|
|
1733
|
-
success: true,
|
|
1734
|
-
sessionId: record.id,
|
|
1735
|
-
toolCalls: [],
|
|
1736
|
-
};
|
|
1737
|
-
await doFinalizeRecord(
|
|
1738
|
-
{
|
|
1739
|
-
manifestStore: this.manifestStore,
|
|
1740
|
-
worktreeManager: this.worktreeManager,
|
|
1741
|
-
store: this.store,
|
|
1742
|
-
modelService: this.modelService,
|
|
1743
|
-
pi: this.pi,
|
|
1744
|
-
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
1745
|
-
// [F-5 修复] 同 finalizeRecord——chat 轮路由注销单一汇聚点钩子。
|
|
1746
|
-
onFinalized: (id) => this.unregisterChatRoundRoute(id),
|
|
1747
|
-
sessionDir: this.sessionsDir,
|
|
1748
|
-
},
|
|
1749
|
-
record,
|
|
1750
|
-
doneResult,
|
|
1751
|
-
"closed",
|
|
1752
|
-
"user-close", // close action 主动关闭
|
|
1753
|
-
);
|
|
1754
|
-
// [C-1] 终态通知(设计 D2 路径②):正文空串占位 + sessionFile 指针行(idle 下
|
|
1755
|
-
// 末轮增量已由该轮轮次通知送达,终态再发属重复)。doneResult.text 已改保真
|
|
1756
|
-
// (P-1 修复),正文空由 notifyClosed 的 emptyBody 参数显式表达。
|
|
1757
|
-
// dedup 身份独立于轮次通知(notifyClosed 置 round=undefined),60s 窗内不被吞。
|
|
1758
|
-
// 防重入:closeSubagent 对 closed record 幂等 no-op,本路径不会被二次进入。
|
|
1759
|
-
this.notifyHost.notifyClosed(record, true);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
/**
|
|
1763
|
-
* [M5] closeAfterRound 消费:chatMode 轮次完成时终态化 record(closed + user-close)。
|
|
1764
|
-
*
|
|
1765
|
-
* 由 onRoundSettled(agent_settled 回调)调用——chatMode 轮次完成的统一汇聚点(热路径轮
|
|
1766
|
-
* 不经 runAndFinalize CAS 分支,旧消费点对 chatMode 不可达)。合成 result 沿用 record.result
|
|
1767
|
-
*(= 本轮增量,设计 D2 路径①):本轮增量已由调用方前置的轮次通知送达,终态通知正文因此
|
|
1768
|
-
* 是同一段增量 + 轮次统计 + sessionFile 指针行(notifyClosed),不重发全历史。
|
|
1769
|
-
*
|
|
1770
|
-
* 时序:同步前缀(disarm + kill + CAS)在 session-runner 的 resolveRun(0) 之前执行完——
|
|
1771
|
-
* 冷路径轮的 runAndFinalize 续体因 timer 已 disarm 跳过 early return,但其 tryTransition
|
|
1772
|
-
* CAS 对已 closed 的 record 失败 → 跳过二次 finalize(无双收尾);热路径轮无 runAndFinalize
|
|
1773
|
-
* 续体,本方法是唯一收尾。冷路径续体 .then 的 notifyComplete 与轮次通知同 key=`id:round`,
|
|
1774
|
-
* 60s dedup 吞(不与下方终态通知叠加成第三条——后者 key 是裸 id)。
|
|
1775
|
-
*/
|
|
1776
|
-
private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
|
|
1777
|
-
// 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
|
|
1778
|
-
// [T2④ / LC-2] 终止语义收敛(30s 升级 SIGKILL,终态化后无后续回收通道);
|
|
1779
|
-
// settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
|
|
1780
|
-
// force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
|
|
1781
|
-
disarmIdleTimer(record.id);
|
|
1782
|
-
disarmSettledWatchdog(record.id);
|
|
1783
|
-
disarmRoundFromProtocol(record.id);
|
|
1784
|
-
killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
|
|
1785
|
-
this.terminateChatSession(record, "close", "closeAfterRoundSettled");
|
|
1786
|
-
if (!tryTransition(record, "closed", "user-close")) {
|
|
1787
|
-
return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
|
|
1788
|
-
}
|
|
1789
|
-
const doneResult: AgentResult = {
|
|
1790
|
-
text: record.result ?? "",
|
|
1791
|
-
turns: record.turnCount,
|
|
1792
|
-
durationMs: Date.now() - record.startedAt,
|
|
1793
|
-
success: true,
|
|
1794
|
-
sessionId: record.id,
|
|
1795
|
-
toolCalls: [],
|
|
1796
|
-
};
|
|
1797
|
-
await this.finalizeRecord(record, doneResult, "closed", "user-close");
|
|
1798
|
-
// [C-1] 终态通知(设计 D2 路径①):与前置轮次通知(key=`id:round`)dedup 身份区分,
|
|
1799
|
-
// 「最后一轮轮次通知 + 终态通知」两条都送达父 agent。
|
|
1800
|
-
this.notifyHost.notifyClosed(record);
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// ── 编排层专用接口(workflow 消费)──────────────────────
|
|
1804
|
-
|
|
1805
|
-
/**
|
|
1806
|
-
* workflow 编排层专用:sync-await 接口,内部走 background 管道但返回 Promise<AgentResult>。
|
|
1807
|
-
*
|
|
1808
|
-
* 与 execute() 的区别(D-A1):
|
|
1809
|
-
* 1. 返回 workflow AgentResult(content 字段),非 ExecutionHandle
|
|
1810
|
-
* 2. 不经 chat 轮次 kick-off(detached 回注)→ 不注入 followUp 完成通知(BC-11,结果直接返回 workflow)
|
|
1811
|
-
* 3. T2 删 sync 时 executeAndAwait 不受牵连(独立方法)
|
|
1812
|
-
*
|
|
1813
|
-
* 共享:runSpawn + ConcurrencyPool + record + pending emit(D-A4)。
|
|
1814
|
-
*/
|
|
704
|
+
* workflow 编排层专用 sync-await 接口(D-A1)。本体已迁 RunOrchestration;壳纯转发
|
|
705
|
+
* (SAR / piEngineServiceAdapter 消费)。 */
|
|
1815
706
|
async executeAndAwait(
|
|
1816
707
|
opts: ExecuteOptions,
|
|
1817
708
|
signal?: AbortSignal,
|
|
1818
709
|
onEvent?: (event: AgentEvent) => void,
|
|
1819
710
|
stream?: SubagentStream,
|
|
1820
711
|
): Promise<WorkflowAgentResult> {
|
|
1821
|
-
this.
|
|
1822
|
-
// [T4② / PS-4] 与 execute() 同款入口校验(两入口共享 runAndFinalize → armIdleTimer 链)。
|
|
1823
|
-
this.assertIdleTimeoutMsSafe(opts);
|
|
1824
|
-
|
|
1825
|
-
// ── BC-12 嵌套护栏:复用 execute() 的嵌套上下文深度检查 ──
|
|
1826
|
-
// [ALS 断裂修复] current() 内含基线兜底(与 execute 同)。
|
|
1827
|
-
const parentNesting = this.execNesting.current();
|
|
1828
|
-
const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
|
|
1829
|
-
if (nestingDepth > MAX_FORK_DEPTH) {
|
|
1830
|
-
throw new ForkDepthExceededError(
|
|
1831
|
-
`subagent nesting depth ${nestingDepth} > ${MAX_FORK_DEPTH} (max recursion), refusing to spawn deeper`,
|
|
1832
|
-
);
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// ── 步骤 1: IDENTITY 解析 ──
|
|
1836
|
-
const identity = await this.resolveIdentity(opts);
|
|
1837
|
-
|
|
1838
|
-
// ── 步骤 2: RECORD 创建(mode="background" 进池)──
|
|
1839
|
-
const record = this.createRecordForMode(identity, opts, "background");
|
|
1840
|
-
this.notifyHost.emitPendingRegister(record.id, record.agent);
|
|
1841
|
-
|
|
1842
|
-
// ── 步骤 2.5: worktree creation (only worktree===true; handle injection is execute()'s path) ──
|
|
1843
|
-
// Workflow path receives boolean only (AgentCallOpts.worktree: boolean) — WorktreeHandle is a
|
|
1844
|
-
// main-thread non-serializable object that cannot cross worker postMessage, so no object branch
|
|
1845
|
-
// here (unlike execute() :445-447 which serves the subagent-tool path).
|
|
1846
|
-
// On create failure, finalizeFailed cleans up the record, then
|
|
1847
|
-
// throw lets SAR.run() convert it to an AgentResult.error (not return-handle like execute()).
|
|
1848
|
-
let worktreeHandle: WorktreeHandle | undefined;
|
|
1849
|
-
if (opts.worktree === true) {
|
|
1850
|
-
// [create-await 竞态守卫] 与 execute 同款(见其 worktree 分支注释)——差异仅在
|
|
1851
|
-
// 失败语义:executeAndAwait 对齐「失败 throw」,SAR.run 的 catch 会转成 AgentResult.error
|
|
1852
|
-
// (cancelled 呈现对齐 cancel 抢先路径)。赋值→终态检查→runAndFinalize 同一同步段。
|
|
1853
|
-
// 检查结果经标志位带出 try(守卫 throw 不能落在 try 内——会被下方 catch 当作
|
|
1854
|
-
// create 失败再走 finalizeFailed,对已 closed 的 record 语义未定义)。
|
|
1855
|
-
let cancelledDuringCreate = false;
|
|
1856
|
-
try {
|
|
1857
|
-
worktreeHandle = await this.worktreeManager.create(this.cwd, record.id);
|
|
1858
|
-
record.worktreeHandle = worktreeHandle;
|
|
1859
|
-
if (record.status === "closed") {
|
|
1860
|
-
cancelledDuringCreate = true;
|
|
1861
|
-
}
|
|
1862
|
-
} catch (err) {
|
|
1863
|
-
// finalizeFailed: CAS→finalizeRecord→emitUnregister (record already registered above).
|
|
1864
|
-
// throw (not return-handle): executeAndAwait's caller SAR.run() catches and wraps into
|
|
1865
|
-
// AgentResult.error. Diverges from execute() which returns buildEarlyFailedHandle
|
|
1866
|
-
// because the two methods have different return types.
|
|
1867
|
-
await this.finalizeFailed(record, err);
|
|
1868
|
-
throw err;
|
|
1869
|
-
}
|
|
1870
|
-
if (cancelledDuringCreate) {
|
|
1871
|
-
await this.worktreeManager.cleanup(worktreeHandle);
|
|
1872
|
-
throw new Error(`subagent ${record.id} cancelled during worktree creation`);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
|
|
1877
|
-
const effectiveSignal = signal ?? record.controller?.signal;
|
|
1878
|
-
|
|
1879
|
-
// 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
|
|
1880
|
-
const result = await this.runAndFinalize(
|
|
1881
|
-
record,
|
|
1882
|
-
{ ...opts, worktree: worktreeHandle },
|
|
1883
|
-
identity,
|
|
1884
|
-
effectiveSignal,
|
|
1885
|
-
PRIORITY_BACKGROUND,
|
|
1886
|
-
onEvent,
|
|
1887
|
-
stream,
|
|
1888
|
-
);
|
|
1889
|
-
|
|
1890
|
-
// ── 步骤 6: D-A10 AgentResult 映射 ──
|
|
1891
|
-
// [MF-2] 不在此 emit pending:unregister——runAndFinalize 内部已覆盖所有路径:
|
|
1892
|
-
// - CAS 成功(runAndFinalize L629)→ finalizeRecord 末尾 emit(L797)
|
|
1893
|
-
// - CAS 失败(cancel/finalizeFailed/dispose 抢先转终态)→ 那些路径各自已 emit
|
|
1894
|
-
// (cancelBackground L709 / finalizeFailed→finalizeRecord / dispose L240)
|
|
1895
|
-
// 旧实现无条件 emit 一次 → CAS 成功分支重复 emit(双注销)。
|
|
1896
|
-
const wfResult = mapToWorkflowAgentResult(result);
|
|
1897
|
-
// W2 改动 7:注入 worktreePath(worktree 隔离激活时来自 step 2.5 的 worktreeHandle)。
|
|
1898
|
-
// mapToWorkflowAgentResult 不感知 worktree(它只做 subagents AgentResult → workflow AgentResult
|
|
1899
|
-
// 的 DTO 映射),故在 caller 侧 mutate 刚新建的产物对象(无共享引用,安全)。
|
|
1900
|
-
//
|
|
1901
|
-
// ⚠️ worktreePath is diagnostic only, may not exist — see AgentResult.worktreePath JSDoc
|
|
1902
|
-
// (orchestration/models/types.ts)。下方诊断标识符语义(not cwd)说明同源。
|
|
1903
|
-
//
|
|
1904
|
-
// 诊断标识符语义(not cwd):
|
|
1905
|
-
// - runAndFinalize 内的 finalizeRecord 在 return 前已 cleanup(git worktree remove --force),
|
|
1906
|
-
// worktreePath 指向的目录已被删除,不保证存在。
|
|
1907
|
-
// - worktreePath 仅供日志/trace 关联(如定位某条 session jsonl 的 worktree 来源),无运行时语义。
|
|
1908
|
-
// - **不可作为后续 agent 的 cwd**——目录已删,复用会 ENOENT。
|
|
1909
|
-
// - wave 内 worktree 复用(spec-w §2 "wave 内 8 action 共享 worktree")在 pi 当前架构下
|
|
1910
|
-
// 不可行:worktree 绑定单次 agent() record,每次 executeAndAwait 结束 finalizeRecord
|
|
1911
|
-
// 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd。
|
|
1912
|
-
wfResult.worktreePath = record.worktreeHandle?.path;
|
|
1913
|
-
return wfResult;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// ── 状态查询(TUI 调)──────────────────────────────────
|
|
1917
|
-
|
|
1918
|
-
/** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
|
|
1919
|
-
private onChange(listener: () => void): () => void {
|
|
1920
|
-
return this.store.onChange(listener);
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// [D4] listRunning 已删除:零生产调用方(TUI 计数经 collectRecords / notify-host 的
|
|
1924
|
-
// piAdapter 直调 store.listRunning 覆盖),唯一消费是初始空态单测——保留 store 层方法。
|
|
1925
|
-
|
|
1926
|
-
/** 合并内存(running) + 磁盘(session.jsonl 重建) record(/subagents list + tool list 消费)。
|
|
1927
|
-
* 按 rootSessionId 过滤:根进程=本 session(sessionRootId===sessionId);
|
|
1928
|
-
* 子进程=env 贯穿的真 ROOT(sessionRootId≠sessionId)→ 看到整棵 ROOT 树(决策 3)。
|
|
1929
|
-
* [perf] 磁盘源为 light(头部 identity + 状态,无 eventLog/result/turns 等重数据)
|
|
1930
|
-
* ——列表/补全/hasRunning 够用;详情场景调 getFullRecord(id) 懒加载补齐。 */
|
|
1931
|
-
private collectRecords(limit: number, statusFilter: StatusFilter = "all"): SubagentRecord[] {
|
|
1932
|
-
return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? undefined);
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
/**
|
|
1936
|
-
* collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
|
|
1937
|
-
* startHandler 缺省 collect 解析用)。
|
|
1938
|
-
* config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
|
|
1939
|
-
* 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
|
|
1940
|
-
* reloadGlobalConfig 刷新)。
|
|
1941
|
-
*/
|
|
1942
|
-
/** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
|
|
1943
|
-
* budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
|
|
1944
|
-
private collectSyncSection() {
|
|
1945
|
-
return this.modelService.getGlobalConfig().collectSync;
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
getCollectSyncDefault(): "async" | "sync" {
|
|
1949
|
-
return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
/** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
|
|
1953
|
-
* 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
|
|
1954
|
-
* 返回 undefined:id 不存在于内存与磁盘。 */
|
|
1955
|
-
private getFullRecord(id: string): SubagentRecord | undefined {
|
|
1956
|
-
return this.store.getFullRecord(id);
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
// ── 执行内部:身份解析 + record 创建 ──────────
|
|
1960
|
-
|
|
1961
|
-
/** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。
|
|
1962
|
-
*
|
|
1963
|
-
* [u-h2] pi 未命中跨引擎候选(D2-4):resolveModel 抛 notFoundError(pi registry
|
|
1964
|
-
* 全等裁决未命中)时反查其他已注册引擎清单,唯一命中则追加「该 id 属于引擎 X」
|
|
1965
|
-
* 候选段(场景 3);其余裁决失败(孪生歧义/auth)与命中路径原样返回(零回归)。
|
|
1966
|
-
* execute() 与 executeAndAwait() 两个派发路径共享本方法,故 chat 与 workflow 域的
|
|
1967
|
-
* pi 校验同享场景 3 文案。
|
|
1968
|
-
*/
|
|
1969
|
-
private async resolveIdentity(
|
|
1970
|
-
opts: ExecuteOptions,
|
|
1971
|
-
pre?: { agent: string; agentConfig: AgentConfig | undefined },
|
|
1972
|
-
): Promise<ResolvedIdentity> {
|
|
1973
|
-
// agentRef 语义(S2):agent 参数 = .md 绝对路径;不传 = 不加载 agentConfig,
|
|
1974
|
-
// 直接用 override → 主 agent model。DEFAULT_AGENT_NAME 仅作 record 显示名
|
|
1975
|
-
// (TUI 层 extractAgentName 共用,保证显示一致)。
|
|
1976
|
-
const agent = pre?.agent ?? opts.agent ?? DEFAULT_AGENT_NAME;
|
|
1977
|
-
// 显式 agent ref(用户点名)失败必须报错,不静默降级:无 require 的 loadByPath
|
|
1978
|
-
// 对相对路径/裸名/文件缺失都返回 undefined → agentConfig undefined → resolveModel
|
|
1979
|
-
// 静默回落 override→主 agent model,用户拿到的 subagent 无 systemPrompt/工具白名单
|
|
1980
|
-
// 且零反馈。require:true 让失败抛出带 <available_subagents> 指引的错误(对齐
|
|
1981
|
-
// workflow name not found 反馈风格);不传 agent = 默认 general-purpose 语义,
|
|
1982
|
-
// agentConfig 保持 undefined(合法缺省,走 override → ctxModel 兑底)。
|
|
1983
|
-
// [u-h2 D2-1] execute() 已在路由前解析 agentConfig(pre 通道),此处复用不二次加载。
|
|
1984
|
-
const agentConfig = pre
|
|
1985
|
-
? pre.agentConfig
|
|
1986
|
-
: opts.agent
|
|
1987
|
-
? this.modelService.getRequiredAgentConfig(opts.agent)
|
|
1988
|
-
: undefined;
|
|
1989
|
-
|
|
1990
|
-
let resolved: ResolvedModel;
|
|
1991
|
-
try {
|
|
1992
|
-
resolved = this.modelService.resolveModel(
|
|
1993
|
-
opts.agent ?? "",
|
|
1994
|
-
{ model: opts.model, thinkingLevel: opts.thinkingLevel },
|
|
1995
|
-
opts.ctxModel,
|
|
1996
|
-
agentConfig,
|
|
1997
|
-
);
|
|
1998
|
-
} catch (err) {
|
|
1999
|
-
throw withCrossEngineHint(err, listEngines(), (id) => {
|
|
2000
|
-
try {
|
|
2001
|
-
return getEngine(id);
|
|
2002
|
-
} catch {
|
|
2003
|
-
return undefined; // 清单快照与注册表并发变化的防御:取不到引擎按未注册处理
|
|
2004
|
-
}
|
|
2005
|
-
});
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
return { agent, agentConfig, resolved };
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
/**
|
|
2012
|
-
* [u-h2 D2-1③] 非 pi 引擎的 identity 解析:跳过 pi registry 三层解析(ctxModel 主
|
|
2013
|
-
* agent model 不透传——主 agent 的 pi id 对目标引擎大概率无效,缺省语义归引擎)。
|
|
2014
|
-
*
|
|
2015
|
-
* 逐层语义(设计 D2-1 归趋表):
|
|
2016
|
-
* - model 源 = engineModel(调用参数 opts.model > agentConfig.model frontmatter,
|
|
2017
|
-
* agent 作者声明不忽略——配错在 validateModel 同步报错,不落引擎缺省静默续跑);
|
|
2018
|
-
* - 无显式 model → 校验/留痕走引擎缺省(validateModel(undefined) 的 canonicalRef);
|
|
2019
|
-
* - thinkingLevel 直接透传(引擎中立参数,不涉 registry)。
|
|
2020
|
-
*
|
|
2021
|
-
* 引擎未实现 validateModel 时 modelRef 原样透传(其 prepare 期校验兜底,现状语义)。
|
|
2022
|
-
*/
|
|
2023
|
-
private resolveIdentityForEngine(
|
|
2024
|
-
engine: EnginePort,
|
|
2025
|
-
engineModel: string | undefined,
|
|
2026
|
-
agent: string,
|
|
2027
|
-
agentConfig: AgentConfig | undefined,
|
|
2028
|
-
opts: ExecuteOptions,
|
|
2029
|
-
): ResolvedIdentity {
|
|
2030
|
-
const canonical = validateModelForEngine(engine, engineModel);
|
|
2031
|
-
// record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
|
|
2032
|
-
// 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
|
|
2033
|
-
// 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
|
|
2034
|
-
// 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
|
|
2035
|
-
// 整串进 name,不再落 "<ref>/" 畸形)。
|
|
2036
|
-
const modelStr = canonical ?? engineModel ?? "";
|
|
2037
|
-
const model = splitEngineModelRef(modelStr);
|
|
2038
|
-
return {
|
|
2039
|
-
agent,
|
|
2040
|
-
agentConfig,
|
|
2041
|
-
resolved: {
|
|
2042
|
-
model: {
|
|
2043
|
-
id: model.id,
|
|
2044
|
-
name: model.name,
|
|
2045
|
-
provider: model.provider,
|
|
2046
|
-
reasoning: false,
|
|
2047
|
-
},
|
|
2048
|
-
thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
2049
|
-
},
|
|
2050
|
-
};
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
/** 步骤 2:按 mode 生成 id + controller,创建 record 并注册。
|
|
2054
|
-
* [L-1] ExecutionMode 类型固定 "background"(sync 已删除),id/controller 分支简化。 */
|
|
2055
|
-
private createRecordForMode(
|
|
2056
|
-
identity: ResolvedIdentity,
|
|
2057
|
-
opts: ExecuteOptions,
|
|
2058
|
-
mode: ExecutionMode,
|
|
2059
|
-
): ExecutionRecord {
|
|
2060
|
-
// FR-1: record id 用全局 UUID,不依赖 transcript/PID
|
|
2061
|
-
const id = `sa-${crypto.randomUUID()}`;
|
|
2062
|
-
const controller = new AbortController();
|
|
2063
|
-
|
|
2064
|
-
// 从 async 调用链读父执行上下文:主 session 链上无 store → 顶层 record;
|
|
2065
|
-
// B run() 期间包了嵌套上下文,B 内创建 C 时读到 B → C.parentRecordId=B.id, C.depth=B.depth+1。
|
|
2066
|
-
// depth 语义:顶层(无父)=0;有父=父 depth+1。靠 recordId 是否存在区分,不用负数魔数。
|
|
2067
|
-
// [ALS 断裂修复] current() 内含基线兜底——本进程的身份在 initSession 已确定(env 注入),
|
|
2068
|
-
// 任何上下文下都能正确挂父链。
|
|
2069
|
-
const parentCtx = this.execNesting.current();
|
|
2070
|
-
const parentRecordId = parentCtx?.recordId;
|
|
2071
|
-
const depth = parentCtx ? parentCtx.depth + 1 : 0;
|
|
2072
|
-
|
|
2073
|
-
const record = createRecord(id, {
|
|
2074
|
-
agent: identity.agent,
|
|
2075
|
-
// model 留痕词形与拆分同源(joinEngineModelRef):provider 为空串只写 id——
|
|
2076
|
-
// 契约变更④的无斜杠 ref(provider=""/id=ref)不得落成 "/ref" 或 "ref/" 畸形;
|
|
2077
|
-
// 续聊回读侧 splitEngineModelRef 对无斜杠串还原 provider=""/id=ref,往返自洽。
|
|
2078
|
-
model: joinEngineModelRef(identity.resolved.model),
|
|
2079
|
-
thinkingLevel: identity.resolved.thinkingLevel,
|
|
2080
|
-
mode,
|
|
2081
|
-
task: opts.task,
|
|
2082
|
-
slug: opts.slug,
|
|
2083
|
-
startedAt: Date.now(),
|
|
2084
|
-
rootSessionId: this.sessionRootId ?? undefined,
|
|
2085
|
-
parentRecordId,
|
|
2086
|
-
depth,
|
|
2087
|
-
chatMode: opts.conversation === true,
|
|
2088
|
-
idleTimeoutMs: opts.idleTimeoutMs,
|
|
2089
|
-
// P4 引擎留痕(D9①):opts.engine/engineFallback 由引擎适配层写入(PiEngine.run
|
|
2090
|
-
// 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
|
|
2091
|
-
engine: opts.engine,
|
|
2092
|
-
engineFallback: opts.engineFallback,
|
|
2093
|
-
// subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
|
|
2094
|
-
// 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
|
|
2095
|
-
// undefined = async(缺省语义,旧记录零迁移)。
|
|
2096
|
-
collectMode: opts.collect === "sync" ? "sync" : undefined,
|
|
2097
|
-
controller,
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
this.store.register(record);
|
|
2101
|
-
return record;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
/** [MF#R4] worktree 前置失败的 early-return handle。
|
|
2105
|
-
* record 已被 finalizeFailed 收尾为 failed、detached promise 从未启动。 */
|
|
2106
|
-
private buildEarlyFailedHandle(record: ExecutionRecord): ExecutionHandle {
|
|
2107
|
-
const details = project(record);
|
|
2108
|
-
return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details };
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
// ── 引擎分支(D4/D10:非 pi 引擎的 chat 域执行骨架,U0)──────────
|
|
2112
|
-
|
|
2113
|
-
/**
|
|
2114
|
-
* chat 域统一执行入口(D2 单轨:全引擎——含 pi——经此进入 EnginePort)。路由
|
|
2115
|
-
*(routeEngineForHost:三层 + pi 同步短路 + probe/守卫)已由 execute 完成——这里
|
|
2116
|
-
* 只剩 unsupported 预检 → identity(pi 链解析 / 非 pi 按目标引擎校验,
|
|
2117
|
-
* [u-h2 D2-1/D2-2])→ record 创建+盖章 → worktree → detached 引擎 run。
|
|
2118
|
-
* 全部同步拒绝发生在 record 创建前(不产生孤儿 record)。
|
|
2119
|
-
*/
|
|
2120
|
-
private async executeViaEngine(
|
|
2121
|
-
opts: ExecuteOptions,
|
|
2122
|
-
preIdentity: { agent: string; agentConfig: AgentConfig | undefined },
|
|
2123
|
-
route: EngineRouteResult,
|
|
2124
|
-
mode: ExecutionMode,
|
|
2125
|
-
): Promise<ExecutionHandle> {
|
|
2126
|
-
const engine = route.engine;
|
|
2127
|
-
// [D3-④ 预检 capabilities 化] 唯一实现 = common/capability-gate(capabilities
|
|
2128
|
-
// 驱动,含 maxTurns 扩位)。检查点钉死:execute/executeViaEngine 同步段、record
|
|
2129
|
-
// 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
|
|
2130
|
-
// 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
|
|
2131
|
-
// 检查若只落在 engine.run 内则拒绝异步化为「派发成功 + 静默失败 record」)。
|
|
2132
|
-
// [W3 契约变更③补注(协议化能力位方向判定)] 同步拒只覆盖「manifest 少声明」
|
|
2133
|
-
// 方向;**manifest 多声明**(声明支持而引擎实际不支持)由首个 run 的协议握手
|
|
2134
|
-
// `initialize` 发现 → engine_capability_mismatch 该 run 失败 + record 标 failed,
|
|
2135
|
-
// 并**清理 run 前已建的前置副作用**(worktree 经 finalizeFailed → finalizeRecord
|
|
2136
|
-
// Step 3b cleanupWorktreeIfBound 清理)。非 gate 位不一致(无论强弱)一律
|
|
2137
|
-
// warn 留痕不阻断(诊断面归 EngineClient,设计 §3.3 能力位段)。
|
|
2138
|
-
assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
|
|
2139
|
-
|
|
2140
|
-
// identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
|
|
2141
|
-
// - pi:pi 链三层解析在路由后执行(现状三层解析链行为零变化;含 resolveModel
|
|
2142
|
-
// 失败的跨引擎候选提示,D2-4);
|
|
2143
|
-
// - 非 pi:跳过 pi registry,model 按目标引擎校验(同步期 throw,record 创建前
|
|
2144
|
-
// ——场景 2 错误;ctxModel 不透传,缺省语义归引擎,D2-1③)。
|
|
2145
|
-
const isPiRoute = route.engineId === DEFAULT_ENGINE_ID;
|
|
2146
|
-
// [u-h2 D2-1③] 非 pi 的 model 源 = 调用参数 > agent .md frontmatter(作者声明不
|
|
2147
|
-
// 忽略)——frontmatter 声明须真正透传给引擎(taskSpec.model 消费 opts.model),
|
|
2148
|
-
// 不能只进 record 留痕;无显式 model 时引擎落自身缺省(validateModel(undefined) 裁决)。
|
|
2149
|
-
const engineModel = isPiRoute ? undefined : (opts.model ?? preIdentity.agentConfig?.model);
|
|
2150
|
-
const identity = isPiRoute
|
|
2151
|
-
? await this.resolveIdentity(opts, preIdentity)
|
|
2152
|
-
: this.resolveIdentityForEngine(engine, engineModel, preIdentity.agent, preIdentity.agentConfig, opts);
|
|
2153
|
-
|
|
2154
|
-
// record 盖章路由结果(D5 字节级守护的执行侧落点):
|
|
2155
|
-
// - pi 纯缺省/显式 pi:不盖 engine 键(pi record entry 序列化产物不得新增 engine
|
|
2156
|
-
// 键,undefined 经 JSON 省略)——与旧 pi 主路径 piOpts 剥离语义逐字节一致;
|
|
2157
|
-
// - pi 兜底:engine='pi' + engineFallback 留痕(engine = 实际执行引擎,from=请求
|
|
2158
|
-
// 引擎留痕);
|
|
2159
|
-
// - 非 pi:engine=route.engineId 显式留痕(+engineFallback 如有)+ model 覆写
|
|
2160
|
-
// (frontmatter 声明透传,u-h2 D2-1③)。
|
|
2161
|
-
const recordOpts: ExecuteOptions = isPiRoute
|
|
2162
|
-
? route.engineFallback !== undefined
|
|
2163
|
-
? { ...opts, engine: DEFAULT_ENGINE_ID, engineFallback: route.engineFallback }
|
|
2164
|
-
: opts.engine === undefined
|
|
2165
|
-
? opts
|
|
2166
|
-
: { ...opts, engine: undefined }
|
|
2167
|
-
: {
|
|
2168
|
-
...opts,
|
|
2169
|
-
...(engineModel !== undefined ? { model: engineModel } : {}),
|
|
2170
|
-
engine: route.engineId,
|
|
2171
|
-
...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
|
|
2172
|
-
};
|
|
2173
|
-
const record = this.createRecordForMode(identity, recordOpts, mode);
|
|
2174
|
-
this.notifyHost.emitPendingRegister(record.id, record.agent);
|
|
2175
|
-
|
|
2176
|
-
// ── worktree 创建(仅 worktree===true 或已传入 handle 时)──
|
|
2177
|
-
// record 先创建,worktree 失败时可 finalizeFailed(record 已在 store 中)。
|
|
2178
|
-
// worktree 必须显式开启:worktree===true 创建新 worktree;worktree===undefined/false 不创建。
|
|
2179
|
-
// fork 不隐含 worktree(UC-1 fork 可独立使用,fork 仅继承上下文,在 parent cwd 跑)。
|
|
2180
|
-
// 非 pi 引擎带 worktree 已被上方预检同步拒绝(caps.sandbox='none'),此段实际仅
|
|
2181
|
-
// sandbox 能力引擎(pi:caps.sandbox='emulated')可达。
|
|
2182
|
-
let worktreeHandle: WorktreeHandle | undefined;
|
|
2183
|
-
if (typeof opts.worktree === "object") {
|
|
2184
|
-
// 传入的是已创建的 WorktreeHandle
|
|
2185
|
-
worktreeHandle = opts.worktree;
|
|
2186
|
-
} else if (opts.worktree === true) {
|
|
2187
|
-
// worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
|
|
2188
|
-
try {
|
|
2189
|
-
worktreeHandle = await this.worktreeManager.create(this.cwd, record.id);
|
|
2190
|
-
record.worktreeHandle = worktreeHandle;
|
|
2191
|
-
// [create-await 竞态守卫] create 的 await 窗口内 cancel/dispose 可 CAS 把 record
|
|
2192
|
-
// 转成 closed 终态——cancelBackground 当时读到的 worktreeHandle 可能仍是 undefined
|
|
2193
|
-
// (cleanup 被跳过)。赋值后同同步段检查终态:closed 则主动 cleanup(幂等,抢先的
|
|
2194
|
-
// fire-and-forget 清理无害)+ early-failed 返回,不进轮次 kick-off(避免子进程白跑)。
|
|
2195
|
-
// 实现约束:赋值 → 终态检查 → kick-off 必须在同一同步段,中间禁止插入 await。
|
|
2196
|
-
if (record.status === "closed") {
|
|
2197
|
-
await this.worktreeManager.cleanup(worktreeHandle);
|
|
2198
|
-
return this.buildEarlyFailedHandle(record);
|
|
2199
|
-
}
|
|
2200
|
-
} catch (err) {
|
|
2201
|
-
// create 失败→不进入 run,finalizeFailed 统一收尾(含 emitPendingUnregister failed)
|
|
2202
|
-
const _result = await this.finalizeFailed(record, err);
|
|
2203
|
-
return this.buildEarlyFailedHandle(record);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
if (isPiRoute) {
|
|
2208
|
-
// pi:chat 轮次经协议 run(会话形态 chat{recordId})发往 pi-subagent-cli 引擎
|
|
2209
|
-
// 进程(W3——与 run 域 remote-engine 路由同构),编排收尾(notify/终态迁移)
|
|
2210
|
-
// 与旧 pi 主路径语义一致。
|
|
2211
|
-
this.kickOffChatRound(
|
|
2212
|
-
record,
|
|
2213
|
-
{ ...recordOpts, worktree: worktreeHandle },
|
|
2214
|
-
identity,
|
|
2215
|
-
record.controller!.signal,
|
|
2216
|
-
PRIORITY_BACKGROUND,
|
|
2217
|
-
);
|
|
2218
|
-
} else {
|
|
2219
|
-
// 非 pi 引擎:engine.run 自足执行(handle+outcome),编排侧 journal 接线 + 终态迁移
|
|
2220
|
-
this.kickOffEngineRun(record, recordOpts, engine);
|
|
2221
|
-
}
|
|
2222
|
-
return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: project(record) };
|
|
712
|
+
return this.runOrchestration.executeAndAwait(opts, signal, onEvent, stream);
|
|
2223
713
|
}
|
|
2224
714
|
|
|
2225
715
|
/**
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
*/
|
|
2233
|
-
private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
|
|
2234
|
-
const signal = record.controller?.signal;
|
|
2235
|
-
// [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
|
|
2236
|
-
this.roundSupervisor.noteRunStarted(record.id);
|
|
2237
|
-
void (async () => {
|
|
2238
|
-
try {
|
|
2239
|
-
await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
|
|
2240
|
-
} catch {
|
|
2241
|
-
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致(runAndFinalize 同款)
|
|
2242
|
-
if (signal?.aborted) {
|
|
2243
|
-
await this.finalizeAborted(record);
|
|
2244
|
-
} else {
|
|
2245
|
-
await this.finalizeFailed(record, new Error("aborted"));
|
|
2246
|
-
}
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
// [review MF1] acquire 成功后必须 finally release:不 release 则
|
|
2250
|
-
// DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
|
|
2251
|
-
// maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
|
|
2252
|
-
try {
|
|
2253
|
-
// [W4] adopted = 走了表 3 行 1 接管分支(record 保持 resumable 交监督器)——
|
|
2254
|
-
// 跳过 bg 完成回注(合并单条通知由监督器 sendMergedFailureNotice 承担,
|
|
2255
|
-
// route 会再发一条 toNotifyRecord 投影通知 = 双通知,正是 R3 要消除的时序窗口)。
|
|
2256
|
-
const adopted = await this.runEngineTask(record, opts, engine, signal);
|
|
2257
|
-
// cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
|
|
2258
|
-
if (!adopted && record.closedReason !== "cancelled") {
|
|
2259
|
-
this.collectCoordinator.route(record);
|
|
2260
|
-
}
|
|
2261
|
-
} finally {
|
|
2262
|
-
this.pool.release();
|
|
2263
|
-
this.roundSupervisor.noteRunEnded(record.id);
|
|
2264
|
-
}
|
|
2265
|
-
})();
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
/**
|
|
2269
|
-
* kickOffEngineRun 的 acquire 后主体:journal 接线(D6 第②级:taskId=record.id,
|
|
2270
|
-
* 初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际池 key)→ engine.run
|
|
2271
|
-
* (signal 接 record controller,kill-chain 两级生效)→ engineHandle 回填(终态迁移
|
|
2272
|
-
* 落 entry 前)→ 终态迁移。bg notify 归编排侧(与 pi 轮次收尾通知归编排对称)。
|
|
2273
|
-
*/
|
|
2274
|
-
private async runEngineTask(
|
|
2275
|
-
record: ExecutionRecord,
|
|
2276
|
-
opts: ExecuteOptions,
|
|
2277
|
-
engine: EnginePort,
|
|
2278
|
-
signal: AbortSignal | undefined,
|
|
2279
|
-
): Promise<boolean> {
|
|
2280
|
-
// [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
|
|
2281
|
-
//(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
|
|
2282
|
-
// 不传 forwardEvents。
|
|
2283
|
-
const journal = wireEventJournal({ engineId: engine.id, taskId: record.id });
|
|
2284
|
-
// [R4 §3.4 不变量 3] 运行中句柄回填:create 应答后(远早于 run resolve)回填
|
|
2285
|
-
// record.engineHandle 并经 reportRecordTransition 落 entry——运行中的 GUI 经 entry
|
|
2286
|
-
// 重建 record 即拿到 ①②级读取钥匙(sessionRef/dbPath/poolKey/journalPath),
|
|
2287
|
-
// 不再等终态回填(详情页中途打开可见当时进度快照)。journalPath 此时已是
|
|
2288
|
-
// retarget 后的最终路径(onPoolResolved 在 prepare 期先行触发,writer 是路径权威)。
|
|
2289
|
-
// 仅 chat 域接线——workflow 域 SAR 无运行中 record 读取方,刻意不做同类回填(防误扩展)。
|
|
2290
|
-
const backfillEngineHandle = (partial: { sessionRef: Record<string, string>; poolKey: string }): void => {
|
|
2291
|
-
if (record.engineHandle !== undefined && record.engineHandle.sessionRef["sessionId"] !== undefined) {
|
|
2292
|
-
return; // 幂等守卫:终态回填已落(迟到重复回调不覆盖)
|
|
2293
|
-
}
|
|
2294
|
-
record.engineHandle = {
|
|
2295
|
-
sessionRef: partial.sessionRef,
|
|
2296
|
-
poolKey: partial.poolKey,
|
|
2297
|
-
journalPath: journal.path,
|
|
2298
|
-
};
|
|
2299
|
-
this.store.reportRecordTransition(record);
|
|
2300
|
-
};
|
|
2301
|
-
// 对齐点③:journal 路径权威 = 引擎声明的池 key(writer 初始用占位,retarget 后
|
|
2302
|
-
// 与 handle.poolKey 同源)。
|
|
2303
|
-
const runCtx: RunContext = {
|
|
2304
|
-
taskId: record.id,
|
|
2305
|
-
poolKey: JOURNAL_INITIAL_POOL_KEY,
|
|
2306
|
-
signal,
|
|
2307
|
-
ctxModel: opts.ctxModel,
|
|
2308
|
-
onEvent: journal.onEvent,
|
|
2309
|
-
onPoolResolved: journal.onPoolResolved,
|
|
2310
|
-
// [R4 §3.4 不变量 3] 运行中句柄回填通道(engine/port.ts RunContext.onHandleReady)
|
|
2311
|
-
onHandleReady: backfillEngineHandle,
|
|
2312
|
-
// D9①:路由层 fallback 留痕投影进 outcome(zcode 无独立 record 通路)
|
|
2313
|
-
...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
|
|
2314
|
-
// D10 终止链:engine spawn 的子进程注册进 spawnedChildren 记账
|
|
2315
|
-
//(cancelBackground SIGTERM / dispose killAll 收割对非 pi record 生效)
|
|
2316
|
-
onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
|
|
2317
|
-
};
|
|
2318
|
-
try {
|
|
2319
|
-
const { handle, outcome } = await engine.run(executeOptionsToEngineTaskSpec(opts), runCtx);
|
|
2320
|
-
// engineHandle 终态回填(U2:终态迁移落 entry 前;R4 起为兜底面——运行中回填
|
|
2321
|
-
// 已由 onHandleReady 提前落 entry,此处覆写终态权威值)。sessionRef 整体透传——
|
|
2322
|
-
// 失败终态 sessionId 缺失时也回填已有部分(dbPath/poolKey),读侧①级降②级
|
|
2323
|
-
// 的防御形态;journalPath 取 retarget 后的实际落盘路径(writer 是路径权威)。
|
|
2324
|
-
record.engineHandle = {
|
|
2325
|
-
sessionRef: handle.data.sessionRef,
|
|
2326
|
-
poolKey: handle.data.poolKey,
|
|
2327
|
-
journalPath: journal.path,
|
|
2328
|
-
};
|
|
2329
|
-
await journal.close();
|
|
2330
|
-
return await this.finalizeEngineOutcome(record, outcome);
|
|
2331
|
-
} catch (err) {
|
|
2332
|
-
// engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
|
|
2333
|
-
// journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
|
|
2334
|
-
await journal.close();
|
|
2335
|
-
// [W4 表 3 行 1] 引擎进程死亡(engine_crashed——run 帧已受理后进程死亡/stdin
|
|
2336
|
-
// 写失败)且宿主存活 → record 保持 resumable 交监督器接管(禁 completed 谎报、
|
|
2337
|
-
// 禁直接 closed 终局——resume 锚点在盘,逻辑任务可续)。prepare 期失败
|
|
2338
|
-
// (handshake/protocol/model 拒——进程创建前)维持现状 closed(确定性失败,
|
|
2339
|
-
// 保持 resumable 无意义)。
|
|
2340
|
-
if (
|
|
2341
|
-
err instanceof EngineSdkError &&
|
|
2342
|
-
err.code === "engine_crashed" &&
|
|
2343
|
-
record.chatMode !== true &&
|
|
2344
|
-
record.status === "running"
|
|
2345
|
-
) {
|
|
2346
|
-
this.adoptResumableAfterEngineDeath(record, toErrorMessage(err));
|
|
2347
|
-
return true;
|
|
2348
|
-
}
|
|
2349
|
-
await this.finalizeFailed(record, err);
|
|
2350
|
-
return false;
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
/**
|
|
2355
|
-
* [W4 表 3 行 1] 引擎/子进程死亡、宿主存活的 record 处置:run 终态如实记 failed
|
|
2356
|
-
* 证据(record.error),record **保持 resumable**(session 文件在盘,逻辑任务可续
|
|
2357
|
-
* ——冷路径 resume 可直接续写),交监督器接管(合并单条通知 + 三态判定)。禁止
|
|
2358
|
-
* 两个事故方向:completed 谎报(G3)与 closed 直接终局(resume 可能性丢失,等待
|
|
2359
|
-
* 无主——2026-09-08 事故环 2/3 的 core 侧形态)。
|
|
2360
|
-
*
|
|
2361
|
-
* 判据状态源钉死 record 级:写点只动 record 字段(resumable/result/error),
|
|
2362
|
-
* 不清镜像不查引擎——引擎进程被动重建(ensureConnected 退避重填镜像)不翻转
|
|
2363
|
-
* 本处置(纳管模型:死亡事件纳管、重建不解管)。
|
|
2364
|
-
*/
|
|
2365
|
-
private adoptResumableAfterEngineDeath(record: ExecutionRecord, errMsg: string): void {
|
|
2366
|
-
record.error = errMsg;
|
|
2367
|
-
record.result = undefined;
|
|
2368
|
-
record.resumable = true;
|
|
2369
|
-
this.store.reportRecordTransition(record);
|
|
2370
|
-
this.roundSupervisor.adoptOnProcessDeath(record, errMsg);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
/**
|
|
2374
|
-
* 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
|
|
2375
|
-
* 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
|
|
2376
|
-
*/
|
|
2377
|
-
private effectiveMaxConcurrentFor(record: ExecutionRecord): number {
|
|
2378
|
-
return Math.max(1, this.pool.maxConcurrent - record.depth);
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
/**
|
|
2382
|
-
* engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
|
|
2383
|
-
* 否则 done(result=content)。CAS 抢锁(tryTransition)防与 cancelBackground 双收尾。
|
|
2384
|
-
*
|
|
2385
|
-
* [W4 表 3 行 1] 进程死亡分诊:outcome.error 存在且 **exitCode === null**(引擎侧
|
|
2386
|
-
* 进程被信号终止/崩溃的合成 outcome 形态——RemoteEngine 运行中失败合成分支恒
|
|
2387
|
-
* exitCode:null,引擎如实上报的 turn 失败带数值 exitCode;engine-client 注释同源
|
|
2388
|
-
* 「exitCode null = 被信号杀死,杀链判据」)且宿主存活且非 conversation 形态 →
|
|
2389
|
-
* record 保持 resumable 交监督器接管(如实 failed 证据 + 合并通知 + 三态判定),
|
|
2390
|
-
* 不再 closed 终局。返回 true = 走了接管分支(调用方跳过 bg 完成回注)。
|
|
2391
|
-
*/
|
|
2392
|
-
private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
|
|
2393
|
-
if (outcome.sessionFile !== undefined) {
|
|
2394
|
-
record.sessionFile = outcome.sessionFile;
|
|
2395
|
-
}
|
|
2396
|
-
if (
|
|
2397
|
-
outcome.error !== undefined &&
|
|
2398
|
-
outcome.exitCode === null &&
|
|
2399
|
-
record.chatMode !== true &&
|
|
2400
|
-
record.status === "running"
|
|
2401
|
-
) {
|
|
2402
|
-
this.adoptResumableAfterEngineDeath(record, outcome.error);
|
|
2403
|
-
return true;
|
|
2404
|
-
}
|
|
2405
|
-
const result = this.outcomeToAgentResult(record, outcome);
|
|
2406
|
-
if (tryTransition(record, "closed", "gc")) {
|
|
2407
|
-
await this.finalizeRecord(record, result, "closed", "gc");
|
|
2408
|
-
}
|
|
2409
|
-
return false;
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
// ── 执行内部:run + finalize(workflow 域 sync 面;chat 域轮次走 kickOffChatRound)──
|
|
2413
|
-
|
|
2414
|
-
/**
|
|
2415
|
-
* workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
|
|
2416
|
-
* runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
|
|
2417
|
-
* 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
|
|
2418
|
-
*/
|
|
2419
|
-
private async runAndFinalize(
|
|
2420
|
-
record: ExecutionRecord,
|
|
2421
|
-
opts: ExecuteOptions,
|
|
2422
|
-
identity: ResolvedIdentity,
|
|
2423
|
-
signal: AbortSignal | undefined,
|
|
2424
|
-
priority: number,
|
|
716
|
+
* [H2 W2] workflow 域统一派发入口。本体已迁 WorkflowDispatch([D-R4-1] workflow 族
|
|
717
|
+
* 独立聚合);壳纯转发,对外签名不变(SAR / pump 消费)。 */
|
|
718
|
+
async executeWorkflowAgent(
|
|
719
|
+
opts: AgentCallOpts,
|
|
720
|
+
parentRunId: string,
|
|
721
|
+
signal?: AbortSignal,
|
|
2425
722
|
onEvent?: (event: AgentEvent) => void,
|
|
2426
723
|
stream?: SubagentStream,
|
|
2427
|
-
): Promise<
|
|
2428
|
-
|
|
2429
|
-
let acquired = false;
|
|
2430
|
-
if (pooled) {
|
|
2431
|
-
const acquireFailure = await this.acquirePoolOrFinalize(record, signal, priority);
|
|
2432
|
-
if (acquireFailure !== undefined) return acquireFailure;
|
|
2433
|
-
acquired = true;
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
// worktree 句柄经 executeOptionsToEngineTaskSpec(opts).worktree 直传引擎
|
|
2437
|
-
//(AgentCallOpts.worktree 接受 WorktreeHandle——原 runSpawn 显式传参的协议形态)。
|
|
2438
|
-
const engine = this.resolveChatEnginePort();
|
|
2439
|
-
// journal 接线(D6 第②级,与 kickOffEngineRun 同一实现):forwardEvents = onEvent
|
|
2440
|
-
//(workflow liveRecord 桥接,D-A8)。
|
|
2441
|
-
const journal = wireEventJournal({ engineId: engine.id, taskId: record.id, forwardEvents: onEvent });
|
|
2442
|
-
|
|
2443
|
-
let result: AgentResult;
|
|
2444
|
-
try {
|
|
2445
|
-
const runCtx: RunContext = {
|
|
2446
|
-
taskId: record.id,
|
|
2447
|
-
poolKey: JOURNAL_INITIAL_POOL_KEY,
|
|
2448
|
-
signal,
|
|
2449
|
-
ctxModel: identity.resolved.model,
|
|
2450
|
-
onEvent: journal.onEvent,
|
|
2451
|
-
onPoolResolved: journal.onPoolResolved,
|
|
2452
|
-
...(stream !== undefined ? { stream } : {}),
|
|
2453
|
-
...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
|
|
2454
|
-
// D10 终止链:引擎 spawn 的子进程注册进 spawnedChildren 镜像记账
|
|
2455
|
-
onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
|
|
2456
|
-
};
|
|
2457
|
-
const { handle, outcome } = await engine.run(this.taskSpecWithModel(opts, record.model), runCtx);
|
|
2458
|
-
record.engineHandle = {
|
|
2459
|
-
sessionRef: handle.data.sessionRef,
|
|
2460
|
-
poolKey: handle.data.poolKey,
|
|
2461
|
-
journalPath: journal.path,
|
|
2462
|
-
};
|
|
2463
|
-
await journal.close();
|
|
2464
|
-
result = this.outcomeToAgentResult(record, outcome);
|
|
2465
|
-
} catch (err) {
|
|
2466
|
-
await journal.close();
|
|
2467
|
-
// engine.run prepare 期 reject(进程创建前)→ 合成 failed result + 收尾。
|
|
2468
|
-
// swallow(不 re-throw):sync 调用方拿到合成 failed result,避免异常逃逸到
|
|
2469
|
-
// tool 层 + record 卡 running。
|
|
2470
|
-
if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
|
|
2471
|
-
return this.finalizeFailed(record, err);
|
|
2472
|
-
} finally {
|
|
2473
|
-
this.releaseRoundResources(record, pooled && acquired, stream);
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
// v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
|
|
2477
|
-
await this.settleOneShotOutcome(record, result, signal?.aborted === true);
|
|
2478
|
-
return result;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
/** engine.run taskSpec 装配单一来源(runAndFinalize 与 kickOffChatRound 共用):
|
|
2482
|
-
* executeOptions 协议映射 + model = record 留痕词形(resolved 解析产物,
|
|
2483
|
-
* joinEngineModelRef 规范形)覆盖——原 identity.resolved 经 runSpawn --model
|
|
2484
|
-
* 兜底的协议等价承载。 */
|
|
2485
|
-
private taskSpecWithModel(opts: ExecuteOptions, model: string | undefined): AgentCallOpts {
|
|
2486
|
-
return { ...executeOptionsToEngineTaskSpec(opts), ...(model !== undefined ? { model } : {}) };
|
|
724
|
+
): Promise<WorkflowAgentResult> {
|
|
725
|
+
return this.workflowDispatch.executeWorkflowAgent(opts, parentRunId, signal, onEvent, stream);
|
|
2487
726
|
}
|
|
2488
727
|
|
|
2489
728
|
/**
|
|
2490
|
-
* one-shot
|
|
2491
|
-
*
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
*/
|
|
2495
|
-
private async settleOneShotOutcome(
|
|
2496
|
-
record: ExecutionRecord,
|
|
2497
|
-
result: AgentResult,
|
|
2498
|
-
aborted: boolean,
|
|
2499
|
-
): Promise<void> {
|
|
2500
|
-
if (!tryTransition(record, "closed", aborted ? "cancelled" : result.success ? "user-close" : "gc")) return;
|
|
2501
|
-
if (!aborted && result.success && record.closeAfterRound === true) {
|
|
2502
|
-
// [M5] busy 时 close(force:false) 置的标志在本轮完成时消费终态化。
|
|
2503
|
-
await this.consumeCloseAfterRound(record, result, "user-close");
|
|
2504
|
-
} else if (!aborted && result.success) {
|
|
2505
|
-
// [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
|
|
2506
|
-
await this.finalizeRoundToIdle(record, result);
|
|
2507
|
-
} else if (!aborted && record.closeAfterRound === true) {
|
|
2508
|
-
// [M5] 优雅关闭挂起的失败轮:轮已完成即兑现 close 意图终态化(含本轮 result)。
|
|
2509
|
-
await this.consumeCloseAfterRound(record, result, "gc");
|
|
2510
|
-
} else {
|
|
2511
|
-
// 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
|
|
2512
|
-
await this.finalizeRecord(record, result, "closed", aborted ? "cancelled" : "gc");
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
/** AgentOutcome → execution AgentResult 单一映射源(workflow run 域映射;
|
|
2517
|
-
* finalizeEngineOutcome 终态 result 复用此处构造——exitCode null = 被信号杀死的
|
|
2518
|
-
* 合成终态,error 如实透传)。 */
|
|
2519
|
-
private outcomeToAgentResult(record: ExecutionRecord, outcome: AgentOutcome): AgentResult {
|
|
2520
|
-
if (outcome.sessionFile !== undefined) {
|
|
2521
|
-
record.sessionFile = outcome.sessionFile;
|
|
2522
|
-
}
|
|
2523
|
-
return {
|
|
2524
|
-
text: outcome.content,
|
|
2525
|
-
turns: outcome.usage?.turns ?? 0,
|
|
2526
|
-
durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
|
|
2527
|
-
success: outcome.error === undefined,
|
|
2528
|
-
...(outcome.error !== undefined ? { error: outcome.error } : {}),
|
|
2529
|
-
sessionId: outcome.sessionId ?? record.id,
|
|
2530
|
-
toolCalls: [],
|
|
2531
|
-
...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
|
|
2532
|
-
};
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
/** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
|
|
2536
|
-
* 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
|
|
2537
|
-
* 不触发轮次资源回收——与原控制流逐字节一致)。 */
|
|
2538
|
-
private async acquirePoolOrFinalize(
|
|
2539
|
-
record: ExecutionRecord,
|
|
2540
|
-
signal: AbortSignal | undefined,
|
|
2541
|
-
priority: number,
|
|
2542
|
-
): Promise<AgentResult | undefined> {
|
|
2543
|
-
try {
|
|
2544
|
-
await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
|
|
2545
|
-
} catch {
|
|
2546
|
-
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
2547
|
-
if (signal?.aborted) return this.finalizeAborted(record);
|
|
2548
|
-
return this.finalizeFailed(record, new Error("aborted"));
|
|
2549
|
-
}
|
|
2550
|
-
return undefined;
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
/** [U04 提取·装配] 解析 worktree 参数:boolean → WorktreeHandle | undefined(true/undefined 由 run 内部处理)。 */
|
|
2554
|
-
private resolveWorktreeHandle(opts: ExecuteOptions): WorktreeHandle | undefined {
|
|
2555
|
-
return typeof opts.worktree === "object" ? opts.worktree : undefined;
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
/** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
|
|
2559
|
-
* 成功)、streaming widget 清除。 */
|
|
2560
|
-
private releaseRoundResources(
|
|
2561
|
-
_record: ExecutionRecord,
|
|
2562
|
-
holdSlot: boolean,
|
|
2563
|
-
stream: SubagentStream | undefined,
|
|
2564
|
-
): void {
|
|
2565
|
-
if (holdSlot) this.pool.release();
|
|
2566
|
-
// 清除 streaming widget(subagent 终态,幂等)
|
|
2567
|
-
stream?.dispose();
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
/** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
|
|
2571
|
-
* 不销毁对话——回退 idle(可恢复),让 agent 可重试 message 或 close。与一次性模式
|
|
2572
|
-
* (finalizeFailed 终态销毁)区分。返回合成 failed result(swallow,不 re-throw)。 */
|
|
2573
|
-
private async finalizeChatSpawnFailure(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
2574
|
-
const errMsg = toErrorMessage(err);
|
|
2575
|
-
const failedResult: AgentResult = {
|
|
2576
|
-
text: "",
|
|
2577
|
-
turns: record.turnCount,
|
|
2578
|
-
durationMs: Date.now() - record.startedAt,
|
|
2579
|
-
success: false,
|
|
2580
|
-
error: errMsg,
|
|
2581
|
-
sessionId: record.id,
|
|
2582
|
-
toolCalls: [],
|
|
2583
|
-
};
|
|
2584
|
-
disarmRoundFromProtocol(record.id);
|
|
2585
|
-
if (tryTransition(record, "closed", "gc")) {
|
|
2586
|
-
// 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
|
|
2587
|
-
await this.finalizeRoundToIdle(record, failedResult);
|
|
2588
|
-
}
|
|
2589
|
-
return failedResult;
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
/** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
|
|
2593
|
-
* 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
|
|
2594
|
-
private async consumeCloseAfterRound(
|
|
2595
|
-
record: ExecutionRecord,
|
|
2596
|
-
result: AgentResult,
|
|
2597
|
-
reason: ClosedReason,
|
|
2598
|
-
): Promise<void> {
|
|
2599
|
-
record.closeAfterRound = undefined;
|
|
2600
|
-
await this.finalizeRecord(record, result, "closed", reason);
|
|
729
|
+
* [D5 双写点 gate 判据] SP-5 升级(one-shot → chatMode)的 conversation 位检查。
|
|
730
|
+
* 本体已迁 RunOrchestration;壳纯转发(subagent-actions-core 消费)。 */
|
|
731
|
+
canUpgradeToConversation(record: Pick<ExecutionRecord, "engine">): boolean {
|
|
732
|
+
return this.runOrchestration.canUpgradeToConversation(record);
|
|
2601
733
|
}
|
|
2602
734
|
|
|
2603
735
|
/**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
* 消费,见 settleChatRoundFromResponse);
|
|
2609
|
-
* - 流式 delta / 轮次生命周期经 host/streamDelta + host/roundLifecycle 反向通道
|
|
2610
|
-
* 回流:首轮 runId 键(RunContext.stream / onRoundLifecycle)、续聊轮 recordId
|
|
2611
|
-
* 键(chatRoundRoutes 注册的 recordId 路由);
|
|
2612
|
-
* - recordId 路由在本方法注册(首轮 + 冷续轮替换式重注册),record 终态化路径注销。
|
|
2613
|
-
* chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
|
|
2614
|
-
* 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
|
|
2615
|
-
*/
|
|
2616
|
-
private kickOffChatRound(
|
|
2617
|
-
record: ExecutionRecord,
|
|
2618
|
-
opts: ExecuteOptions,
|
|
2619
|
-
identity: ResolvedIdentity,
|
|
2620
|
-
signal: AbortSignal | undefined,
|
|
2621
|
-
priority: number,
|
|
2622
|
-
/** 冷续锚点(M2-B1 协议形态):run.params.chat.resume。undefined = 新 session。 */
|
|
2623
|
-
resume?: ResumeAnchor,
|
|
2624
|
-
): void {
|
|
2625
|
-
// 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
|
|
2626
|
-
// 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
|
|
2627
|
-
const stream = createBackgroundStream(record.id, this.streamSink, this.uiObservability.getMode(), process.env);
|
|
2628
|
-
|
|
2629
|
-
// 注册 recordId 键反向通道路由(替换式——冷续轮重注册):interact 续聊轮的
|
|
2630
|
-
// streamDelta / roundLifecycle 分发目标;首轮流式 delta 走 ctx.stream(runId 键),
|
|
2631
|
-
// 本路由的 onStreamDelta 承担中段守护刷新 + dispose 后的续轮 delta(幂等 no-op)。
|
|
2632
|
-
// 仅 chat 会话形态注册(一次性 run 无反向轮次面)。
|
|
2633
|
-
const prevUnregister = this.chatRoundRoutes.get(record.id);
|
|
2634
|
-
prevUnregister?.();
|
|
2635
|
-
const engine = this.resolveChatEnginePort();
|
|
2636
|
-
if (record.chatMode) {
|
|
2637
|
-
this.chatRoundRoutes.set(
|
|
2638
|
-
record.id,
|
|
2639
|
-
engine.registerChatRoundRoute?.(record.id, {
|
|
2640
|
-
onStreamDelta: (delta) => {
|
|
2641
|
-
refreshFromProtocolEvent(record.id);
|
|
2642
|
-
stream?.onDelta(delta);
|
|
2643
|
-
},
|
|
2644
|
-
onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
|
|
2645
|
-
}) ?? (() => {}),
|
|
2646
|
-
);
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
// [W4] 冷路径 resume 轮的在途记账:死亡纳管 record 被主 agent resume = 决策收敛
|
|
2650
|
-
// (清指引标记与看门狗,回归「该等」)。热路径轮(deliverChatMessage)不经此处,
|
|
2651
|
-
// 其「该等」由 hasLiveProcess 判据覆盖(进程活)——conversation 形态本就豁免监督域。
|
|
2652
|
-
this.roundSupervisor.noteRunStarted(record.id);
|
|
2653
|
-
void (async () => {
|
|
2654
|
-
try {
|
|
2655
|
-
await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
|
|
2656
|
-
} catch {
|
|
2657
|
-
// S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
|
|
2658
|
-
if (signal?.aborted) {
|
|
2659
|
-
await this.finalizeAborted(record);
|
|
2660
|
-
} else {
|
|
2661
|
-
await this.finalizeFailed(record, new Error("aborted"));
|
|
2662
|
-
}
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
try {
|
|
2666
|
-
// [F-2 首轮/冷续轮 arm 重接] 轮开跑(pool 槽已到手、run 协议帧即将派发)挂
|
|
2667
|
-
// **中段**无进展检测——被删的 inproc stdout-pump 是首轮唯一中段守护,协议化后
|
|
2668
|
-
// 引擎侧 spawn-runner 仅 turn 计数无墙钟,本 arm 是首轮 wedged 的唯一熔断
|
|
2669
|
-
// (LC-1 场景①:pi 无事件行输出)。refresh 源(对照 deliverChatMessage 热路径
|
|
2670
|
-
// 的 arm/refresh 形态,同一守护实例语义):① runId 键协议事件行(ctx.onEvent,
|
|
2671
|
-
// 含 text_delta——引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta
|
|
2672
|
-
// 两通道,刷新面与热路径 streamDelta 等效);② settled 相位交棒
|
|
2673
|
-
// (ctx.onRoundLifecycle → handleChatRoundPhase → noteRoundSettledFromProtocol,
|
|
2674
|
-
// 首轮 runId 键同样生效);idle 相位 disarmRoundFromProtocol 两段一并清。
|
|
2675
|
-
// arm 置于 acquire 之后:排队窗口不计入 no-progress 静默(窗语义 = 轮开跑后)。
|
|
2676
|
-
armMidRoundNoProgress(record.id, {
|
|
2677
|
-
onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
2678
|
-
onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
|
|
2679
|
-
});
|
|
2680
|
-
// 协议 run:chatMode = 会话形态(chat.recordId = 关联键;resume 存在 = 冷续;
|
|
2681
|
-
// 应答时点 = 首轮 agent_settled,本轮 settle / 守护交棒 / idle 定时器挂载由
|
|
2682
|
-
// handleChatRoundPhase + settleChatRoundFromResponse 承接);非 chatMode =
|
|
2683
|
-
// 一次性 run(协议面无 chat 键,终态语义对齐原 settleOneShotOutcome)。
|
|
2684
|
-
const { outcome } = await engine.run(
|
|
2685
|
-
// resume 锚点轮引擎侧覆盖 model 解析(taskSpec 装配单一来源见 taskSpecWithModel)。
|
|
2686
|
-
this.taskSpecWithModel(opts, record.model),
|
|
2687
|
-
{
|
|
2688
|
-
taskId: record.id,
|
|
2689
|
-
poolKey: PI_POOL_KEY,
|
|
2690
|
-
signal,
|
|
2691
|
-
...(stream !== undefined ? { stream } : {}),
|
|
2692
|
-
ctxModel: identity.resolved.model,
|
|
2693
|
-
// chat 会话形态参数(conversation 形态必传;冷续带锚点);一次性 run 不携带。
|
|
2694
|
-
...(record.chatMode
|
|
2695
|
-
? {
|
|
2696
|
-
chat: {
|
|
2697
|
-
recordId: record.id,
|
|
2698
|
-
...(resume !== undefined ? { resume } : {}),
|
|
2699
|
-
},
|
|
2700
|
-
// 首轮 runId 键生命周期帧:守护交棒 / idle 定时器挂载 / failed 分诊。
|
|
2701
|
-
onRoundLifecycle: (phase: HostRoundLifecycleParams) => this.handleChatRoundPhase(record, phase),
|
|
2702
|
-
// 中段守护刷新源①:首轮协议事件行(message_*/tool_*/turn_end——有效
|
|
2703
|
-
// 事件到达即刷新;chat 域不接 journal,事件仅作活性信号消费)。
|
|
2704
|
-
onEvent: () => refreshFromProtocolEvent(record.id),
|
|
2705
|
-
}
|
|
2706
|
-
: {}),
|
|
2707
|
-
},
|
|
2708
|
-
);
|
|
2709
|
-
if (outcome.sessionFile !== undefined) record.sessionFile = outcome.sessionFile;
|
|
2710
|
-
const result = this.outcomeToAgentResult(record, outcome);
|
|
2711
|
-
if (record.chatMode) {
|
|
2712
|
-
// 本轮 settle(应答 = 首轮 agent_settled):round+1 / 增量通知 / closeAfterRound 消费。
|
|
2713
|
-
this.settleChatRoundFromResponse(record, outcome);
|
|
2714
|
-
} else {
|
|
2715
|
-
// 一次性 run 终态收口(对齐原 settleOneShotOutcome:成功轮 SP-5 回退
|
|
2716
|
-
// running-resumable 等待 upgrade;失败/取消一次性销毁)。
|
|
2717
|
-
await this.settleOneShotOutcome(record, result, signal?.aborted === true);
|
|
2718
|
-
}
|
|
2719
|
-
// background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
|
|
2720
|
-
// 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
|
|
2721
|
-
// 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
|
|
2722
|
-
// 表达)——迟到的完成回注不注入(可能已切换的)新 session。
|
|
2723
|
-
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2724
|
-
this.collectCoordinator.route(record);
|
|
2725
|
-
}
|
|
2726
|
-
} catch (err) {
|
|
2727
|
-
// 轮次 run 失败(prepare 期 reject / 引擎进程死亡 / cancel 后未收敛合成终态):
|
|
2728
|
-
// chatMode MF-6——不销毁对话,回退可恢复(session 文件在盘,冷续 run 接续);
|
|
2729
|
-
// 非 chatMode 终态销毁(finalizeFailed)。cancel 抢先时 record 已终态化
|
|
2730
|
-
//(tryTransition 失败跳过),此处仅吞错。
|
|
2731
|
-
if (record.chatMode) {
|
|
2732
|
-
await this.finalizeChatSpawnFailure(record, err);
|
|
2733
|
-
} else {
|
|
2734
|
-
await this.finalizeFailed(record, err);
|
|
2735
|
-
}
|
|
2736
|
-
if (err instanceof Error) {
|
|
2737
|
-
logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
|
|
2738
|
-
}
|
|
2739
|
-
} finally {
|
|
2740
|
-
this.pool.release();
|
|
2741
|
-
// 在途 resume 守卫清除(幂等;冷续轮收尾)+ streaming widget 清除(轮终,幂等——
|
|
2742
|
-
// 续轮 delta 落已 dispose 的 stream 为 no-op,与 inproc 形态逐点一致)。
|
|
2743
|
-
this.resumesInFlight.delete(record.id);
|
|
2744
|
-
stream?.dispose();
|
|
2745
|
-
// [W4] 轮收口重评估(死亡纳管 record 的轮终 → 驱动可能又死 → 重新三态判定)。
|
|
2746
|
-
this.roundSupervisor.noteRunEnded(record.id);
|
|
2747
|
-
}
|
|
2748
|
-
})();
|
|
736
|
+
* [V2 决策 3 → H1 U2 改写 / U6 定形] chatMode 统一投递入口。本体已迁
|
|
737
|
+
* RunOrchestration;壳纯转发(chatActions 聚合面消费)。 */
|
|
738
|
+
private async deliverChatMessage(record: ExecutionRecord, text: string): Promise<void> {
|
|
739
|
+
return this.runOrchestration.deliverChatMessage(record, text);
|
|
2749
740
|
}
|
|
2750
741
|
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
* - failed:轮异常终止(engine_round_aborted / engine_round_crashed /
|
|
2759
|
-
* engine_round_epipe_exhausted)——record 如实标 failed(禁 completed 谎报),
|
|
2760
|
-
* chatMode 按 MF-6 回退可恢复(onChatRoundFailed)。
|
|
2761
|
-
*/
|
|
2762
|
-
private handleChatRoundPhase(record: ExecutionRecord, phase: HostRoundLifecycleParams): void {
|
|
2763
|
-
switch (phase.phase) {
|
|
2764
|
-
case "settled":
|
|
2765
|
-
noteRoundSettledFromProtocol(record.id);
|
|
2766
|
-
break;
|
|
2767
|
-
case "idle":
|
|
2768
|
-
disarmRoundFromProtocol(record.id);
|
|
2769
|
-
this.armChatIdleTimer(record);
|
|
2770
|
-
if (phase.anchor !== undefined) {
|
|
2771
|
-
this.backfillChatAnchor(record, phase.anchor);
|
|
2772
|
-
}
|
|
2773
|
-
break;
|
|
2774
|
-
case "failed":
|
|
2775
|
-
this.onChatRoundFailed(record, phase.error);
|
|
2776
|
-
break;
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
742
|
+
// ── 域 #18 finalize 簇 聚合转发(R3 抽取;本体 execution/service/record-lifecycle.ts;
|
|
743
|
+
// H4 落点 D5)──
|
|
744
|
+
// [R4] finalizeFailed / finalizeAborted 两转发已删(R3 过渡期消费方 = R4 域派发链,
|
|
745
|
+
// 已随域迁 RunOrchestration/WorkflowDispatch,经 deps 装配闭包直指 recordLifecycle
|
|
746
|
+
// ——壳内与全仓零剩余消费,删转发判据满足)。finalizeRecord 转发保留:消费方 =
|
|
747
|
+
// C-6 装配闭包(roundSupervisor/reconcile sweep 的 finalizeClosed——经壳转发方法
|
|
748
|
+
// late-bound 读取,装配点零改动,清单①预判兑现)。
|
|
2779
749
|
|
|
2780
750
|
/**
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
2783
|
-
*
|
|
751
|
+
* D-017 时序收尾:委托 doFinalizeRecord(finalize-record.ts 独立模块)。本体已迁
|
|
752
|
+
* RecordLifecycle(FinalizeDeps 装配经 deps 现读 + onFinalized 钩子经 C-5 回调);
|
|
753
|
+
* 壳纯转发,签名不变。
|
|
2784
754
|
*/
|
|
2785
|
-
private
|
|
2786
|
-
const onTimeout = (): void => {
|
|
2787
|
-
killRecordChildWithEscalation(record.id, "idle timer");
|
|
2788
|
-
this.terminateChatSession(record, "close", "idle timer");
|
|
2789
|
-
};
|
|
2790
|
-
try {
|
|
2791
|
-
armIdleTimer(record.id, onTimeout, record.idleTimeoutMs);
|
|
2792
|
-
} catch (err) {
|
|
2793
|
-
bestEffort(err, "armIdleTimer (chat idle phase)", "error");
|
|
2794
|
-
try {
|
|
2795
|
-
armIdleTimer(record.id, onTimeout, DEFAULT_IDLE_TIMEOUT_MS);
|
|
2796
|
-
logger.warn(
|
|
2797
|
-
`[subagents] idleTimeoutMs invalid for ${record.id}, fell back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms) — idle GC and round notification gate stay active`,
|
|
2798
|
-
);
|
|
2799
|
-
} catch (fallbackErr) {
|
|
2800
|
-
bestEffort(fallbackErr, "armIdleTimer fallback (chat idle phase)", "error");
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
/** [W3] idle 帧锚点回填(冷续锚点 = 引擎侧会话滚动/compaction 后的最新定位)。 */
|
|
2806
|
-
private backfillChatAnchor(
|
|
755
|
+
private finalizeRecord(
|
|
2807
756
|
record: ExecutionRecord,
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
}
|
|
2814
|
-
// engineHandle 回填(①级读取钥匙;幂等守卫——终态回填已落则不覆盖)。
|
|
2815
|
-
if (record.engineHandle === undefined || record.engineHandle.sessionRef["sessionId"] === undefined) {
|
|
2816
|
-
record.engineHandle = {
|
|
2817
|
-
sessionRef: { ...anchor.sessionRef },
|
|
2818
|
-
poolKey: anchor.poolKey,
|
|
2819
|
-
...(anchor.journalPath !== undefined ? { journalPath: anchor.journalPath } : {}),
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
2822
|
-
this.store.reportRecordTransition(record);
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
|
-
/**
|
|
2826
|
-
* [W3] chat 轮 failed 相位分诊:错误如实落 record(失败相位错误码——engine_round_
|
|
2827
|
-
* aborted(宿主 cancel/close)/ engine_round_crashed(子进程外部死亡)/
|
|
2828
|
-
* engine_round_epipe_exhausted(EPIPE 兜底耗尽)),禁 completed 谎报。chatMode 按
|
|
2829
|
-
* MF-6 不销毁对话——回退可恢复(finalizeRoundToIdle + 通知),冷续 run 接续;
|
|
2830
|
-
* record 已终态(cancel/close 抢先)= 迟到帧,幂等跳过。
|
|
2831
|
-
*/
|
|
2832
|
-
private onChatRoundFailed(record: ExecutionRecord, error: ProtocolError): void {
|
|
2833
|
-
disarmRoundFromProtocol(record.id);
|
|
2834
|
-
if (record.status !== "running") return;
|
|
2835
|
-
const errMsg = `${error.code}: ${error.message}`;
|
|
2836
|
-
record.lastError = errMsg;
|
|
2837
|
-
const failedResult: AgentResult = {
|
|
2838
|
-
text: "",
|
|
2839
|
-
turns: record.turnCount,
|
|
2840
|
-
durationMs: Date.now() - record.startedAt,
|
|
2841
|
-
success: false,
|
|
2842
|
-
error: errMsg,
|
|
2843
|
-
sessionId: record.id,
|
|
2844
|
-
toolCalls: [],
|
|
2845
|
-
};
|
|
2846
|
-
void this.finalizeRoundToIdle(record, failedResult)
|
|
2847
|
-
.then(() => {
|
|
2848
|
-
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2849
|
-
this.collectCoordinator.route(record);
|
|
2850
|
-
}
|
|
2851
|
-
})
|
|
2852
|
-
.catch((err: unknown) => bestEffort(err, "chat round failed finalize", "error"));
|
|
757
|
+
result: AgentResult,
|
|
758
|
+
status: "closed",
|
|
759
|
+
closedReason?: ClosedReason,
|
|
760
|
+
): Promise<void> {
|
|
761
|
+
return this.recordLifecycle.finalizeRecord(record, result, status, closedReason);
|
|
2853
762
|
}
|
|
2854
763
|
|
|
2855
764
|
/**
|
|
2856
|
-
* [
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
2861
|
-
* idle 定时器挂载不在此处——idle 相位帧先于应答帧到达,armChatIdleTimer 已锚定。
|
|
765
|
+
* [W4] 轮次活性监督器(D2「等待有主」权威层):死亡事件纳管(run failed / 引擎
|
|
766
|
+
* exited → adoptOnProcessDeath)+ boot 分区重认领(initSession)+ 三态判定
|
|
767
|
+
* (record 级视图)+ 该放弃(终态化 failed + 注销 + 终止通知)。装配绑定面
|
|
768
|
+
* (deps/giveUp 编排/sweep 挂点)在 round-supervisor/service-binding.ts——变化轴
|
|
769
|
+
* 独立(通知文案 / 终态化编排 / store 读侧判据只动该文件),本字段只持实例。
|
|
2862
770
|
*/
|
|
2863
|
-
private
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
//(notify 后推进:notify 失败时 base 不推进,增量并入下一轮防丢文本)。
|
|
2871
|
-
// [T4①/PS-2] 通知门与 kickOff 回注同款:parent-new/parent-fork 编排性关闭后
|
|
2872
|
-
// 迟到的应答 settle 不注入(可能已切换的)新 session;cancelled 由 cancelBackground
|
|
2873
|
-
// 自行 notify。
|
|
2874
|
-
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
2875
|
-
this.collectCoordinator.route(record);
|
|
2876
|
-
}
|
|
2877
|
-
record.roundBaseTurnIndex = record.turnCount;
|
|
2878
|
-
this.store.reportRecordTransition(record);
|
|
2879
|
-
// [M5] closeAfterRound 消费:chatMode 轮次完成的统一汇聚点(优雅关闭在本轮
|
|
2880
|
-
// 完成时兑现终态化)。
|
|
2881
|
-
if (record.closeAfterRound) {
|
|
2882
|
-
record.closeAfterRound = undefined;
|
|
2883
|
-
void this.closeAfterRoundSettled(record);
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
771
|
+
private readonly roundSupervisor: RoundSupervisor = createRoundSupervisorForService({
|
|
772
|
+
getStore: () => this.store,
|
|
773
|
+
getPi: () => this.pi,
|
|
774
|
+
getSessionRootId: () => this.sessionRootId,
|
|
775
|
+
getMainSessionFile: () => this.mainSessionFile,
|
|
776
|
+
finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
|
|
777
|
+
});
|
|
2886
778
|
|
|
2887
|
-
/** [W3] chat 域引擎终止意图(协议面):cancel = interact cancel(引擎侧 SIGTERM →
|
|
2888
|
-
* settle 等待 → 杀链升级);close = interact close force(立即杀链收割)。fire-and-forget
|
|
2889
|
-
* ——失败 best-effort 留证(宿主组级收割兜底 = EngineClient killAll,dispose 路径)。 */
|
|
2890
|
-
private terminateChatSession(
|
|
2891
|
-
record: ExecutionRecord,
|
|
2892
|
-
kind: "cancel" | "close",
|
|
2893
|
-
source: string,
|
|
2894
|
-
): void {
|
|
2895
|
-
void (async () => {
|
|
2896
|
-
const engine = this.resolveChatEnginePort();
|
|
2897
|
-
const handle = this.chatHandleFor(record);
|
|
2898
|
-
const result = kind === "cancel"
|
|
2899
|
-
? await engine.interact(handle, { kind: "cancel" })
|
|
2900
|
-
: await engine.interact(handle, { kind: "close", payload: { force: true } });
|
|
2901
|
-
if (!result.ok) {
|
|
2902
|
-
logger.debug(
|
|
2903
|
-
`[subagents] terminateChatSession (${kind}, ${source}) rejected for ${record.id}: ${result.message}`,
|
|
2904
|
-
);
|
|
2905
|
-
}
|
|
2906
|
-
})().catch((err: unknown) =>
|
|
2907
|
-
bestEffort(err, `terminateChatSession (${kind}, ${source})`, "debug"),
|
|
2908
|
-
);
|
|
2909
|
-
}
|
|
2910
779
|
|
|
2911
|
-
/** chat record 的协议 interact handle(engineHandle 缺省时按 recordId 合成——
|
|
2912
|
-
* 引擎侧 interact 定位键 = sessionRef.recordId;sessionFile 有值时一并携带)。 */
|
|
2913
|
-
private chatHandleFor(record: ExecutionRecord): EngineHandle {
|
|
2914
|
-
const known = record.engineHandle;
|
|
2915
|
-
return {
|
|
2916
|
-
data: {
|
|
2917
|
-
v: 1,
|
|
2918
|
-
engineId: record.engine ?? DEFAULT_ENGINE_ID,
|
|
2919
|
-
sessionRef: {
|
|
2920
|
-
recordId: record.id,
|
|
2921
|
-
...(known?.sessionRef["sessionFile"] !== undefined
|
|
2922
|
-
? { sessionFile: known.sessionRef["sessionFile"] }
|
|
2923
|
-
: record.sessionFile !== undefined
|
|
2924
|
-
? { sessionFile: record.sessionFile }
|
|
2925
|
-
: {}),
|
|
2926
|
-
},
|
|
2927
|
-
poolKey: known?.poolKey ?? PI_POOL_KEY,
|
|
2928
|
-
...(known?.journalPath !== undefined ? { journalPath: known.journalPath } : {}),
|
|
2929
|
-
adapterVersion: "subagent-core/host-bridge",
|
|
2930
|
-
},
|
|
2931
|
-
};
|
|
2932
|
-
}
|
|
2933
780
|
|
|
2934
|
-
/** [W3] pi 引擎 port 解析(chat 域路由与终止面共用):registry cli 形态 port,
|
|
2935
|
-
* 未注册 = 不可用 stub(engine_not_found,见 pi-host-binding)。 */
|
|
2936
|
-
private resolveChatEnginePort(): EnginePort {
|
|
2937
|
-
return resolveHostPiEnginePort(() => null);
|
|
2938
|
-
}
|
|
2939
781
|
|
|
2940
|
-
/** [W3] record 终态化路径的 chat 反向通道路由注销(幂等)。 */
|
|
2941
|
-
private unregisterChatRoundRoute(recordId: string): void {
|
|
2942
|
-
const unregister = this.chatRoundRoutes.get(recordId);
|
|
2943
|
-
if (unregister !== undefined) {
|
|
2944
|
-
this.chatRoundRoutes.delete(recordId);
|
|
2945
|
-
unregister();
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
782
|
|
|
2949
783
|
/** 引擎服务面适配器(HostBridgeServiceFace 结构视图):闭包持有本实例的编排面。
|
|
2950
784
|
* [W3] chat 域轮次交接可选面(takeChatRound/runChatRound/resumeChatRound)随
|
|
@@ -2967,256 +801,110 @@ export class SubagentService {
|
|
|
2967
801
|
return this.piEngineServiceAdapter();
|
|
2968
802
|
}
|
|
2969
803
|
|
|
2970
|
-
|
|
2971
|
-
* 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
|
|
2972
|
-
* 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
|
|
2973
|
-
*
|
|
2974
|
-
* stop 手段(abort/kill/disarm)无条件先执行:对已终态 record 幂等无害,且保证
|
|
2975
|
-
* cancel 语义 = 进程必死;收尾副作用(completeRecord/tombstone/archive/notify)只归
|
|
2976
|
-
* CAS 赢家。[A2-1] 此前 CAS 被误删,cancel 可在 doFinalizeRecord Step 0 await 窗口
|
|
2977
|
-
* 命中已终态 record——覆写终态 + tombstone/finalized 双标 + notify 双发 + 谎报 true。
|
|
2978
|
-
*/
|
|
2979
|
-
private cancelBackground(record: ExecutionRecord): boolean {
|
|
2980
|
-
record.controller?.abort();
|
|
2981
|
-
// [M6] 显式 kill + disarm:chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0)
|
|
2982
|
-
// 返回,`opts.signal.removeEventListener("abort", onAbort)`(session-runner runSpawn 尾部)
|
|
2983
|
-
// 已移除 abort→kill listener;热路径续聊轮(PiEngine 直接 stdin 写入)不再
|
|
2984
|
-
// 进 runSpawn。此后 cancel 只有 controller.abort() 无人响应——record 已终态化 cancelled
|
|
2985
|
-
// 但子进程继续跑完当前 turn(工具副作用继续发生),之后 agent_settled 还对已 archived
|
|
2986
|
-
// record 触发脏通知(round+1 → 新 dedup key → "finished a round"),最终靠 5min idle
|
|
2987
|
-
// timer 兜底 kill。故 cancel 必须显式 kill + disarm idle timer。非 chatMode 路径
|
|
2988
|
-
// listener 仍在(abort 已 kill 一次),此处对已 killed child 是 no-op,无副作用。
|
|
2989
|
-
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
|
|
2990
|
-
// 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
|
|
2991
|
-
// 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
|
|
2992
|
-
// [W3] chatMode 的实际终止经协议 interact cancel(引擎进程内 SIGTERM → settle
|
|
2993
|
-
// 等待 → 杀链升级;首轮在途 run 的 abort 帧由 controller.signal 经 RemoteEngine
|
|
2994
|
-
// cancel 分级另行承载,两路幂等)。
|
|
2995
|
-
killRecordChildWithEscalation(record.id, "cancelBackground");
|
|
2996
|
-
disarmIdleTimer(record.id);
|
|
2997
|
-
disarmSettledWatchdog(record.id);
|
|
2998
|
-
disarmRoundFromProtocol(record.id);
|
|
2999
|
-
this.unregisterChatRoundRoute(record.id);
|
|
3000
|
-
if (record.chatMode) {
|
|
3001
|
-
this.terminateChatSession(record, "cancel", "cancelBackground");
|
|
3002
|
-
}
|
|
3003
|
-
// [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
|
|
3004
|
-
// record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
|
|
3005
|
-
// cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
|
|
3006
|
-
// 双标 + notify 双发,绝不可执行)。
|
|
3007
|
-
if (!tryTransition(record, "closed", "cancelled")) {
|
|
3008
|
-
return false; // detached 已 finalize,cancel 来晚了
|
|
3009
|
-
}
|
|
3010
|
-
// 抢到锁:completeRecord(用空 result 填 cancelled)+ archive(立即移出内存)+ notify。
|
|
3011
|
-
// 写 cancelled tombstone:session.jsonl 被 abort 截断,cancelled 状态靠 sidecar 标记,
|
|
3012
|
-
// collectRecords 重建时 override status=cancelled。durationMs 用真实耗时(startedAt → now)。
|
|
3013
|
-
const cancelledResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: "cancelled by user", sessionId: record.id, toolCalls: [] };
|
|
3014
|
-
completeRecord(record, cancelledResult, "closed", "cancelled");
|
|
3015
|
-
// 写 tombstone(best-effort,sessionFile 可能为 undefined——窗口期 cancel)。
|
|
3016
|
-
if (record.sessionFile) {
|
|
3017
|
-
writeCancelledTombstone(record.sessionFile, {
|
|
3018
|
-
id: record.id,
|
|
3019
|
-
status: "cancelled",
|
|
3020
|
-
agent: record.agent,
|
|
3021
|
-
startedAt: record.startedAt,
|
|
3022
|
-
endedAt: record.endedAt ?? Date.now(),
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
this.store.archive(record);
|
|
3026
|
-
// worktree cleanup + removeAliveMarker(cancel 不写 finalized,BC-4 互斥)。
|
|
3027
|
-
// cleanup 已 async 化——boolean 同步返回语义不变,清理 fire-and-forget。
|
|
3028
|
-
if (record.worktreeHandle) {
|
|
3029
|
-
void this.worktreeManager.cleanup(record.worktreeHandle).catch((err: unknown) => {
|
|
3030
|
-
bestEffort(err, "worktree cleanup (cancelBackground)");
|
|
3031
|
-
});
|
|
3032
|
-
}
|
|
3033
|
-
if (record.sessionFile) {
|
|
3034
|
-
try {
|
|
3035
|
-
removeAliveMarker(record.sessionFile);
|
|
3036
|
-
} catch (err) {
|
|
3037
|
-
bestEffort(err, "removeAliveMarker (cancelBackground)");
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
// pending-notifications:cancel 注销(只记 registry 状态)
|
|
3041
|
-
this.notifyHost.emitPendingUnregister(record.id, "closed");
|
|
3042
|
-
// cancel 完成通知(与轮次收尾 .then 对称——cancel 抢先时 .then 跳过 notify)
|
|
3043
|
-
this.collectCoordinator.route(record);
|
|
3044
|
-
return true;
|
|
3045
|
-
}
|
|
804
|
+
// ── 壳生命周期编排与断言(dispose 时序留壳 = R3 检查点③;D4 断言面)(R0 重排)──
|
|
3046
805
|
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
* [Critical #1] cleanup 全部在 manifest 写之前,manifest best-effort 不阻断(详见 finalize-record.ts)。 */
|
|
3050
|
-
private async finalizeRecord(
|
|
3051
|
-
record: ExecutionRecord,
|
|
3052
|
-
result: AgentResult,
|
|
3053
|
-
status: "closed",
|
|
3054
|
-
closedReason?: ClosedReason,
|
|
3055
|
-
): Promise<void> {
|
|
3056
|
-
await doFinalizeRecord(
|
|
3057
|
-
{
|
|
3058
|
-
manifestStore: this.manifestStore,
|
|
3059
|
-
worktreeManager: this.worktreeManager,
|
|
3060
|
-
store: this.store,
|
|
3061
|
-
modelService: this.modelService,
|
|
3062
|
-
pi: this.pi,
|
|
3063
|
-
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
3064
|
-
// [F-5 修复] chat record 终态化的路由注销(单一汇聚点钩子,见 FinalizeDeps
|
|
3065
|
-
// .onFinalized)——chatRoundRoutes 仅 chatMode 注册且注销幂等,非 chat record
|
|
3066
|
-
// 经此为 no-op。
|
|
3067
|
-
onFinalized: (id) => this.unregisterChatRoundRoute(id),
|
|
3068
|
-
sessionDir: this.sessionsDir,
|
|
3069
|
-
},
|
|
3070
|
-
record,
|
|
3071
|
-
result,
|
|
3072
|
-
status,
|
|
3073
|
-
closedReason,
|
|
3074
|
-
);
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
/**
|
|
3078
|
-
* 对话模式轮次完成收尾:委托 doFinalizeRoundToIdle(record 进 idle,保留内存 + worktree)。
|
|
3079
|
-
* 与 finalizeRecord 对称的委托方法,deps 同源注入。chatMode + done/failed/cancelled 时由 runAndFinalize 调用
|
|
3080
|
-
*(MF-6:chatMode 失败/取消也回退 idle 而非终态销毁)。 */
|
|
3081
|
-
private async finalizeRoundToIdle(
|
|
3082
|
-
record: ExecutionRecord,
|
|
3083
|
-
result: AgentResult,
|
|
3084
|
-
): Promise<void> {
|
|
3085
|
-
await doFinalizeRoundToIdle(
|
|
3086
|
-
{
|
|
3087
|
-
manifestStore: this.manifestStore,
|
|
3088
|
-
worktreeManager: this.worktreeManager,
|
|
3089
|
-
store: this.store,
|
|
3090
|
-
modelService: this.modelService,
|
|
3091
|
-
pi: this.pi,
|
|
3092
|
-
emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
|
|
3093
|
-
},
|
|
3094
|
-
record,
|
|
3095
|
-
result,
|
|
3096
|
-
);
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3099
|
-
/** run() 创建期异常的收尾(H1 修复):createAndConfigureSession 失败会抛,本方法合成 failed
|
|
3100
|
-
* AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
|
|
3101
|
-
* 继续返回(不 re-throw,swallow 策略)。
|
|
3102
|
-
* [W3 契约变更⑤(run 期失败清理前置副作用)] kickOffEngineRun 前已建的 worktree
|
|
3103
|
-
* (executeViaEngine 创建点,record.worktreeHandle 已绑定)经本方法 → finalizeRecord
|
|
3104
|
-
* → doFinalizeRecord Step 3b cleanupWorktreeIfBound 清理(manifest 多声明 run 期
|
|
3105
|
-
* 失败 / engine.run prepare 期 reject 共用本收尾链);CAS 没抢锁(cancel 抢先终态)
|
|
3106
|
-
* 时由 cancelBackground 的 worktree cleanup 覆盖。唯一前置副作用 = worktree(并发
|
|
3107
|
-
* 池槽 acquire/release 在 kickOffEngineRun finally 内自回收,journal 是宿主数据不清理)。 */
|
|
3108
|
-
private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
|
|
3109
|
-
const errMsg = toErrorMessage(err);
|
|
3110
|
-
// durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
|
|
3111
|
-
const failedResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: errMsg, sessionId: record.id, toolCalls: [] };
|
|
3112
|
-
// CAS 抢锁:抢到(status 仍 running)则完整收尾;没抢到(cancel 已先设 cancelled)跳过。
|
|
3113
|
-
// SP-1: failed → closed + gc(通用失败终态)。
|
|
3114
|
-
if (tryTransition(record, "closed", "gc")) {
|
|
3115
|
-
await this.finalizeRecord(record, failedResult, "closed", "gc");
|
|
3116
|
-
}
|
|
3117
|
-
return failedResult;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
/** S1: 排队中被 abort 走 cancelled 终态(对齐已运行被 abort 的 cancelBackground)。 */
|
|
3121
|
-
private async finalizeAborted(record: ExecutionRecord): Promise<AgentResult> {
|
|
3122
|
-
const cancelledResult: AgentResult = { text: "", turns: record.turnCount, durationMs: Date.now() - record.startedAt, success: false, error: "cancelled by user", sessionId: record.id, toolCalls: [] };
|
|
3123
|
-
if (tryTransition(record, "closed", "cancelled")) {
|
|
3124
|
-
await this.finalizeRecord(record, cancelledResult, "closed", "cancelled");
|
|
3125
|
-
}
|
|
3126
|
-
return cancelledResult;
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
// ── 内部 ────────────────────────────────────────────────
|
|
806
|
+
private _disposed = false;
|
|
807
|
+
// [B-7 已清] _seq 死字段(全文件零消费)按 r0-inventory 清单①登记于 R1 删除。
|
|
3130
808
|
|
|
3131
|
-
/**
|
|
3132
|
-
* 校验 Service 就绪(pi 已注入 + 未 dispose)。
|
|
3133
|
-
*
|
|
3134
|
-
* dispose 后调用是异常路径:session_shutdown 已清资源,正常情况下紧接着
|
|
3135
|
-
* session_start 会 initSession 复活。若走到这里说明 session_start 没跟上
|
|
3136
|
-
* (RPC 边界 / reload 异常等),service 卡在 disposed 状态。
|
|
809
|
+
/** session 结束清理(清定时器,丢弃 pending 通知)。幂等。
|
|
3137
810
|
*
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
3141
|
-
*/
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
811
|
+
* [M-7] dispose 顺序假设:pending:unregister emit 依赖 pending-notifications 扩展的
|
|
812
|
+
* listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
|
|
813
|
+
* 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
|
|
814
|
+
* 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
|
|
815
|
+
|
|
816
|
+
dispose(): void {
|
|
817
|
+
if (this._disposed) return;
|
|
818
|
+
this._disposed = true;
|
|
819
|
+
this.stopGcTimer();
|
|
820
|
+
// [v2 D4/C-1] settled 重扫 handler 惰化:原直改聚合内部态 settledRescanState.disposed
|
|
821
|
+
// (r0-inventory 清单① C-1 跨聚合边,R2 兑现收敛为显式接口)——时序契约注释随迁
|
|
822
|
+
// SyncCollectDomain.lazyDispose(trailing 边沿不扫描防「已落标未写账」的永久丢失)。
|
|
823
|
+
this.syncCollect.lazyDispose();
|
|
824
|
+
// [dispose stub] 第一时间换 stub,防 trailing ui_request 调到 stale handler 闭包
|
|
825
|
+
// (仍持有 disposed session 的 ctx)产生误导性 console.error。stub 干净降级为 cancelled。
|
|
826
|
+
// 必须在 emit/abort 之前——这些步骤可能同步触发 trailing pump。
|
|
827
|
+
// [D4-④] 原 setUiRequestHandler 方法已删(initSession 参数为唯一注入入口)。
|
|
828
|
+
// [R1/C-3] 原直写聚合字段两行(handler 换 stub + 缺失告警去重重置)收敛为显式接口
|
|
829
|
+
// 方法 disposeSessionUi()——字段所有权在聚合(时序契约注释随迁)。
|
|
830
|
+
this.baselines.disposeSessionUi();
|
|
831
|
+
// [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
|
|
832
|
+
// cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
|
|
833
|
+
setHostUiRequestEndpoint(disposedUiRequestStub);
|
|
834
|
+
// [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
|
|
835
|
+
// abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
|
|
836
|
+
this.store.abortRunningControllers();
|
|
837
|
+
killAllSpawnedChildren();
|
|
838
|
+
// [H1 U2/U6] Continuation 实例全量清理(路由注销面已随 interact 面退役)——
|
|
839
|
+
// dispose 后容器不应再收 message。
|
|
840
|
+
// [R4 / C-4 兑现] 原直调 this.continuations.clear() 的跨聚合写边收敛为聚合显式
|
|
841
|
+
// 接口(字段所有权随域 #14 迁 RunOrchestration)。
|
|
842
|
+
this.runOrchestration.clearContinuations();
|
|
843
|
+
// [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
|
|
844
|
+
// 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
|
|
845
|
+
// running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
|
|
846
|
+
// 冷 idToFile(无目录扫描则 miss → 跳过落标 → E1 重建误收已转换成员);先转换取
|
|
847
|
+
// 内存命中,与 flushBatch 出口①同款通路。同样在 flushPendingNotifications 之前
|
|
848
|
+
//(转换条目加入本次 flush)与 notifier/store dispose 之前(写账与 appendEntry
|
|
849
|
+
// 通道仍可用)。仍在跑成员不在此处理——后续 disposeAllRecords 按现有退出路径关闭。
|
|
850
|
+
// [R2] 本体迁 SyncCollectDomain.convertPendingSyncBufferToAsync(E9 转账时序逐行等价)。
|
|
851
|
+
this.syncCollect.convertPendingSyncBufferToAsync();
|
|
852
|
+
// SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
|
|
853
|
+
// 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
|
|
854
|
+
this.disposeAllRecords("parent-shutdown");
|
|
855
|
+
// [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
|
|
856
|
+
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
857
|
+
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
858
|
+
this.notifyHost.flushPendingNotifications();
|
|
859
|
+
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
860
|
+
// 复查失败时 pending 挂在 ledger 内存态,随后 dispose 销毁——旧注释「flush 后 dispose
|
|
861
|
+
//(防丢失)」的承诺与行为不符。现把未投递 pending 经 ledger entry 通道复写落盘
|
|
862
|
+
// (notifyId 幂等,重复 entry 由恢复扫描后写覆盖吸收),重启 recoverFromSession 按
|
|
863
|
+
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
864
|
+
this.persistUndeliveredNotificationsForReplay();
|
|
865
|
+
this.notifyHost.dispose();
|
|
866
|
+
// [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
|
|
867
|
+
// 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
|
|
868
|
+
this.roundSupervisor.dispose();
|
|
869
|
+
this.store.dispose();
|
|
3153
870
|
}
|
|
3154
871
|
|
|
3155
872
|
/**
|
|
3156
|
-
* [T4
|
|
873
|
+
* [T4④ / PS-5] shutdown flush 被门拦时把未投递 pending 复写落盘(供重启 replay)。
|
|
3157
874
|
*
|
|
3158
|
-
*
|
|
3159
|
-
*
|
|
3160
|
-
*
|
|
3161
|
-
*
|
|
3162
|
-
*
|
|
875
|
+
* 触发条件:flushPendingNotifications 后 ledger 仍有 pending(isIdle 门拦 / sendDelivery
|
|
876
|
+
* 受理失败的残留)且主 agent 非 idle——即本次 shutdown 注定投不出去。落盘动作 =
|
|
877
|
+
* pi.appendEntry 重写 NOTIFY_LEDGER_CUSTOM_TYPE entry(与 ledger.record 同通道同 schema,
|
|
878
|
+
* notifyId 幂等:恢复扫描按后写覆盖 + ack/abandoned 差集去重,重复账面不产生重复投递)。
|
|
879
|
+
* 主 agent idle 时 flush 已投出,无需复写(零开销)。
|
|
3163
880
|
*/
|
|
3164
|
-
private
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
881
|
+
private persistUndeliveredNotificationsForReplay(): void {
|
|
882
|
+
try {
|
|
883
|
+
const ledger = getBoundNotifyLedger();
|
|
884
|
+
const pending = ledger?.pendingEntries() ?? [];
|
|
885
|
+
if (pending.length === 0) return;
|
|
886
|
+
if (this.isIdleFn?.() !== false) return; // idle:flush 已投出(或无 gate 场景照旧)
|
|
887
|
+
for (const item of pending) {
|
|
888
|
+
this.pi?.appendEntry(NOTIFY_LEDGER_CUSTOM_TYPE, {
|
|
889
|
+
v: 1,
|
|
890
|
+
notifyId: item.notifyId,
|
|
891
|
+
content: item.content,
|
|
892
|
+
record: item.record,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
logger.warn(
|
|
896
|
+
`[subagents] shutdown flush blocked by busy main agent: ${pending.length} pending notification(s) persisted to ledger for replay on next session_start`,
|
|
897
|
+
{ count: pending.length },
|
|
3179
898
|
);
|
|
899
|
+
} catch (err) {
|
|
900
|
+
// 落盘复写是防丢失增强,失败不阻断 dispose(账本原 entry 仍随 pi flush 落盘)。
|
|
901
|
+
bestEffort(err, "persistUndeliveredNotificationsForReplay", "error");
|
|
3180
902
|
}
|
|
3181
903
|
}
|
|
3182
904
|
}
|
|
3183
905
|
|
|
3184
|
-
//
|
|
3185
|
-
//
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
function getServiceSlot(): ServiceSlot {
|
|
3191
|
-
let slot = Reflect.get(globalThis, SERVICE_SLOT_KEY) as ServiceSlot | undefined;
|
|
3192
|
-
if (!slot) {
|
|
3193
|
-
slot = { current: null };
|
|
3194
|
-
Reflect.set(globalThis, SERVICE_SLOT_KEY, slot);
|
|
3195
|
-
}
|
|
3196
|
-
return slot;
|
|
3197
|
-
}
|
|
3198
|
-
|
|
3199
|
-
/** 获取进程单例。session_start 前为 null。 */
|
|
3200
|
-
export function getSubagentService(): SubagentService | null {
|
|
3201
|
-
return getServiceSlot().current;
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
/** 设置进程单例(session_start 首次创建时)。 */
|
|
3205
|
-
export function setSubagentService(service: SubagentService): void {
|
|
3206
|
-
getServiceSlot().current = service;
|
|
3207
|
-
}
|
|
3208
|
-
|
|
3209
|
-
/**
|
|
3210
|
-
* [U10① D6] 第三宿主最小构造入口:仅凭参数注入构造 SubagentService(无全局查找)。
|
|
3211
|
-
*
|
|
3212
|
-
* 构造依赖(modelService / getMainSessionFile / uiRequestHandler)全部经 init
|
|
3213
|
-
* 参数注入;本工厂是 `new SubagentService(init)` 的薄包装,不读也不写
|
|
3214
|
-
* getSubagentService/setSubagentService 的全局槽位——session_start 单例流程
|
|
3215
|
-
* 行为零改动,宿主自持实例时用本工厂。构造内部行为与直接 new 逐字等价。
|
|
3216
|
-
*
|
|
3217
|
-
* @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
|
|
3218
|
-
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
3219
|
-
*/
|
|
3220
|
-
export function createSubagentService(init: SubagentServiceInit): SubagentService {
|
|
3221
|
-
return new SubagentService(init);
|
|
3222
|
-
}
|
|
906
|
+
// [R6] 进程单例访问器族(SERVICE_SLOT_KEY / getServiceSlot / getSubagentService /
|
|
907
|
+
// setSubagentService / createSubagentService)已外移 service/service-bootstrap.ts
|
|
908
|
+
//(globalThis[Symbol.for] slot 防 jiti 多实例分裂,机制注释随迁);barrel
|
|
909
|
+
// packages/subagent-core/src/index.ts 直接改指向该文件,壳不做 re-export(防壳↔
|
|
910
|
+
// bootstrap 值环——设计 v4 import 纪律)。
|