@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.
Files changed (176) hide show
  1. package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
  2. package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
  3. package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
  4. package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/execution/relay-env.js +5 -4
  9. package/dist/index.cjs +9799 -7954
  10. package/dist/index.d.cts +1896 -1360
  11. package/dist/index.d.ts +1896 -1360
  12. package/dist/index.js +8952 -7513
  13. package/dist.bundle/index.cjs +7897 -6054
  14. package/package.json +5 -5
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/record-store-last-line.test.ts +12 -0
  17. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  18. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  19. package/src/core/logger.ts +1 -1
  20. package/src/execution/__tests__/alive-store.test.ts +117 -1
  21. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  22. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  23. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  24. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  25. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  26. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  27. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  28. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  29. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  30. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  31. package/src/execution/__tests__/execution-record.test.ts +8 -148
  32. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  33. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  34. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  35. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  36. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  37. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  38. package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
  39. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  40. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  41. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  43. package/src/execution/__tests__/nested-visibility.test.ts +13 -1
  44. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  45. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  46. package/src/execution/__tests__/record-binding.test.ts +613 -0
  47. package/src/execution/__tests__/record-origin.test.ts +318 -0
  48. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  49. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  50. package/src/execution/__tests__/record-store.test.ts +72 -54
  51. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  52. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  53. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  54. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  55. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  56. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  57. package/src/execution/__tests__/state-marker.test.ts +288 -0
  58. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  59. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  60. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  61. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  62. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  63. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  64. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  65. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  66. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  67. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  68. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  69. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  70. package/src/execution/alive-store.ts +37 -28
  71. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  72. package/src/execution/config.ts +1 -1
  73. package/src/execution/conversation-continuation.ts +541 -0
  74. package/src/execution/dialog-queue.ts +60 -12
  75. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  76. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  77. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  78. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  79. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  80. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  81. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
  82. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  83. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  84. package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
  85. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  86. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  87. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  88. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  89. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  90. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  91. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  92. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  93. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  94. package/src/execution/engine/client/engine-client.ts +111 -26
  95. package/src/execution/engine/client/mirror.ts +9 -0
  96. package/src/execution/engine/client/remote-engine.ts +52 -34
  97. package/src/execution/engine/client/reverse-router.ts +6 -27
  98. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  99. package/src/execution/engine/common/capability-gate.ts +27 -0
  100. package/src/execution/engine/common/data-dir.ts +3 -2
  101. package/src/execution/engine/common/errors.ts +1 -1
  102. package/src/execution/engine/common/journal-wiring.ts +18 -9
  103. package/src/execution/engine/common/run-signals.ts +83 -0
  104. package/src/execution/engine/d8-compat.ts +1 -1
  105. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  106. package/src/execution/engine/host/host-bridge.ts +26 -41
  107. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  108. package/src/execution/engine/host/spawned-children.ts +1 -1
  109. package/src/execution/engine/inflight-snapshot.ts +92 -0
  110. package/src/execution/engine/port.ts +52 -54
  111. package/src/execution/engine/types.ts +11 -14
  112. package/src/execution/execution-record.ts +15 -47
  113. package/src/execution/finalize-record.ts +94 -197
  114. package/src/execution/idle-gc.ts +9 -3
  115. package/src/execution/lifecycle-manager.ts +25 -361
  116. package/src/execution/lifecycle-predicates.ts +22 -19
  117. package/src/execution/manifest-store.ts +40 -42
  118. package/src/execution/model-config-service.ts +1 -1
  119. package/src/execution/notifier.ts +39 -1
  120. package/src/execution/notify-host.ts +13 -4
  121. package/src/execution/path-encoding.ts +6 -0
  122. package/src/execution/record-entry.ts +20 -2
  123. package/src/execution/record-store.ts +925 -160
  124. package/src/execution/relay-env.ts +8 -3
  125. package/src/execution/round-supervisor/domain.ts +17 -5
  126. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  127. package/src/execution/round-supervisor/service-binding.ts +71 -21
  128. package/src/execution/round-supervisor/supervisor.ts +23 -9
  129. package/src/execution/service/record-access.ts +547 -0
  130. package/src/execution/service/record-lifecycle.ts +542 -0
  131. package/src/execution/service/run-orchestration.ts +1647 -0
  132. package/src/execution/service/service-bootstrap.ts +111 -0
  133. package/src/execution/service/service-constants.ts +27 -0
  134. package/src/execution/service/session-baselines.ts +401 -0
  135. package/src/execution/service/sync-collect-domain.ts +442 -0
  136. package/src/execution/service/workflow-dispatch.ts +567 -0
  137. package/src/execution/session-file-gc.ts +11 -8
  138. package/src/execution/session-reconstructor.ts +44 -1
  139. package/src/execution/sessions-index.ts +20 -3
  140. package/src/execution/settled-watchdog.ts +86 -43
  141. package/src/execution/state-marker.ts +516 -0
  142. package/src/execution/stream-sink.ts +1 -1
  143. package/src/execution/subagent-actions-core.ts +53 -31
  144. package/src/execution/subagent-service.ts +643 -2955
  145. package/src/execution/subprocess-agent-runner.ts +46 -323
  146. package/src/execution/types.ts +60 -43
  147. package/src/execution/ui-request-handler-factory.ts +5 -0
  148. package/src/execution/worktree-manager.ts +1 -1
  149. package/src/execution/worktree-registry.ts +12 -8
  150. package/src/index.ts +16 -5
  151. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  152. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  153. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  154. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  155. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  156. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  157. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  158. package/src/orchestration/execute-agent-call.ts +1 -1
  159. package/src/orchestration/file-run-store.ts +1 -1
  160. package/src/orchestration/models/ports.ts +20 -13
  161. package/src/orchestration/models/types.ts +11 -13
  162. package/src/orchestration/run-snapshot.ts +18 -22
  163. package/src/orchestration/worker-message-pump.ts +33 -57
  164. package/src/shared/atomic-write.ts +10 -8
  165. package/dist/chunk-T2SYBW3I.js +0 -24
  166. package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
  167. package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
  168. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  169. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  170. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  171. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  172. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  173. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  174. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  175. package/src/execution/finalized-marker.ts +0 -70
  176. package/src/execution/tombstone-store.ts +0 -72
@@ -0,0 +1,547 @@
1
+ // [H3/R3] RecordAccess 聚合(域 #3/#8/#10/#13:孤儿/manifest 恢复 + 查询面 + action
2
+ // 网关 + 身份解析/record 创建)——自 SubagentService 上帝类 strangler 抽取的第三个
3
+ // 聚合的**读建面**(设计 docs/design/subagent-service-decomposition.md §2.1 / §3.3 D5;
4
+ // 成员归属以 r0-inventory.md 清单① + 域分区为准)。
5
+ //
6
+ // [计划变更 D-R3-1] G1「每聚合 ≤700 行」与 R0 八域体量(分区实测 833 物理行)冲突,
7
+ // 拆两文件(dev agent 停线报告、主 agent 核验追认):本文件(读建面 #3/#8/#10/#13)
8
+ // + record-lifecycle.ts(终态迁移
9
+ // 写面 #4/#11/#17/#18,D5/H4 落点)。两文件组间零互调(已验证),各自独立 deps,
10
+ // 壳分别装配——聚合间零 import(G2 / R1 打样模式 3)。
11
+ //
12
+ // 单一职责:record 的**获得与可见性**——boot 孤儿自愈(manifest 重物化 / entry-born
13
+ // 恢复)、身份解析与 record 创建注册、内存∪磁盘查询投影、message/close action 的
14
+ // 归属校验网关(冷查/冷复活经 cold-lookup.ts)。终态迁移写面(close/cancel/finalize/
15
+ // dispose 批量回收)在 record-lifecycle.ts——D5「store 与终态迁移入口的唯一宿主」。
16
+ //
17
+ // [R1 打样模式——R3 落地](模式权威定义见 session-baselines.ts 文件头)
18
+ // 1. 依赖注入形态:deps 全晚绑定闭包(构造期零求值)。基线字段(pi/sessionRootId/
19
+ // sessionId/mainSessionFile/execNesting)运行时可变(initSession 注入 / 复活翻转),
20
+ // 断言/调用时经壳 getter 现读;叶子模块实例(store/manifestStore/modelService)为
21
+ // #1 留壳共享依赖,getter 现读同一实例(晚绑定形态统一)——深绑测试的 store 读
22
+ // 路径(ServiceInternals)保持生效,断言零改写。
23
+ // 2. 转发壳写法:壳保留同名方法(含原可见性)单行转发;本聚合公共面 = 壳转发面,
24
+ // 聚合内部互调(recoverOrphanRecords / rematerializeReconnectableEntryManifests /
25
+ // coldLookupDeps 消费)保持 private。原 private 方法因壳转发需要可见性放宽为
26
+ // public(strangler 必然,非行为变化)。
27
+ // 3. 只搬不改:方法体除依赖注入通道替换(this.X → this.deps.getX())外逐字节保留
28
+ // ——本聚合侧的写点通道(r0-inventory 清单②):#13 创建期 createRecord + store.register
29
+ // (C+B 通道)、#3 manifest 重物化投影(D 通道)原样随迁,不经任何中转。
30
+
31
+ import { getLogger } from "../../core/logger.ts";
32
+ import { toErrorMessage } from "../../core/error-message.ts";
33
+
34
+ import { bestEffort } from "../best-effort.ts";
35
+ import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdLookupDeps } from "../cold-lookup.ts";
36
+ import { createRecord, project, snapshot } from "../execution-record.ts";
37
+ import type { ExecutionNestingContext } from "../engine/common/nesting-guard.ts";
38
+ import {
39
+ joinEngineModelRef,
40
+ splitEngineModelRef,
41
+ validateModelForEngine,
42
+ withCrossEngineHint,
43
+ } from "../engine/model-validation.ts";
44
+ import type { EnginePort } from "../engine/port.ts";
45
+ import { getEngine, listEngines } from "../engine/registry.ts";
46
+ import type { ManifestStore } from "../manifest-store.ts";
47
+ import type { ModelConfigService } from "../model-config-service.ts";
48
+ import type { AgentConfig, ResolvedModel } from "../model-resolver.ts";
49
+ import type { RecordStore, StatusFilter } from "../record-store.ts";
50
+ // [R6/D-R3-2] 跨进程身份 env 名 ENV_SELF_RECORD_ID 归位常量叶子文件
51
+ // service-constants.ts(原 SSOT 在 session-baselines.ts,R3 时的聚合间单向 import
52
+ // 合法边随之消除)——聚合→支撑文件方向(import 常量),守卫允许。
53
+ import { ENV_SELF_RECORD_ID } from "./service-constants.ts";
54
+ import {
55
+ DEFAULT_AGENT_NAME,
56
+ isReconnectableFinalReason,
57
+ type ExecuteOptions,
58
+ type ExecutionHandle,
59
+ type ExecutionMode,
60
+ type ExecutionRecord,
61
+ type RecordSnapshot,
62
+ type SubagentRecord,
63
+ } from "../types.ts";
64
+
65
+ const logger = getLogger("subagents");
66
+
67
+ /** resolveIdentity 的产物——一次确定、写入 record 后不再变。
68
+ * [R3] 接口本体自壳文件迁入(唯一生产者 resolveIdentity/resolveIdentityForEngine);
69
+ * 壳经 type import 消费(execute/executeAndAwait/workflow 派发链的局部类型标注),
70
+ * 对外无导出面(原模块内私有接口)。 */
71
+ export interface ResolvedIdentity {
72
+ agent: string;
73
+ agentConfig: AgentConfig | undefined;
74
+ resolved: ResolvedModel;
75
+ }
76
+
77
+ /**
78
+ * [R1 打样模式 1] 聚合协作 deps——**全部晚绑定闭包,构造期零求值**。
79
+ *
80
+ * 窄结构类型只声明聚合真实消费的通道(不整实例注入)。本聚合无跨域编排回调:
81
+ * 读建面自足(查询/创建/恢复只依赖 #1 留壳叶子 + #2 基线字段现读);跨域边只有
82
+ * baselines deps.recoverOrphans 回调(R1 打样时预留)经壳转发指向本聚合的
83
+ * recoverOrphansIfRootProcess(壳装配零改动)。
84
+ */
85
+ export interface RecordAccessDeps {
86
+ /** [D4 下沉] assertReady 断言(本体在 SessionBaselines,壳转发)。查询/action
87
+ * 入口的就绪门(findRecord/lookupRecordAnyState/getRecordForAction)。 */
88
+ readonly assertReady: () => void;
89
+ /** RecordStore(#1 留壳共享依赖;本聚合消费面:恢复扫描/查询投影/注册/冷查)。 */
90
+ readonly getStore: () => RecordStore;
91
+ /** ManifestStore(#1 留壳;tmp 恢复 + 重物化投影写)。 */
92
+ readonly getManifestStore: () => ManifestStore;
93
+ /** ModelConfigService(resolveIdentity 三层解析 + agentConfig 加载)。 */
94
+ readonly getModelService: () => ModelConfigService;
95
+ /** 所属根 session ID(恢复过滤 / record 归属盖章 / 查询 root 过滤;#2 基线现读)。 */
96
+ readonly getSessionRootId: () => string | null;
97
+ /** 本进程 pi session ID(查询 root 过滤兜底;#2 基线现读)。 */
98
+ readonly getSessionId: () => string | null;
99
+ /** 主 session 文件(孤儿恢复扫描数据源;initSession 先赋值后恢复——时序就绪)。 */
100
+ readonly getMainSessionFile: () => string | undefined;
101
+ /** 嵌套身份基线 ALS(record 父链挂接 + action 网关直接父校验;#2 基线现读)。 */
102
+ readonly getExecNesting: () => ExecutionNestingContext;
103
+ }
104
+
105
+ /**
106
+ * 域 #3/#8/#10/#13 聚合:record 读建面(R3 自 SubagentService 抽取)。
107
+ *
108
+ * 字段所有权(r0-inventory 清单①):#30 coldLookupDeps(唯一消费方 getRecordForAction
109
+ * 冷查分支)。壳零感知其余内部态。
110
+ */
111
+ export class RecordAccess {
112
+ private readonly deps: RecordAccessDeps;
113
+
114
+ constructor(deps: RecordAccessDeps) {
115
+ this.deps = deps;
116
+ }
117
+
118
+ // ── 域 #3 孤儿/manifest 恢复 ──
119
+
120
+ /**
121
+ * 孤儿终态恢复(residual-fixes):session_start 主动触发一次——父扩展死后再无人写
122
+ * 终态 entry 的 record 在此判定落盘(否则侧栏永久 running)。幂等不 throw,失败不
123
+ * 阻断 session_start。
124
+ *
125
+ * [T5① / PS-8] 只有根进程做扫描者:恢复机制假设「单扫描者」,但子进程 sessionRootId
126
+ * 经 env 与父同值(过滤域 = 整树共享的 sessions/records 目录),env 贯穿让每个子进程
127
+ * 都成了扫描者——递归编排中任一子进程启动时,恰有兄弟记录 marker 缺失或超软超时
128
+ *(hours-long wave 必然命中)→ 活记录被无关进程盖 .finalized sidecar,closed entry
129
+ * 写进别的进程的 session 文件(跨进程互写,无任何锁)。子进程身份判据 = env
130
+ * PI_SUBAGENT_SELF_RECORD_ID(父 spawn 时注入的「子进程自己的 record id」,仅子进程
131
+ * 非空)——与 execNesting 基线同源。根进程恢复语义不变。
132
+ */
133
+ recoverOrphansIfRootProcess(): void {
134
+ const isChildProcess = (process.env[ENV_SELF_RECORD_ID] ?? "") !== "";
135
+ if (!isChildProcess) {
136
+ this.recoverOrphanRecords();
137
+ } else if (process.env.XYZ_AGENT_DEBUG) {
138
+ logger.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
139
+ }
140
+ }
141
+
142
+ /** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
143
+ * private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
144
+ * 是 initSession,转 private)。判定语义见 store 侧注释。
145
+ * mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
146
+ * 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
147
+ * 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
148
+ * E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。
149
+ * [M1 Gate B] 末段跑可重连 entry 的 manifest 重物化(闭含 dispose 时 manifest
150
+ * fire-and-forget 写被 SIGKILL 竞态吞掉的窗口)。 */
151
+ private recoverOrphanRecords(): void {
152
+ try {
153
+ this.deps.getStore().recoverOrphanRecords(this.deps.getSessionRootId() ?? undefined, this.deps.getMainSessionFile());
154
+ } catch (err) {
155
+ logger.warn("[subagents] orphan recovery failed", {
156
+ reason: toErrorMessage(err),
157
+ });
158
+ }
159
+ try {
160
+ this.deps.getStore().recoverEntryOnlyOrphans(this.deps.getMainSessionFile(), this.deps.getSessionRootId() ?? undefined);
161
+ } catch (err) {
162
+ logger.warn("[subagents] entry-only orphan recovery failed", {
163
+ reason: toErrorMessage(err),
164
+ });
165
+ }
166
+ try {
167
+ this.rematerializeReconnectableEntryManifests();
168
+ } catch (err) {
169
+ logger.warn("[subagents] reconnectable entry manifest re-materialization failed", {
170
+ reason: toErrorMessage(err),
171
+ });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * [M1 Gate B] 可重连终态 entry 的 manifest 重物化(boot 自愈段)。
177
+ *
178
+ * 缺口:编排性关闭(disposeAllRecords)的 manifest 写是 fire-and-forget——SIGKILL /
179
+ * 崩溃打进 shutdown 窗口时 entry(pi flush)与 manifest 写可能只活下来前者。末条
180
+ * subagent-record entry 为 closed + closedReason ∈ 可重连集(RECONNECTABLE_FINAL_
181
+ * REASONS = parent-shutdown/disconnected,types.ts SSOT)的 record,若查询面
182
+ * (collectRecords:内存∪磁盘∪manifest)已不可见,则从 entry 自描述快照重物化
183
+ * manifest——恢复 list 可见性 + message 的可重连分流(D4 revive 准入仍由
184
+ * cold-lookup 四守卫把门,重物化只补反查索引,不复活任何执行态)。
185
+ *
186
+ * 刻意收窄的语义边界:
187
+ * - 只认可重连集。user-close/cancelled(主动告别,close 语义不可旁路)与
188
+ * gc/parent-fork/parent-new(自洽终态,无续聊歧义)不重物化——条目自洽,
189
+ * 「不可恢复」即其对外语义,补可见性收益不抵语义面扩大(M1 负向断言锁定)。
190
+ * - 只补本 rootSessionId 的 entry(每 session boot 治自己的树;跨 session 记录
191
+ * 归属其自身 boot 段,防本进程替异树批量落盘)。
192
+ * - 已可见(磁盘锚或 manifest 幸存)的 id 跳过——重物化是幂等补缺,不是覆写源。
193
+ */
194
+ private rematerializeReconnectableEntryManifests(): void {
195
+ if (this.deps.getMainSessionFile() === undefined) return;
196
+ const visibleIds = new Set(
197
+ // [S3] includeWorkflow:true:已可见判定是全量存在性判定(幂等跳过守卫),
198
+ // 不吃 origin 缺省过滤——否则已可见的 reconnectable workflow record 被误判
199
+ // 不可见,每次 boot 冗余重物化 manifest(治理/全态查询语义,同 lookupRecordAnyState)。
200
+ this.deps.getStore().collectRecords(COLD_LOOKUP_SCAN_LIMIT, "all", undefined, true).map((r) => r.id),
201
+ );
202
+ for (const rec of this.deps.getStore().scanLastRecordEntries(this.deps.getMainSessionFile())) {
203
+ if (visibleIds.has(rec.id)) continue; // 查询面已可见:磁盘锚或 manifest 幸存
204
+ if (rec.rootSessionId !== this.deps.getSessionRootId()) continue; // 只治本 session 树
205
+ if (rec.status !== "closed" || !isReconnectableFinalReason(rec.closedReason)) continue;
206
+ // manifest 投影补写走 store 公开原语([H4 收口 / G1] store 外零 manifest 直写;
207
+ // status 恒 closed——entry 的 closed 即终态自描述,无 running 形态可达此处)。
208
+ this.deps.getStore().rematerializeManifest({
209
+ id: rec.id,
210
+ rootSessionId: rec.rootSessionId ?? "",
211
+ parentRecordId: rec.parentRecordId,
212
+ agentName: rec.agent,
213
+ status: "closed",
214
+ closedReason: rec.closedReason,
215
+ createdAt: rec.startedAt,
216
+ completedAt: rec.endedAt ?? Date.now(),
217
+ sessionFile: rec.sessionFile,
218
+ task: rec.task,
219
+ slug: rec.slug,
220
+ model: rec.model,
221
+ });
222
+ }
223
+ }
224
+
225
+ /** 启动清扫:manifest tmp 残留(崩溃打断的原子写留下的 *.json.tmp.<pid>)静默删除
226
+ * ——[U4c / D6] tmp 恢复已退役(manifest 是可丢可重建缓存,promote 语义失效)。
227
+ * 幂等,不 throw。ADR-035 启动清扫接线——session_start 每次都调(与
228
+ * maybeCleanupExpiredSessionFiles 一致)。manifestStore 保持 private 封装,本方法
229
+ * 是公开入口。 */
230
+ async recoverManifestTmpFiles(): Promise<{ deleted: number; recovered: number }> {
231
+ try {
232
+ const deleted = await this.deps.getManifestStore().sweepTmpFiles();
233
+ // recovered 恒 0:promote 已随 U4c 退役——公开签名保留 {deleted, recovered}
234
+ // 形态(subagent-service 壳转发 + subagent-workflow session-lifecycle 消费
235
+ // swept.deleted,改签名波及 extension 消费面)。
236
+ return { deleted, recovered: 0 };
237
+ } catch (err) {
238
+ bestEffort(err, "recoverManifestTmpFiles", "error");
239
+ return { deleted: 0, recovered: 0 };
240
+ }
241
+ }
242
+
243
+ // ── 域 #13 身份解析/record 创建 ──
244
+
245
+ /** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。
246
+ *
247
+ * [u-h2] pi 未命中跨引擎候选(D2-4):resolveModel 抛 notFoundError(pi registry
248
+ * 全等裁决未命中)时反查其他已注册引擎清单,唯一命中则追加「该 id 属于引擎 X」
249
+ * 候选段(场景 3);其余裁决失败(孪生歧义/auth)与命中路径原样返回(零回归)。
250
+ * execute() 与 executeAndAwait() 两个派发路径共享本方法,故 chat 与 workflow 域的
251
+ * pi 校验同享场景 3 文案。
252
+ */
253
+ async resolveIdentity(
254
+ opts: ExecuteOptions,
255
+ pre?: { agent: string; agentConfig: AgentConfig | undefined },
256
+ ): Promise<ResolvedIdentity> {
257
+ // agentRef 语义(S2):agent 参数 = .md 绝对路径;不传 = 不加载 agentConfig,
258
+ // 直接用 override → 主 agent model。DEFAULT_AGENT_NAME 仅作 record 显示名
259
+ // (TUI 层 extractAgentName 共用,保证显示一致)。
260
+ const agent = pre?.agent ?? opts.agent ?? DEFAULT_AGENT_NAME;
261
+ // 显式 agent ref(用户点名)失败必须报错,不静默降级:无 require 的 loadByPath
262
+ // 对相对路径/裸名/文件缺失都返回 undefined → agentConfig undefined → resolveModel
263
+ // 静默回落 override→主 agent model,用户拿到的 subagent 无 systemPrompt/工具白名单
264
+ // 且零反馈。require:true 让失败抛出带 <available_subagents> 指引的错误(对齐
265
+ // workflow name not found 反馈风格);不传 agent = 默认 general-purpose 语义,
266
+ // agentConfig 保持 undefined(合法缺省,走 override → ctxModel 兑底)。
267
+ // [u-h2 D2-1] execute() 已在路由前解析 agentConfig(pre 通道),此处复用不二次加载。
268
+ const agentConfig = pre
269
+ ? pre.agentConfig
270
+ : opts.agent
271
+ ? this.deps.getModelService().getRequiredAgentConfig(opts.agent)
272
+ : undefined;
273
+
274
+ let resolved: ResolvedModel;
275
+ try {
276
+ resolved = this.deps.getModelService().resolveModel(
277
+ opts.agent ?? "",
278
+ { model: opts.model, thinkingLevel: opts.thinkingLevel },
279
+ opts.ctxModel,
280
+ agentConfig,
281
+ );
282
+ } catch (err) {
283
+ throw withCrossEngineHint(err, listEngines(), (id) => {
284
+ try {
285
+ return getEngine(id);
286
+ } catch {
287
+ return undefined; // 清单快照与注册表并发变化的防御:取不到引擎按未注册处理
288
+ }
289
+ });
290
+ }
291
+
292
+ return { agent, agentConfig, resolved };
293
+ }
294
+
295
+ /**
296
+ * [u-h2 D2-1③] 非 pi 引擎的 identity 解析:跳过 pi registry 三层解析(ctxModel 主
297
+ * agent model 不透传——主 agent 的 pi id 对目标引擎大概率无效,缺省语义归引擎)。
298
+ *
299
+ * 逐层语义(设计 D2-1 归趋表):
300
+ * - model 源 = engineModel(调用参数 opts.model > agentConfig.model frontmatter,
301
+ * agent 作者声明不忽略——配错在 validateModel 同步报错,不落引擎缺省静默续跑);
302
+ * - 无显式 model → 校验/留痕走引擎缺省(validateModel(undefined) 的 canonicalRef);
303
+ * - thinkingLevel 直接透传(引擎中立参数,不涉 registry)。
304
+ *
305
+ * 引擎未实现 validateModel 时 modelRef 原样透传(其 prepare 期校验兜底,现状语义)。
306
+ */
307
+ resolveIdentityForEngine(
308
+ engine: EnginePort,
309
+ engineModel: string | undefined,
310
+ agent: string,
311
+ agentConfig: AgentConfig | undefined,
312
+ opts: ExecuteOptions,
313
+ ): ResolvedIdentity {
314
+ const canonical = validateModelForEngine(engine, engineModel);
315
+ // record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
316
+ // 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
317
+ // 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
318
+ // 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
319
+ // 整串进 name,不再落 "<ref>/" 畸形)。
320
+ const modelStr = canonical ?? engineModel ?? "";
321
+ const model = splitEngineModelRef(modelStr);
322
+ return {
323
+ agent,
324
+ agentConfig,
325
+ resolved: {
326
+ model: {
327
+ id: model.id,
328
+ name: model.name,
329
+ provider: model.provider,
330
+ reasoning: false,
331
+ },
332
+ thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
333
+ },
334
+ };
335
+ }
336
+
337
+ /** 步骤 2:按 mode 生成 id + controller,创建 record 并注册。
338
+ * [L-1] ExecutionMode 类型固定 "background"(sync 已删除),id/controller 分支简化。
339
+ * [H2 W2] originFields:workflow 域派发(executeWorkflowAgent)的来源身份——
340
+ * origin:"workflow" + parentRunId 进 record(D1);缺省不传 = tool 来源(存量零迁移)。 */
341
+ createRecordForMode(
342
+ identity: ResolvedIdentity,
343
+ opts: ExecuteOptions,
344
+ mode: ExecutionMode,
345
+ originFields?: { origin: "workflow"; parentRunId: string },
346
+ ): ExecutionRecord {
347
+ // FR-1: record id 用全局 UUID,不依赖 transcript/PID
348
+ const id = `sa-${crypto.randomUUID()}`;
349
+ const controller = new AbortController();
350
+
351
+ // 从 async 调用链读父执行上下文:主 session 链上无 store → 顶层 record;
352
+ // B run() 期间包了嵌套上下文,B 内创建 C 时读到 B → C.parentRecordId=B.id, C.depth=B.depth+1。
353
+ // depth 语义:顶层(无父)=0;有父=父 depth+1。靠 recordId 是否存在区分,不用负数魔数。
354
+ // [ALS 断裂修复] current() 内含基线兜底——本进程的身份在 initSession 已确定(env 注入),
355
+ // 任何上下文下都能正确挂父链。
356
+ const parentCtx = this.deps.getExecNesting().current();
357
+ const parentRecordId = parentCtx?.recordId;
358
+ const depth = parentCtx ? parentCtx.depth + 1 : 0;
359
+
360
+ const base = createRecord(id, {
361
+ agent: identity.agent,
362
+ // model 留痕词形与拆分同源(joinEngineModelRef):provider 为空串只写 id——
363
+ // 契约变更④的无斜杠 ref(provider=""/id=ref)不得落成 "/ref" 或 "ref/" 畸形;
364
+ // 续聊回读侧 splitEngineModelRef 对无斜杠串还原 provider=""/id=ref,往返自洽。
365
+ model: joinEngineModelRef(identity.resolved.model),
366
+ thinkingLevel: identity.resolved.thinkingLevel,
367
+ mode,
368
+ task: opts.task,
369
+ slug: opts.slug,
370
+ startedAt: Date.now(),
371
+ rootSessionId: this.deps.getSessionRootId() ?? undefined,
372
+ parentRecordId,
373
+ depth,
374
+ chatMode: opts.conversation === true,
375
+ idleTimeoutMs: opts.idleTimeoutMs,
376
+ // P4 引擎留痕(D9①):opts.engine/engineFallback 由引擎适配层写入(PiEngine.run
377
+ // 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
378
+ engine: opts.engine,
379
+ engineFallback: opts.engineFallback,
380
+ // subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
381
+ // 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
382
+ // undefined = async(缺省语义,旧记录零迁移)。
383
+ collectMode: opts.collect === "sync" ? "sync" : undefined,
384
+ controller,
385
+ });
386
+ // [H2 W2] 来源身份在对象构造点落位(origin/parentRunId 为 readonly,创建期一次性
387
+ // 写入——与 createRecord 的 identity 语义同款「创建时确定,不可变」)。
388
+ const record: ExecutionRecord =
389
+ originFields !== undefined
390
+ ? { ...base, origin: originFields.origin, parentRunId: originFields.parentRunId }
391
+ : base;
392
+
393
+ this.deps.getStore().register(record);
394
+ return record;
395
+ }
396
+
397
+ /** [MF#R4] worktree 前置失败的 early-return handle。
398
+ * record 已被 finalizeFailed 收尾为 failed、detached promise 从未启动。 */
399
+ buildEarlyFailedHandle(record: ExecutionRecord): ExecutionHandle {
400
+ const details = project(record);
401
+ return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details };
402
+ }
403
+
404
+ // ── 域 #8 查询面 ──
405
+
406
+ /**
407
+ * 按 id 查内存 running record 的只读快照(G3-002 修复)。
408
+ * 不从 session.jsonl 重建(cancel/list 单点查询只关心内存 running record)。
409
+ * 供 tool 层 cancelHandler 翻译 throw 用(id 不存在 / mode / 终态三种错误)。
410
+ * 不存在返回 undefined。
411
+ */
412
+ findRecord(id: string): RecordSnapshot | undefined {
413
+ this.deps.assertReady();
414
+ const record = this.deps.getStore().getMutable(id);
415
+ return record ? snapshot(record) : undefined;
416
+ }
417
+
418
+ /**
419
+ * [v8.5 A1/B] 全态查找:任意状态(running/closed)× 任意归属(含异 root session)的
420
+ * record 快照。供 message 拒绝文案分流(A1)与 fork-from 源解析(B)共用。
421
+ *
422
+ * 与 getRecordForAction 的差异:不做归属/直接父校验、不重建可变 record 入内存,
423
+ * 只读快照(light 形态可能缺详情重数据,身份/sidecar 状态字段齐全)。查询顺序与
424
+ * getRecordForAction 冷路径同款(idToFile 索引直查 → collectRecords 全扫兑底),
425
+ * 不限 status——终态(sidecar closed)记录也能查到。
426
+ *
427
+ * 返回 undefined:id 在内存与磁盘均不存在。
428
+ */
429
+ lookupRecordAnyState(id: string): SubagentRecord | undefined {
430
+ try {
431
+ this.deps.assertReady();
432
+ } catch {
433
+ return undefined; // 未初始化/disposed 时按「不存在」处理(文案分流无需区分)
434
+ }
435
+ const direct = this.deps.getStore().findLightById(id);
436
+ if (direct) return direct;
437
+ // [S2] 全扫兜底传 includeWorkflow:true:错误文案指引「add includeWorkflow:true」
438
+ // 意味着本查询必须能找到 workflow record,否则文案与查询能力不一致(治理/全态语义)。
439
+ return this.deps.getStore().collectRecords(COLD_LOOKUP_SCAN_LIMIT, "all", undefined, true).find((r) => r.id === id);
440
+ }
441
+
442
+ /** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
443
+ onChange(listener: () => void): () => void {
444
+ return this.deps.getStore().onChange(listener);
445
+ }
446
+
447
+ // [D4] listRunning 已删除:零生产调用方(TUI 计数经 collectRecords / notify-host 的
448
+ // piAdapter 直调 store.listRunning 覆盖),唯一消费是初始空态单测——保留 store 层方法。
449
+
450
+ /** 合并内存(running) + 磁盘(session.jsonl 重建) record(/subagents list + tool list 消费)。
451
+ * 按 rootSessionId 过滤:根进程=本 session(sessionRootId===sessionId);
452
+ * 子进程=env 贯穿的真 ROOT(sessionRootId≠sessionId)→ 看到整棵 ROOT 树(决策 3)。
453
+ * [perf] 磁盘源为 light(头部 identity + 状态,无 eventLog/result/turns 等重数据)
454
+ * ——列表/补全/hasRunning 够用;详情场景调 getFullRecord(id) 懒加载补齐。 */
455
+ collectRecords(
456
+ limit: number,
457
+ statusFilter: StatusFilter = "all",
458
+ includeWorkflow: boolean = false,
459
+ ): SubagentRecord[] {
460
+ return this.deps.getStore().collectRecords(limit, statusFilter, this.deps.getSessionRootId() ?? this.deps.getSessionId() ?? undefined, includeWorkflow);
461
+ }
462
+
463
+ /** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
464
+ * 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
465
+ * 返回 undefined:id 不存在于内存与磁盘。 */
466
+ getFullRecord(id: string): SubagentRecord | undefined {
467
+ return this.deps.getStore().getFullRecord(id);
468
+ }
469
+
470
+ // ── 域 #10 action 网关 ──
471
+
472
+ /** [D4-③] 冷路径查询依赖(cold-lookup.ts;deps 闭包惰性求值:sessionRootId /
473
+ * execNesting 基线运行时可变)。[R3] 初始化器内的箭头函数体运行时才解引用
474
+ * this.deps(构造器体先于任何调用完成),晚绑定语义与原壳字段一致。 */
475
+ private readonly coldLookupDeps: ColdLookupDeps = {
476
+ findLightById: (id) => this.deps.getStore().findLightById(id),
477
+ collectRecords: (limit, statusFilter, rootFilter) =>
478
+ this.deps.getStore().collectRecords(limit, statusFilter, rootFilter),
479
+ register: (record) => this.deps.getStore().register(record),
480
+ reportRecordTransition: (record) => this.deps.getStore().reportRecordTransition(record),
481
+ // [U2a/B4] 透明重生回边原语(store.markResurrected)——ColdLookupDeps 新增字段的
482
+ // 机械装配行(接口装配点不在 U2a 领地清单内的连带接线)。
483
+ markResurrected: (record, wasClosed) => this.deps.getStore().markResurrected(record, wasClosed),
484
+ getSessionRootId: () => this.deps.getSessionRootId(),
485
+ getBaselineRecordId: () => this.deps.getExecNesting().baseline()?.recordId ?? undefined,
486
+ };
487
+
488
+ /**
489
+ * 按 id 查 record 并做归属校验(message/close action 的统一入口)。
490
+ *
491
+ * 设计决策 3(归属守卫):校验 record.rootSessionId 必须等于当前 session 的根 id
492
+ *(this.sessionRootId)。不匹配 / 不存在统一抛「not found or not owned」——不区分
493
+ * 两种失败,防信息泄露(无法通过错误消息探测其他 session 的 subagent id)。
494
+ *
495
+ * 同进程内 running + idle record 都在内存(getMutable);终态 record 已 archive。
496
+ * 跨重启(SP-2)内存空时,从磁盘 collectRecords 重建 idle record 并 register 进内存。
497
+ * reconstructAll 已将跨重启 record(无 sidecar marker + pid 死)标记为 running(v4 B-1 跨重启可续聊语义,record-store buildRecord 分支 4),
498
+ * collectRecords 返回的 SubagentRecord 可直接转为可变 ExecutionRecord 供续操作。
499
+ *
500
+ * @param id subagent record id
501
+ * @param opts.allowReconnect [v8.5 D] message 专属:冷查额外接受「可重连」的 closed 记录
502
+ * (死因∈ RECONNECTABLE_FINAL_REASONS,A 档真实死因 sidecar 是唯一准入门),经四重守卫后
503
+ * resurrectClosed 回边为 running 并续写原 session 文件。仅 message 开启;close/cancel 维持单向终态语义。
504
+ * @returns 可变 ExecutionRecord(message/close handler 直接操作)
505
+ * @throws Error record 不存在 / 非本 session 所有(含恢复指引)
506
+ * @throws ResurrectDeniedError 命中可重连集但被 worktree/异进程活实例守卫拦截(自带完整行动语言)
507
+ */
508
+ getRecordForAction(id: string, opts?: { allowReconnect?: boolean }): ExecutionRecord {
509
+ this.deps.assertReady();
510
+ let record = this.deps.getStore().getMutable(id);
511
+ // SP-2 跨重启恢复:内存未命中时,从磁盘 collectRecords 重建 idle record。
512
+ // reconstructAll 已将跨重启 record(无 sidecar + pid 死)标记为 running(v4 B-1 可续聊语义,非 crashed),
513
+ // 直接转为可变 ExecutionRecord register 进内存,供 message/close action 续操作。
514
+ if (!record) {
515
+ // [D4-③] 冷查/复活链在 cold-lookup.ts([H1 U6] 原 cold-resurrect.ts 改名落位)。
516
+ record = coldLookupForAction(this.coldLookupDeps, id, opts?.allowReconnect === true);
517
+ }
518
+ if (!record || record.rootSessionId !== this.deps.getSessionRootId()) {
519
+ throw new Error(
520
+ `subagent not found or not owned: ${id}. Recovery: use action:'list' to confirm the id; ` +
521
+ `ended subagents cannot be messaged — start a new one; only subagents owned by the current session can be operated on.`,
522
+ );
523
+ }
524
+ // [v4 A-5 / P7] 直接父校验:rootSessionId 已确认 record 属于本 session 树,但递归场景下
525
+ // 孙级 record(parentRecordId = 某子进程的 self recordId)的子进程句柄只存在于其直接父
526
+ // 进程内存。主进程(基线 null)若仅凭 rootSessionId 通过就 message 孙级,会走
527
+ // 冷路径重新 spawn → 双写同一 session 文件(P7 双写者窗口)。统一用 baseline recordId 校验:
528
+ // - 主进程 baseline=undefined → 只能操作 parentRecordId=undefined 的根层 record
529
+ // - 子进程 baseline="sa-X" → 只能操作 parentRecordId="sa-X" 的直接孩子
530
+ // record.parentRecordId===undefined 视作根层,仅主进程可操作(身份缺省的旧/异常 record 归此)。
531
+ const baselineRecordId = this.deps.getExecNesting().baseline()?.recordId ?? undefined;
532
+ if (record.parentRecordId !== baselineRecordId) {
533
+ throw new Error(
534
+ `subagent ${id} is owned by its direct parent; message it through that parent ` +
535
+ `(see /subagents list, parent=${record.parentRecordId ?? "(root layer)"}). [v4 A-5] cross-layer ` +
536
+ `ownership guard: this process's baseline=${baselineRecordId ?? "(root)"} is not the direct parent of ${id}; ` +
537
+ `operating here would race the owning child process's handle and double-write the session file.`,
538
+ );
539
+ }
540
+ return record;
541
+ }
542
+
543
+ // [D4-③] 冷路径查询链(findColdLookupCandidate / assertReconnectAllowed /
544
+ // resurrectColdRecord / coldLookupForAction + isReconnectableClosed 判定)落位
545
+ // cold-lookup.ts(本聚合经 coldLookupDeps 注入)。
546
+ // SP-2 冷路径 [perf] 语义不变:idToFile 索引直查 → collectRecords 全扫兜底。
547
+ }