@zhushanwen/subagent-core 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/{chunk-4KN4TTG7.js → chunk-43ONBFZX.js} +1 -1
  2. package/dist/{chunk-X7SZ5HLQ.js → chunk-V3VHZ2VX.js} +20 -27
  3. package/dist/execution/engine/engines/zcode/constants.cjs +29 -41
  4. package/dist/execution/engine/engines/zcode/constants.d.cts +65 -53
  5. package/dist/execution/engine/engines/zcode/constants.d.ts +65 -53
  6. package/dist/execution/engine/engines/zcode/constants.js +17 -27
  7. package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
  8. package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
  9. package/dist/execution/engine/engines/zcode/reader.js +2 -2
  10. package/dist/index.cjs +16418 -14070
  11. package/dist/index.d.cts +5136 -4016
  12. package/dist/index.d.ts +5136 -4016
  13. package/dist/index.js +16343 -14048
  14. package/dist/{types-DpUO16pj.d.cts → types-Dv4QhSJ_.d.cts} +215 -112
  15. package/dist/{types-DpUO16pj.d.ts → types-Dv4QhSJ_.d.ts} +215 -112
  16. package/package.json +3 -3
  17. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
  18. package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
  19. package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
  20. package/src/__tests__/m2-append-content-probe.test.ts +0 -1
  21. package/src/__tests__/manifest-store.test.ts +10 -9
  22. package/src/__tests__/record-store-cache.test.ts +0 -2
  23. package/src/__tests__/record-store-index.test.ts +1 -2
  24. package/src/__tests__/review-fix-loop-script.test.ts +4 -2
  25. package/src/__tests__/review-fix-loop-utils.test.ts +10 -10
  26. package/src/__tests__/robustness-low-batch1.test.ts +6 -5
  27. package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
  28. package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
  29. package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
  30. package/src/__tests__/session-runner.test.ts +1 -1
  31. package/src/core/__tests__/host-services.test.ts +2 -2
  32. package/src/core/__tests__/logger.test.ts +1 -1
  33. package/src/core/error-message.ts +9 -0
  34. package/src/core/host-services.ts +21 -5
  35. package/src/core/notify-ports.ts +18 -5
  36. package/src/execution/__tests__/agent-profile.test.ts +4 -4
  37. package/src/execution/__tests__/agent-registry.test.ts +370 -0
  38. package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
  39. package/src/execution/__tests__/agents-assembly.test.ts +2 -2
  40. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
  41. package/src/execution/__tests__/chat-engine-routing.test.ts +68 -33
  42. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +4 -4
  43. package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
  44. package/src/execution/__tests__/delivery-methods.test.ts +75 -62
  45. package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
  46. package/src/execution/__tests__/descendant-sweep.test.ts +6 -6
  47. package/src/execution/__tests__/dialog-queue.test.ts +2 -2
  48. package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
  49. package/src/execution/__tests__/epipe-fallback.test.ts +29 -24
  50. package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
  51. package/src/execution/__tests__/execute-nesting.test.ts +34 -188
  52. package/src/execution/__tests__/execution-record.test.ts +217 -60
  53. package/src/execution/__tests__/execution-runtime-face.test.ts +5 -5
  54. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
  55. package/src/execution/__tests__/finalize-record.test.ts +173 -17
  56. package/src/execution/__tests__/gc-timer.test.ts +55 -2
  57. package/src/execution/__tests__/get-record-for-action-restart.test.ts +12 -12
  58. package/src/execution/__tests__/get-state-handshake.test.ts +1 -1
  59. package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
  60. package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
  61. package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
  62. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
  63. package/src/execution/__tests__/keep-alive-no-progress.test.ts +50 -148
  64. package/src/execution/__tests__/kill-all-escalation.test.ts +2 -2
  65. package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
  66. package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
  67. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +1 -1
  68. package/src/execution/__tests__/model-resolver.test.ts +2 -2
  69. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
  70. package/src/execution/__tests__/nested-visibility.test.ts +2 -0
  71. package/src/execution/__tests__/notify-ledger.test.ts +973 -18
  72. package/src/execution/__tests__/output-collector.test.ts +117 -6
  73. package/src/execution/__tests__/pi-invocation.test.ts +1 -1
  74. package/src/execution/__tests__/record-store.test.ts +10 -9
  75. package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
  76. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
  77. package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
  78. package/src/execution/__tests__/rpc-mode.test.ts +2 -2
  79. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
  80. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +15 -9
  81. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +4 -4
  82. package/src/execution/__tests__/run-spawn-edges.test.ts +17 -15
  83. package/src/execution/__tests__/run-spawn-integration.test.ts +2 -2
  84. package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
  85. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
  86. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +4 -4
  87. package/src/execution/__tests__/service-kill-escalation.test.ts +7 -5
  88. package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
  89. package/src/execution/__tests__/session-file-gc.test.ts +35 -0
  90. package/src/execution/__tests__/session-pending.test.ts +257 -14
  91. package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
  92. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +1 -1
  93. package/src/execution/__tests__/session-runner-close-prune.test.ts +2 -2
  94. package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
  95. package/src/execution/__tests__/session-runner-epipe.test.ts +3 -3
  96. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +5 -4
  97. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
  98. package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
  99. package/src/execution/__tests__/settled-watchdog.test.ts +282 -47
  100. package/src/execution/__tests__/spawn-args.test.ts +11 -10
  101. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
  102. package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
  103. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
  104. package/src/execution/__tests__/spawned-children.test.ts +2 -1
  105. package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
  106. package/src/execution/__tests__/stdin-writer.test.ts +6 -4
  107. package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
  108. package/src/execution/__tests__/subagent-actions-core.test.ts +42 -22
  109. package/src/execution/__tests__/subagent-service-message-close.test.ts +29 -26
  110. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +8 -8
  111. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -22
  112. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +30 -29
  113. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +45 -20
  114. package/src/execution/__tests__/subagent-service.test.ts +37 -30
  115. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
  116. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
  117. package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
  118. package/src/execution/__tests__/temp-prompt.test.ts +1 -1
  119. package/src/execution/__tests__/timeout-integration.test.ts +6 -5
  120. package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
  121. package/src/execution/__tests__/turn-limiter.test.ts +1 -1
  122. package/src/execution/__tests__/ui-request-handler-factory.test.ts +12 -6
  123. package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
  124. package/src/execution/__tests__/ui-request-queue.test.ts +142 -2
  125. package/src/execution/__tests__/worktree-manager.test.ts +14 -5
  126. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
  127. package/src/execution/agent-registry.ts +74 -30
  128. package/src/execution/agent-result-mapper.ts +3 -0
  129. package/src/execution/cold-resurrect.ts +199 -0
  130. package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
  131. package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
  132. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
  133. package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
  134. package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
  135. package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
  136. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
  137. package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
  138. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +22 -112
  139. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +15 -28
  140. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +12 -12
  141. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
  142. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +21 -82
  143. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +4 -7
  144. package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
  145. package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
  146. package/src/execution/engine/__tests__/registry.test.ts +4 -2
  147. package/src/execution/engine/__tests__/routing.test.ts +115 -6
  148. package/src/execution/engine/common/capability-gate.ts +92 -0
  149. package/src/execution/engine/common/journal-wiring.ts +97 -0
  150. package/src/execution/engine/common/kill-chain.ts +42 -10
  151. package/src/execution/engine/common/nesting-guard.ts +66 -0
  152. package/src/execution/engine/common/persona-router.ts +18 -8
  153. package/src/execution/engine/common/pool-manager.ts +332 -78
  154. package/src/execution/engine/common/session-view-service.ts +506 -0
  155. package/src/execution/engine/common/session-view-types.ts +125 -0
  156. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
  157. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
  158. package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
  159. package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +1 -1
  160. package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
  161. package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
  162. package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
  163. package/src/execution/engine/engines/pi/reader.ts +11 -0
  164. package/src/execution/engine/engines/pi/registration.ts +4 -2
  165. package/src/execution/{session-runner.ts → engine/engines/pi/session-runner.ts} +617 -412
  166. package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
  167. package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
  168. package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
  169. package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
  170. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +54 -27
  171. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
  172. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +13 -12
  173. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
  174. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
  175. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
  176. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
  177. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +20 -8
  178. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +77 -110
  179. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +25 -319
  180. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
  181. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
  182. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
  183. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
  184. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +48 -148
  185. package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
  186. package/src/execution/engine/engines/zcode/connection.ts +24 -11
  187. package/src/execution/engine/engines/zcode/constants.ts +75 -71
  188. package/src/execution/engine/engines/zcode/golden-sample.ts +8 -46
  189. package/src/execution/engine/engines/zcode/parser.ts +12 -346
  190. package/src/execution/engine/engines/zcode/preparer.ts +12 -143
  191. package/src/execution/engine/engines/zcode/session-channel.ts +351 -39
  192. package/src/execution/engine/engines/zcode/zcode-engine.ts +665 -760
  193. package/src/execution/engine/host-task-spec.ts +32 -36
  194. package/src/execution/engine/model-validation.ts +177 -0
  195. package/src/execution/engine/port.ts +24 -6
  196. package/src/execution/engine/routing.ts +75 -1
  197. package/src/execution/engine/types.ts +30 -103
  198. package/src/execution/execution-record.ts +222 -144
  199. package/src/execution/finalize-record.ts +131 -93
  200. package/src/execution/idle-gc.ts +28 -0
  201. package/src/execution/lifecycle-predicates.ts +1 -1
  202. package/src/execution/notify-host.ts +212 -0
  203. package/src/execution/record-store.ts +119 -73
  204. package/src/execution/round-settlement.ts +93 -0
  205. package/src/execution/session-file-gc.ts +96 -62
  206. package/src/execution/session-reconstructor.ts +256 -143
  207. package/src/execution/sessions-index.ts +117 -47
  208. package/src/execution/settled-watchdog.ts +264 -52
  209. package/src/execution/subagent-actions-core.ts +22 -22
  210. package/src/execution/subagent-service.ts +815 -963
  211. package/src/execution/subprocess-agent-runner.ts +144 -97
  212. package/src/execution/types.ts +9 -0
  213. package/src/execution/ui-request-handler-factory.ts +102 -74
  214. package/src/execution/ui-request-queue.ts +50 -19
  215. package/src/index.ts +442 -316
  216. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +1 -1
  217. package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
  218. package/src/orchestration/__tests__/args-validator.test.ts +14 -0
  219. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +1 -1
  220. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +11 -9
  221. package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
  222. package/src/orchestration/__tests__/file-run-store.test.ts +7 -6
  223. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +2 -2
  224. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +1 -1
  225. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
  226. package/src/orchestration/__tests__/lifecycle.test.ts +7 -5
  227. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
  228. package/src/orchestration/__tests__/run-snapshot.test.ts +8 -5
  229. package/src/orchestration/__tests__/script-lint.test.ts +17 -0
  230. package/src/orchestration/__tests__/test-mocks.ts +2 -0
  231. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
  232. package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
  233. package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +27 -19
  234. package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
  235. package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
  236. package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
  237. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +1 -1
  238. package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
  239. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
  240. package/src/orchestration/agent-opts-resolver.ts +1 -1
  241. package/src/orchestration/args-validator.ts +53 -31
  242. package/src/orchestration/execute-agent-call.ts +19 -73
  243. package/src/orchestration/launcher.ts +128 -81
  244. package/src/orchestration/lifecycle.ts +43 -45
  245. package/src/orchestration/models/ports.ts +4 -4
  246. package/src/orchestration/models/run-runtime.ts +2 -2
  247. package/src/orchestration/models/trace.ts +1 -1
  248. package/src/orchestration/models/types.ts +111 -22
  249. package/src/orchestration/models/workflow-run.ts +1 -1
  250. package/src/orchestration/script-lint.ts +91 -58
  251. package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +157 -150
  252. package/src/shared/__tests__/meta-parser.test.ts +151 -0
  253. package/src/shared/__tests__/resource-discovery.test.ts +258 -215
  254. package/src/shared/atomic-write.ts +12 -9
  255. package/src/shared/meta-parser.ts +158 -84
  256. package/src/shared/model-ref.ts +15 -2
  257. package/src/shared/resource-discovery.ts +9 -183
  258. package/src/shared/schema-jsonify.ts +1 -1
  259. package/workflows/review-fix-loop-utils.cjs +151 -89
  260. package/workflows/review-fix-loop-utils.d.cts +287 -0
  261. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
  262. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  263. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  264. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  265. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  266. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +0 -387
  267. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -710
  268. package/src/execution/engine/engines/zcode/appserver-home.ts +0 -442
  269. package/src/execution/engine/engines/zcode/appserver-probe.ts +0 -141
  270. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  271. package/src/execution/execute-options-mapper.ts +0 -115
  272. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
@@ -2,11 +2,19 @@
2
2
  //
3
3
  // 概率性清理过期 subagent session 文件(TTL 30 天)。
4
4
  // session_start 时调用,best-effort(失败不影响启动)。
5
+ //
6
+ // [D8 池引用计数接线] 同概率门内追加引擎池 TTL 兜底(cleanupExpiredPoolRefs):
7
+ // journal / refs.json 条目按同 30 天 mtime 回收。record 主数据(主 session 文件里
8
+ // 的 subagent-record entry)由 pi 侧管理,其删除对 core 无触发点——done record 的
9
+ // journal 没有精确回收锚,只能靠此兜底;workflow 域(taskId 占位无 record 生命周期
10
+ // 锚)同样依赖它(D8 分域口径「journal 依赖 30 天 TTL 自然回收」的落地)。
5
11
 
6
12
  import * as fs from "node:fs";
7
13
  import * as path from "node:path";
8
14
 
9
15
  import { isProcessAlive, readAliveMarker } from "./alive-store.ts";
16
+ import { getEngineDataDir } from "./engine/common/data-dir.ts";
17
+ import { cleanupExpiredPoolRefs } from "./engine/common/pool-manager.ts";
10
18
 
11
19
  /** 30 天 TTL(毫秒)。 */
12
20
  const TTL_DAYS = 30;
@@ -29,21 +37,105 @@ const SUBAGENTS_DIR = "subagents";
29
37
  * 1. 概率性触发(CLEANUP_PROBABILITY)
30
38
  * 2. 递归扫描 <agentDir>/subagents 下所有 .jsonl 文件
31
39
  * 3. mtime 超 TTL → unlink
40
+ * 4. [D8] 引擎池 TTL 兜底:journal / refs 条目超龄回收(见文件头注释)
32
41
  */
33
42
  export function maybeCleanupExpiredSessionFiles(agentDir: string, cwd: string): void {
34
43
  void cwd;
35
44
  try {
36
45
  if (Math.random() >= CLEANUP_PROBABILITY) return;
37
46
  const dir = path.join(agentDir, SUBAGENTS_DIR);
38
- if (!fs.existsSync(dir)) return;
39
- const now = Date.now();
40
- walkAndClean(dir, now);
47
+ if (fs.existsSync(dir)) {
48
+ walkAndClean(dir, Date.now());
49
+ }
50
+ cleanupEnginePoolsBestEffort();
41
51
  } catch (_e) {
42
52
  // best-effort:任何异常不阻断 session_start
43
53
  void _e;
44
54
  }
45
55
  }
46
56
 
57
+ /** 引擎池 TTL 兜底(dataDir 解析失败 = 未配置宿主服务,best-effort 吞掉)。 */
58
+ function cleanupEnginePoolsBestEffort(): void {
59
+ try {
60
+ cleanupExpiredPoolRefs(getEngineDataDir(), TTL_MS);
61
+ } catch (_e) {
62
+ void _e;
63
+ }
64
+ }
65
+
66
+ /** 孤儿 sidecar 名判定:.cancelled/.finalized/.alive,以及 [MF#2] patch 回传
67
+ * (<branch>.patch 按 branch 命名,与 .jsonl basename 无关联,删除 .jsonl 时无法同删;
68
+ * 作为孤儿按 TTL 清理,避免永久堆积)。 */
69
+ function isOrphanSidecarName(name: string): boolean {
70
+ return (
71
+ name.endsWith(".cancelled") ||
72
+ name.endsWith(".finalized") ||
73
+ name.endsWith(".alive") ||
74
+ name.endsWith(".patch")
75
+ );
76
+ }
77
+
78
+ /** stat 超 TTL 即 unlink(manifest .json 与孤儿 sidecar 共用原语)。失败静默(文件可能已被删除)。 */
79
+ function unlinkIfExpired(full: string, now: number): void {
80
+ try {
81
+ const stat = fs.statSync(full);
82
+ if (now - stat.mtimeMs > TTL_MS) {
83
+ fs.unlinkSync(full);
84
+ }
85
+ } catch (_e) {
86
+ // 文件可能已被删除,忽略
87
+ void _e;
88
+ }
89
+ }
90
+
91
+ /** .jsonl 超 TTL 清理:.alive 探活保护(活进程的 session 不删,D-024 安全网)+
92
+ * 同名 sidecar 一起清理。失败静默(文件可能已被删除)。 */
93
+ function cleanExpiredJsonl(full: string, now: number): void {
94
+ try {
95
+ const stat = fs.statSync(full);
96
+ if (now - stat.mtimeMs > TTL_MS) {
97
+ // .alive 探活:活进程的 session 不删(D-024 安全网)。
98
+ const aliveMarker = readAliveMarker(full);
99
+ if (aliveMarker !== undefined && isProcessAlive(aliveMarker.pid)) {
100
+ return; // 活进程 → 跳过,不清理
101
+ }
102
+ fs.unlinkSync(full);
103
+ // 同名 sidecar 一起清理。
104
+ for (const ext of [".cancelled", ".finalized", ".alive"]) {
105
+ try { fs.unlinkSync(`${full}${ext}`); } catch (_e) { void _e; /* sidecar 可能不存在 */ }
106
+ }
107
+ }
108
+ } catch (_e) {
109
+ // 文件可能已被删除,忽略
110
+ void _e;
111
+ }
112
+ }
113
+
114
+ /** 单条 dirent 分发。分支判断顺序与 fs 副作用时序保持与原实现一致:
115
+ * 目录递归 →(records 内)manifest .json → .jsonl → 孤儿 sidecar。 */
116
+ function cleanDirent(dir: string, entry: fs.Dirent, now: number, allowManifestJson: boolean): void {
117
+ const full = path.join(dir, entry.name);
118
+ if (entry.isDirectory()) {
119
+ // 只在进入名为 records 的子目录时打开 manifest .json 清理。
120
+ // records 在 <enc>/records/ 下递归自动覆盖;其他位置(如 subagents/worktrees.json)
121
+ // 不能匹配 .json——否则会误删 worktree reaper 依赖的状态文件。
122
+ walkAndClean(full, now, entry.name === "records");
123
+ } else if (
124
+ allowManifestJson &&
125
+ entry.name.endsWith(".json") &&
126
+ // 跳过 .tmp.:recoverTmpFiles(session_start)同步处理 tmp,GC 不重复。
127
+ // 不校验内容——30 天 mtime 已是强 orphan 信号,扩展名 + 文件名足够。
128
+ !entry.name.includes(".tmp.")
129
+ ) {
130
+ unlinkIfExpired(full, now);
131
+ } else if (entry.name.endsWith(".jsonl")) {
132
+ cleanExpiredJsonl(full, now);
133
+ } else if (isOrphanSidecarName(entry.name)) {
134
+ // 孤儿 sidecar(兄弟 .jsonl 已被外部删除):按同 TTL 清理。
135
+ unlinkIfExpired(full, now);
136
+ }
137
+ }
138
+
47
139
  /** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。
48
140
  * [F2] 进入名为 records 的子目录时,额外清理超 TTL 的 manifest .json(跳过 .tmp.——
49
141
  * recoverTmpFiles 同步处理)。allowManifestJson 仅由父调用按目录名开启,其他位置
@@ -57,64 +149,6 @@ function walkAndClean(dir: string, now: number, allowManifestJson = false): void
57
149
  return;
58
150
  }
59
151
  for (const entry of entries) {
60
- const full = path.join(dir, entry.name);
61
- if (entry.isDirectory()) {
62
- // 只在进入名为 records 的子目录时打开 manifest .json 清理。
63
- // records 在 <enc>/records/ 下递归自动覆盖;其他位置(如 subagents/worktrees.json)
64
- // 不能匹配 .json——否则会误删 worktree reaper 依赖的状态文件。
65
- walkAndClean(full, now, entry.name === "records");
66
- } else if (
67
- allowManifestJson &&
68
- entry.name.endsWith(".json") &&
69
- // 跳过 .tmp.:recoverTmpFiles(session_start)同步处理 tmp,GC 不重复。
70
- // 不校验内容——30 天 mtime 已是强 orphan 信号,扩展名 + 文件名足够。
71
- !entry.name.includes(".tmp.")
72
- ) {
73
- try {
74
- const stat = fs.statSync(full);
75
- if (now - stat.mtimeMs > TTL_MS) {
76
- fs.unlinkSync(full);
77
- }
78
- } catch (_e) {
79
- // 文件可能已被删除,忽略
80
- void _e;
81
- }
82
- } else if (entry.name.endsWith(".jsonl")) {
83
- try {
84
- const stat = fs.statSync(full);
85
- if (now - stat.mtimeMs > TTL_MS) {
86
- // .alive 探活:活进程的 session 不删(D-024 安全网)。
87
- const aliveMarker = readAliveMarker(full);
88
- if (aliveMarker !== undefined && isProcessAlive(aliveMarker.pid)) {
89
- continue; // 活进程 → 跳过,不清理
90
- }
91
- fs.unlinkSync(full);
92
- // 同名 sidecar 一起清理。
93
- for (const ext of [".cancelled", ".finalized", ".alive"]) {
94
- try { fs.unlinkSync(`${full}${ext}`); } catch (_e) { void _e; /* sidecar 可能不存在 */ }
95
- }
96
- }
97
- } catch (_e) {
98
- // 文件可能已被删除,忽略
99
- void _e;
100
- }
101
- } else if (
102
- entry.name.endsWith(".cancelled") ||
103
- entry.name.endsWith(".finalized") ||
104
- entry.name.endsWith(".alive") ||
105
- // [MF#2] patch 回传(<branch>.patch)按 branch 命名,与 .jsonl basename 无关联,
106
- // 删除 .jsonl 时无法同删;作为孤儿按 TTL 清理,避免永久堆积。
107
- entry.name.endsWith(".patch")
108
- ) {
109
- // 孤儿 sidecar(兄弟 .jsonl 已被外部删除):按同 TTL 清理。
110
- try {
111
- const stat = fs.statSync(full);
112
- if (now - stat.mtimeMs > TTL_MS) {
113
- fs.unlinkSync(full);
114
- }
115
- } catch (_e) {
116
- void _e;
117
- }
118
- }
152
+ cleanDirent(dir, entry, now, allowManifestJson);
119
153
  }
120
154
  }
@@ -274,39 +274,16 @@ interface PendingToolCall {
274
274
  }
275
275
 
276
276
  // ============================================================
277
- // 公开函数
277
+ // 全量重建内部 helper(按处理阶段拆分:读文件 → 扫身份 → 重建 turns → 组装)
278
278
  // ============================================================
279
279
 
280
280
  /**
281
- * session.jsonl 重建完整 SubagentRecord 数据。
282
- *
283
- * ╔══════════════════════════════════════════════════════════════════╗
284
- * ║ 1. readFileSync(sessionFile) + 逐行 JSON.parse(跳 header) ║
285
- * ║ (损坏行静默跳过;失败 → undefined) ║
286
- * ║ 2. 扫 custom entry(customType:"subagent-identity")→ 身份 ║
287
- * ║ 缺 identity → undefined(无法构造 record) ║
288
- * ║ 3. 顺序遍历 message entries: ║
289
- * ║ - role:"assistant" → 开新 Turn ║
290
- * ║ text/thinking 累积;usage → usageDelta ║
291
- * ║ toolCall block → 待匹配队列;stopReason error → lastError ║
292
- * ║ - role:"toolResult" → 配对 toolCallId ║
293
- * ║ 产 InternalToolCall(done/failed)推进请求 Turn ║
294
- * ║ 4. 闭合所有 turn;算 turnCount/totalTokens/result/eventLog ║
295
- * ╚══════════════════════════════════════════════════════════════════╝
296
- *
297
- * 返回 undefined:文件缺失/空/损坏/缺 identity entry/无 assistant message。
298
- * 不抛——消费方(collectRecords)降级跳过该 record。
299
- *
300
- * status 由最后一条 assistant message 的 stopReason 推导(error/aborted → failed,
301
- * 其余 → done)。cancelled 由 tombstone override(record-store 层),本函数不感知。
302
- *
303
- * [perf] 本函数是重路径(全文读 + 全 entry 解析)。列表扫描用 readIdentityHeader
304
- * (只读头部 identity),详情才走本函数(RecordStore.getFullRecord 懒加载)。
281
+ * 读文件 + 逐行 JSON.parse(session.jsonl = newline-delimited JSON)。
282
+ * 第 1 行是 session header(type:"session",跳过);后续每行一个 entry。
283
+ * 损坏行静默跳过(与 SDK parseSessionEntries 行为一致)。
284
+ * @returns 文件缺失/读失败 undefined;否则 entry 数组(可能为空)。
305
285
  */
306
- export function reconstructFromFile(sessionFile: string): ReconstructedRecord | undefined {
307
- // 读文件 + 逐行 JSON.parse(session.jsonl = newline-delimited JSON)。
308
- // 第 1 行是 session header(跳过);后续每行一个 entry。
309
- // 损坏行静默跳过(与 SDK parseSessionEntries 行为一致)。
286
+ function readJsonlEntries(sessionFile: string): JsonlEntry[] | undefined {
310
287
  let raw: string;
311
288
  try {
312
289
  raw = fs.readFileSync(sessionFile, "utf-8");
@@ -338,10 +315,18 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
338
315
  void _e; // 损坏行跳过(不阻断后续行)。
339
316
  }
340
317
  }
318
+ return entries;
319
+ }
341
320
 
342
- if (entries.length === 0) return undefined;
321
+ /** 身份/模型扫描产出(identity custom entry + 途经的 model/thinking_level change)。 */
322
+ interface IdentityScan {
323
+ identity: SubagentIdentityData | undefined;
324
+ model: string;
325
+ thinkingLevel: string | undefined;
326
+ }
343
327
 
344
- // ── 扫身份 custom entry(必须存在,否则无法构造 record)──
328
+ /** identity custom entry(必须存在,否则无法构造 record)+ 途经 model/thinking_level。 */
329
+ function scanIdentityAndModel(entries: JsonlEntry[]): IdentityScan {
345
330
  let identity: SubagentIdentityData | undefined;
346
331
  let model = "";
347
332
  let thinkingLevel: string | undefined;
@@ -357,14 +342,95 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
357
342
  if (typeof entry.thinkingLevel === "string") thinkingLevel = entry.thinkingLevel;
358
343
  }
359
344
  }
360
- if (!identity) return undefined; // 缺身份 无法构造 record。
345
+ return { identity, model, thinkingLevel };
346
+ }
347
+
348
+ /** message entries 重建产出(turns + 跨 entry 累积状态)。 */
349
+ interface RebuiltTurns {
350
+ turns: Turn[];
351
+ lastError: string | undefined;
352
+ totalTokens: number;
353
+ /** 最后一条 entry 的时间戳(ms),推导 endedAt(避免重建 record 耗时随墙钟无限增长)。 */
354
+ lastEntryTsMs: number | undefined;
355
+ }
356
+
357
+ /**
358
+ * assistant message → 填充调用方已创建的空 Turn:text/thinking 累积;toolCall block 进
359
+ * 待匹配队列;usage → usageDelta(token 总量增量返回给调用方累加)。
360
+ *
361
+ * stopReason 语义(与 updateFromEvent 一致):error/aborted → 设 lastError;
362
+ * stop(正常结束)→ 清 lastError(镜像 turn_end 的清除语义,即前序 turn 的瞬态
363
+ * error 在后续成功 turn 后恢复,不误判 success=false);其余 stopReason → 保持
364
+ * prevLastError 原值。 [v4 B-1] status 恒 closed 后 lastStopReason 不再参与推导,死变量已删。
365
+ *
366
+ * content 非数组 → 跳过 usage/stopReason 处理(镜像原实现的 continue):turn 仍存在
367
+ * (由调用方 push),lastError 透传 prevLastError、token 增量为 0。
368
+ */
369
+ function applyAssistantMessage(
370
+ msg: JsonlAssistantMessage,
371
+ turn: Turn,
372
+ pending: PendingToolCall[],
373
+ prevLastError: string | undefined,
374
+ ): { lastError: string | undefined; totalTokensDelta: number } {
375
+ if (!Array.isArray(msg.content)) {
376
+ return { lastError: prevLastError, totalTokensDelta: 0 };
377
+ }
378
+ for (const block of msg.content) {
379
+ if (block.type === "text") {
380
+ turn.text += block.text;
381
+ } else if (block.type === "thinking") {
382
+ turn.thinking += block.thinking;
383
+ } else if (block.type === "toolCall") {
384
+ pending.push({
385
+ toolCallId: block.id,
386
+ toolName: block.name,
387
+ args: block.arguments,
388
+ turn,
389
+ startedTs: msg.timestamp,
390
+ });
391
+ }
392
+ }
393
+
394
+ let totalTokensDelta = 0;
395
+ if (msg.usage) {
396
+ const u = toAgentUsage(msg.usage);
397
+ turn.usageDelta = addUsage(turn.usageDelta, u);
398
+ totalTokensDelta = u.input + u.output + u.cacheRead + u.cacheWrite;
399
+ }
400
+
401
+ let lastError = prevLastError;
402
+ if (msg.stopReason === "error" || msg.stopReason === "aborted") {
403
+ lastError = msg.errorMessage ?? msg.stopReason;
404
+ } else if (msg.stopReason === "stop") {
405
+ lastError = undefined;
406
+ }
407
+ return { lastError, totalTokensDelta };
408
+ }
361
409
 
362
- // ── 重建 turns[] ──
410
+ /** toolResult message 按 toolCallId 配对回填 InternalToolCall;未配对(孤儿)→ 丢弃。 */
411
+ function applyToolResultMessage(msg: JsonlToolResultMessage, pending: PendingToolCall[]): void {
412
+ const idx = pending.findIndex((p) => p.toolCallId === msg.toolCallId);
413
+ if (idx >= 0) {
414
+ const p = pending[idx];
415
+ pending.splice(idx, 1);
416
+ const tc: InternalToolCall = {
417
+ toolName: p.toolName,
418
+ args: p.args,
419
+ result: { content: msg.content, details: msg.details },
420
+ isError: msg.isError ?? false,
421
+ _status: msg.isError ? "failed" : "done",
422
+ startedTs: p.startedTs,
423
+ };
424
+ p.turn.toolCalls.push(tc);
425
+ }
426
+ }
427
+
428
+ /** 顺序遍历 entries 重建 turns[](assistant 开 turn / toolResult 配对回填;user 跳过)。 */
429
+ function rebuildTurns(entries: JsonlEntry[]): RebuiltTurns {
363
430
  const turns: Turn[] = [];
364
431
  const pending: PendingToolCall[] = [];
365
432
  let lastError: string | undefined;
366
433
  let totalTokens = 0;
367
- /** 最后一条 entry 的时间戳(ms),推导 endedAt(避免重建 record 耗时随墙钟无限增长)。 */
368
434
  let lastEntryTsMs: number | undefined;
369
435
 
370
436
  for (const entry of entries) {
@@ -383,81 +449,38 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
383
449
  if (msg.role === "assistant") {
384
450
  const turn = emptyTurn();
385
451
  turns.push(turn);
386
-
387
- if (!Array.isArray(msg.content)) continue;
388
- for (const block of msg.content) {
389
- if (block.type === "text") {
390
- turn.text += block.text;
391
- } else if (block.type === "thinking") {
392
- turn.thinking += block.thinking;
393
- } else if (block.type === "toolCall") {
394
- pending.push({
395
- toolCallId: block.id,
396
- toolName: block.name,
397
- args: block.arguments,
398
- turn,
399
- startedTs: msg.timestamp,
400
- });
401
- }
402
- }
403
-
404
- if (msg.usage) {
405
- const u = toAgentUsage(msg.usage);
406
- turn.usageDelta = addUsage(turn.usageDelta, u);
407
- totalTokens += u.input + u.output + u.cacheRead + u.cacheWrite;
408
- }
409
-
410
- // stopReason 驱动 lastError(与 updateFromEvent 一致):
411
- // error/aborted → 设 lastError;stop(正常结束)→ 清 lastError(镜像 turn_end 的清除语义,
412
- // 即前序 turn 的瞬态 error 在后续成功 turn 后恢复,不误判 success=false)。
413
- // [v4 B-1] status 恒 closed 后 lastStopReason 不再参与推导,死变量已删。
414
- if (msg.stopReason === "error" || msg.stopReason === "aborted") {
415
- lastError = msg.errorMessage ?? msg.stopReason;
416
- } else if (msg.stopReason === "stop") {
417
- lastError = undefined;
418
- }
452
+ const applied = applyAssistantMessage(msg, turn, pending, lastError);
453
+ totalTokens += applied.totalTokensDelta;
454
+ lastError = applied.lastError;
419
455
  } else if (msg.role === "toolResult") {
420
- const idx = pending.findIndex((p) => p.toolCallId === msg.toolCallId);
421
- if (idx >= 0) {
422
- const p = pending[idx];
423
- pending.splice(idx, 1);
424
- const tc: InternalToolCall = {
425
- toolName: p.toolName,
426
- args: p.args,
427
- result: { content: msg.content, details: msg.details },
428
- isError: msg.isError ?? false,
429
- _status: msg.isError ? "failed" : "done",
430
- startedTs: p.startedTs,
431
- };
432
- p.turn.toolCalls.push(tc);
433
- }
434
- // 未配对(孤儿 toolResult)→ 丢弃。
456
+ applyToolResultMessage(msg, pending);
435
457
  }
436
458
  // role:"user" → 跳过(task 来自 identity custom entry)。
437
459
  }
460
+ return { turns, lastError, totalTokens, lastEntryTsMs };
461
+ }
438
462
 
439
- if (turns.length === 0) return undefined; // assistant message → 空壳,降级。
440
-
441
- // 闭合所有 turn(重建的是历史,全是已完成 turn)。
442
- for (const turn of turns) {
443
- turn.closed = true;
444
- }
445
-
446
- const turnCount = turns.length;
447
- const resultText = turns
448
- .map((t) => t.text)
449
- .filter((t) => t.length > 0)
450
- .join("\n\n");
451
-
452
- // eventLog 从 turns[] 派生(与活态 record 的 getEventLog 同形,消费方无感知差异)。
453
- const eventLog = deriveEventLog(turns, lastError, identity.startedAt);
454
-
463
+ /** 组装 ReconstructedRecord(终态投影:status closed/gc + 身份字段归一化 + 派生数据)。 */
464
+ function buildReconstructedRecord(
465
+ sessionFile: string,
466
+ identity: SubagentIdentityData,
467
+ rebuilt: RebuiltTurns,
468
+ eventLog: AgentEventLogEntry[],
469
+ model: string,
470
+ thinkingLevel: string | undefined,
471
+ ): ReconstructedRecord {
455
472
  // 终态 status:最后一条 assistant message 的 stopReason 推导(与 finalizeRecord 的判定一致)。
456
473
  // SP-1:done/failed 合并为 closed,cancelled 由 tombstone override(record-store 层),本函数不感知。
457
474
  const status: ExecutionStatus = "closed";
458
475
  // closedReason 统 gc(通用完成/失败)。error/aborted 的区分由 error 字段保留。
459
476
  const closedReason: import("./types.ts").ClosedReason = "gc";
460
477
 
478
+ const turnCount = rebuilt.turns.length;
479
+ const resultText = rebuilt.turns
480
+ .map((t) => t.text)
481
+ .filter((t) => t.length > 0)
482
+ .join("\n\n");
483
+
461
484
  // rootSessionId 归一化:新文件读 rootSessionId,旧文件 fallback parentSessionId。
462
485
  const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
463
486
  // slug 兜底:旧 identity entry 无 slug 字段 → 空串(与 SubagentRecord.slug: string 契约一致)。
@@ -472,19 +495,74 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
472
495
  sessionFile,
473
496
  status,
474
497
  closedReason,
475
- turns,
498
+ turns: rebuilt.turns,
476
499
  turnCount,
477
- totalTokens,
478
- lastError,
500
+ totalTokens: rebuilt.totalTokens,
501
+ lastError: rebuilt.lastError,
479
502
  model,
480
503
  thinkingLevel,
481
- endedAt: lastEntryTsMs,
504
+ endedAt: rebuilt.lastEntryTsMs,
482
505
  result: resultText.length > 0 ? resultText : undefined,
483
- error: lastError,
506
+ error: rebuilt.lastError,
484
507
  eventLog,
485
508
  };
486
509
  }
487
510
 
511
+ // ============================================================
512
+ // 公开函数
513
+ // ============================================================
514
+
515
+ /**
516
+ * 从 session.jsonl 重建完整 SubagentRecord 数据。
517
+ *
518
+ * ╔══════════════════════════════════════════════════════════════════╗
519
+ * ║ 1. readFileSync(sessionFile) + 逐行 JSON.parse(跳 header) ║
520
+ * ║ (损坏行静默跳过;失败 → undefined) ║
521
+ * ║ 2. 扫 custom entry(customType:"subagent-identity")→ 身份 ║
522
+ * ║ 缺 identity → undefined(无法构造 record) ║
523
+ * ║ 3. 顺序遍历 message entries: ║
524
+ * ║ - role:"assistant" → 开新 Turn ║
525
+ * ║ text/thinking 累积;usage → usageDelta ║
526
+ * ║ toolCall block → 待匹配队列;stopReason error → lastError ║
527
+ * ║ - role:"toolResult" → 配对 toolCallId ║
528
+ * ║ 产 InternalToolCall(done/failed)推进请求 Turn ║
529
+ * ║ 4. 闭合所有 turn;算 turnCount/totalTokens/result/eventLog ║
530
+ * ╚══════════════════════════════════════════════════════════════════╝
531
+ *
532
+ * 返回 undefined:文件缺失/空/损坏/缺 identity entry/无 assistant message。
533
+ * 不抛——消费方(collectRecords)降级跳过该 record。
534
+ *
535
+ * status 由最后一条 assistant message 的 stopReason 推导(error/aborted → failed,
536
+ * 其余 → done)。cancelled 由 tombstone override(record-store 层),本函数不感知。
537
+ *
538
+ * [perf] 本函数是重路径(全文读 + 全 entry 解析)。列表扫描用 readIdentityHeader
539
+ * (只读头部 identity),详情才走本函数(RecordStore.getFullRecord 懒加载)。
540
+ */
541
+ export function reconstructFromFile(sessionFile: string): ReconstructedRecord | undefined {
542
+ // 1. 读文件 + 逐行 JSON.parse(readJsonlEntries:损坏行静默跳过;文件缺失 → undefined)。
543
+ const entries = readJsonlEntries(sessionFile);
544
+ if (!entries || entries.length === 0) return undefined;
545
+
546
+ // 2. 扫身份 custom entry(scanIdentityAndModel:identity 缺失 → 无法构造 record)。
547
+ const { identity, model, thinkingLevel } = scanIdentityAndModel(entries);
548
+ if (!identity) return undefined; // 缺身份 → 无法构造 record。
549
+
550
+ // 3. 重建 turns[](rebuildTurns:assistant 开 turn / toolResult 配对回填)。
551
+ const rebuilt = rebuildTurns(entries);
552
+ if (rebuilt.turns.length === 0) return undefined; // 无 assistant message → 空壳,降级。
553
+
554
+ // 4. 闭合所有 turn(重建的是历史,全是已完成 turn)。
555
+ for (const turn of rebuilt.turns) {
556
+ turn.closed = true;
557
+ }
558
+
559
+ // eventLog 从 turns[] 派生(与活态 record 的 getEventLog 同形,消费方无感知差异)。
560
+ // 必须在闭合 turn 之后派生(deriveEventLog 读 turn.closed)。
561
+ const eventLog = deriveEventLog(rebuilt.turns, rebuilt.lastError, identity.startedAt);
562
+
563
+ return buildReconstructedRecord(sessionFile, identity, rebuilt, eventLog, model, thinkingLevel);
564
+ }
565
+
488
566
  // ============================================================
489
567
  // 轻量头部读取(列表扫描用)
490
568
  // ============================================================
@@ -615,48 +693,62 @@ export function readIdentityTail(sessionFile: string): IdentityHeaderRecon | und
615
693
  return undefined;
616
694
  }
617
695
 
618
- /** 从文本片段解析 identity(头部/全文两入口共享)。解析到 identity 即停(返回);
619
- * 找不到返回 undefined。model/thinking_level 仅在 identity 之前的途经 entry
620
- * best-effort 提取,详情场景由全量重建补齐。 */
621
- function parseIdentityFromText(text: string, sessionFile: string): IdentityHeaderRecon | undefined {
622
- let identity: SubagentIdentityData | undefined;
623
- let model = "";
624
- let thinkingLevel: string | undefined;
625
- for (const line of text.split("\n")) {
626
- const s = line.trim();
627
- if (!s) continue;
628
- // 预筛([S-4] 同款值匹配):只有含三种目标 entry 值串的行才值得 JSON.parse——
629
- // head 64KB 内大 toolResult 行(数十 KB/行)占多数,全量 parse 是冷扫描 CPU 大头。
630
- // 值字符串在任意序列化格式下连续出现,不耦合 pi 的空格习惯。
631
- if (
632
- !s.includes(IDENTITY_CUSTOM_TYPE) &&
633
- !s.includes('"model_change"') &&
634
- !s.includes('"thinking_level_change"')
635
- ) {
636
- continue;
637
- }
638
- let entry: JsonlEntry;
639
- try {
640
- entry = JSON.parse(s) as JsonlEntry;
641
- } catch {
642
- continue; // 损坏行/截断行(头部读取边界)跳过
696
+ // ============================================================
697
+ // 轻量 identity 扫描内部 helper(parseIdentityFromText 按阶段分发)
698
+ // ============================================================
699
+
700
+ /**
701
+ * 预筛([S-4] 同款值匹配):只有含三种目标 entry 值串的行才值得 JSON.parse——
702
+ * head 64KB 内大 toolResult 行(数十 KB/行)占多数,全量 parse 是冷扫描 CPU 大头。
703
+ * 值字符串在任意序列化格式下连续出现,不耦合 pi 的空格习惯。
704
+ */
705
+ function lineMayCarryScannableEntry(s: string): boolean {
706
+ return (
707
+ s.includes(IDENTITY_CUSTOM_TYPE) ||
708
+ s.includes('"model_change"') ||
709
+ s.includes('"thinking_level_change"')
710
+ );
711
+ }
712
+
713
+ /** 轻量扫描的可变累积态(identity + 途经 model/thinkingLevel)。 */
714
+ interface LightIdentityScan {
715
+ identity: SubagentIdentityData | undefined;
716
+ model: string;
717
+ thinkingLevel: string | undefined;
718
+ }
719
+
720
+ /**
721
+ * 单条 entry 并入扫描态。identity 命中(custom + customType 匹配 + data 合法)返回
722
+ * true 供调用方 break(找到即停,后续行不再解析);其余恒 false。三个目标 type 互斥,
723
+ * 与原 if/else-if 链语义一致;custom 但 data 非法 → 不并入、不中断扫描。
724
+ */
725
+ function absorbScannedEntry(state: LightIdentityScan, entry: JsonlEntry): boolean {
726
+ if (entry.type === "custom" && entry.customType === IDENTITY_CUSTOM_TYPE) {
727
+ if (isIdentityData(entry.data)) {
728
+ state.identity = entry.data;
729
+ return true;
643
730
  }
644
- if (entry.type === "custom" && entry.customType === IDENTITY_CUSTOM_TYPE) {
645
- if (isIdentityData(entry.data)) {
646
- identity = entry.data;
647
- break; // 找到即停(后续行不再解析)
648
- }
649
- } else if (entry.type === "model_change") {
650
- if (typeof entry.provider === "string" && typeof entry.modelId === "string") {
651
- model = `${entry.provider}/${entry.modelId}`;
652
- }
653
- } else if (entry.type === "thinking_level_change") {
654
- if (typeof entry.thinkingLevel === "string") thinkingLevel = entry.thinkingLevel;
731
+ return false;
732
+ }
733
+ if (entry.type === "model_change") {
734
+ // model_change: {provider, modelId} → "provider/modelId"(与 record.model 同形)。
735
+ if (typeof entry.provider === "string" && typeof entry.modelId === "string") {
736
+ state.model = `${entry.provider}/${entry.modelId}`;
655
737
  }
738
+ return false;
739
+ }
740
+ if (entry.type === "thinking_level_change") {
741
+ if (typeof entry.thinkingLevel === "string") state.thinkingLevel = entry.thinkingLevel;
656
742
  }
657
- if (!identity) return undefined;
743
+ return false;
744
+ }
658
745
 
659
- // 归一化与全量 recon 同源:rootSessionId fallback 旧字段、slug 兜底空串。
746
+ /** 扫描态 → IdentityHeaderRecon(归一化与全量 recon 同源:rootSessionId fallback 旧字段、slug 兜底空串)。 */
747
+ function toIdentityRecon(
748
+ state: LightIdentityScan,
749
+ identity: SubagentIdentityData,
750
+ sessionFile: string,
751
+ ): IdentityHeaderRecon {
660
752
  const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
661
753
  return {
662
754
  id: identity.id,
@@ -671,8 +763,29 @@ function parseIdentityFromText(text: string, sessionFile: string): IdentityHeade
671
763
  forkDepth: identity.forkDepth,
672
764
  chatMode: identity.chatMode,
673
765
  worktree: identity.worktree,
674
- model,
675
- thinkingLevel,
766
+ model: state.model,
767
+ thinkingLevel: state.thinkingLevel,
676
768
  sessionFile,
677
769
  };
678
770
  }
771
+
772
+ /** 从文本片段解析 identity(头部/全文两入口共享)。解析到 identity 即停(返回);
773
+ * 找不到返回 undefined。model/thinking_level 仅在 identity 之前的途经 entry 中
774
+ * best-effort 提取,详情场景由全量重建补齐。 */
775
+ function parseIdentityFromText(text: string, sessionFile: string): IdentityHeaderRecon | undefined {
776
+ const state: LightIdentityScan = { identity: undefined, model: "", thinkingLevel: undefined };
777
+ for (const line of text.split("\n")) {
778
+ const s = line.trim();
779
+ if (!s) continue;
780
+ if (!lineMayCarryScannableEntry(s)) continue;
781
+ let entry: JsonlEntry;
782
+ try {
783
+ entry = JSON.parse(s) as JsonlEntry;
784
+ } catch {
785
+ continue; // 损坏行/截断行(头部读取边界)跳过
786
+ }
787
+ if (absorbScannedEntry(state, entry)) break; // 找到即停(后续行不再解析)
788
+ }
789
+ if (!state.identity) return undefined;
790
+ return toIdentityRecon(state, state.identity, sessionFile);
791
+ }