@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -21,10 +21,16 @@
21
21
  // D5);同一边沿的多条 pending 合并为一条注入
22
22
  // ③ ack:回执判定成功(主 session 出现 notifyId 匹配的 subagent-bg-notify
23
23
  // custom_message entry)后 appendEntry("subagent-bg-notify-ack")
24
- // ④ replay:重启恢复扫描 ledger/ack 两列 entry 差集重放;重放按 notifyId 幂等去重
24
+ // ④ replay:重启恢复扫描 ledger/ack/abandoned entry 差集重放;重放按 notifyId 幂等去重
25
25
  // (details 携带 notifyId,重复条目可识别)
26
26
  //
27
- // 通道分工(D4):ledger/ack 用 plain appendEntry(type=custom 不进 LLM 上下文——
27
+ // 止损上半场(T4③/PS-6):④只保证「送达保证」下半场,回执确认不可达时(如送达
28
+ // entry 被 compaction 清除)attempts 无上限 = 同一条通知每 120s 重复注入并
29
+ // triggerTurn 无限唤醒 LLM。收敛:重投达 NOTIFY_REDELIVERY_MAX_ATTEMPTS 仍无回执 →
30
+ // 写 abandoned 终态 entry 放弃(warn 含 subagent 标识与 subagents action:"list"
31
+ // 手动核对指引),放弃号跨重启不复活(恢复扫描视同已销账)。
32
+ //
33
+ // 通道分工(D4):ledger/ack/abandoned 用 plain appendEntry(type=custom 不进 LLM 上下文——
28
34
  // session-manager sessionEntryToContextMessages 对 custom 返回 []);送达消息用
29
35
  // pi.sendMessage({triggerTurn:true})(custom_message 进上下文)。两通道不得混用。
30
36
  //
@@ -49,6 +55,11 @@ const logger = getLogger("subagents");
49
55
  export const NOTIFY_LEDGER_CUSTOM_TYPE = "subagent-bg-notify-ledger";
50
56
  /** 销账 entry customType(plain custom entry,不进 LLM 上下文)。 */
51
57
  export const NOTIFY_ACK_CUSTOM_TYPE = "subagent-bg-notify-ack";
58
+ /**
59
+ * [T4③/PS-6] 放弃终态 entry customType(plain custom entry,不进 LLM 上下文)。
60
+ * 与 ack 同形同域:恢复/compaction 扫描把它视同已销账——放弃条目跨重启不复活。
61
+ */
62
+ export const NOTIFY_ABANDONED_CUSTOM_TYPE = "subagent-bg-notify-abandoned";
52
63
 
53
64
  /**
54
65
  * 送达消息的 customType。notifier.ts / bg-notify-render / index.ts(messageRenderer
@@ -65,6 +76,14 @@ export const NOTIFY_CUSTOM_TYPE = "subagent-bg-notify";
65
76
  */
66
77
  export const NOTIFY_WATCHDOG_MS = 120_000;
67
78
 
79
+ /**
80
+ * [T4③/PS-6] 单条通知投递尝试上限(attempts 字段含首次投递):看门狗把 sent 超期
81
+ * 条目转回 pending 前,attempts 已达本上限 → 不再重投,转放弃终态(abandoned entry
82
+ * + warn 恢复指引)。被否语义:「重投无限但幂等」——幂等只防重复入账,不防重复
83
+ * 投递唤醒 LLM(设计 docs/design/subagent-core-unbounded-wait-audit.md §7.2 T4③)。
84
+ */
85
+ export const NOTIFY_REDELIVERY_MAX_ATTEMPTS = 5;
86
+
68
87
  /**
69
88
  * U4 投递计数分桶(设计 §5 U4:分桶口径与 §2.2 三条丢失路径一一对应,回归时定位到
70
89
  * 具体环节):
@@ -103,16 +122,22 @@ export interface NotifyAckEntryData {
103
122
  notifyId: string;
104
123
  }
105
124
 
125
+ /** [T4③/PS-6] abandoned entry 的 data schema(v1,与 ack 同形——终态标记只需身份键)。 */
126
+ export interface NotifyAbandonedEntryData {
127
+ v: 1;
128
+ notifyId: string;
129
+ }
130
+
106
131
  /** ledger 依赖的宿主最小接口(index.ts session_start 装配;解耦便于测试)。 */
107
132
  export interface NotifyLedgerHost {
108
- /** 写 plain custom entry(ledger/ack 通道;pi.appendEntry)。 */
133
+ /** 写 plain custom entry(ledger/ack/abandoned 通道;pi.appendEntry)。 */
109
134
  appendLedgerEntry(customType: string, data: unknown): void;
110
135
  /** 读当前 session 全部 entry(回执扫描 + 恢复扫描;ctx.sessionManager.getEntries())。 */
111
136
  readSessionEntries(): readonly unknown[];
112
137
  /** 主 agent 是否空闲(发送前二次复查用;ctx.isIdle())。 */
113
138
  isIdle(): boolean;
114
139
  /** 订阅 settled 边沿(pi.on("agent_settled")——无退订语义:createExtensionAPI.on
115
- * 只 push 进 extension.handlers、无 off(pi 0.84.1 loader.js:209-214),由 ledger
140
+ * 只 push 进 extension.handlers、无 off(pi 0.84.4 loader.js:209-214),由 ledger
116
141
  * disposed 标志包装)。注意与 ctx.events.on(channel) 消歧:后者走 eventBus、有
117
142
  * trackEventBusSubscription 退订通路(loader.js:174、:338),语义不同。 */
118
143
  onAgentSettled(handler: () => void): void;
@@ -128,7 +153,7 @@ interface NotifyLedgerItem {
128
153
  recordedAt: number;
129
154
  /** 最近一次投递受理时刻;undefined = 尚未投递(pending,等下一边沿)。 */
130
155
  sentAt: number | undefined;
131
- /** 投递尝试次数(看门狗重投计数,诊断用)。 */
156
+ /** 投递尝试次数(含首次;达 NOTIFY_REDELIVERY_MAX_ATTEMPTS 后超期即放弃,诊断用)。 */
132
157
  attempts: number;
133
158
  }
134
159
 
@@ -141,16 +166,24 @@ export interface NotifyLedger {
141
166
  * 同批 pending 合并单条送达(triggerTurn 直达)。 */
142
167
  attemptDeliver(): void;
143
168
  /** ③ 回执销账:扫 session entries,出现 notifyId 匹配的送达 custom_message entry
144
- * → appendEntry(ack) + 摘账(内存态不承担销账职责,权威 = 两列 entry 差集)。 */
169
+ * → appendEntry(ack) + 摘账(内存态不承担销账职责,权威 = 多列 entry 差集)。 */
145
170
  checkReceipts(): void;
146
- /** ④ 重启恢复:扫 ledger/ack 两列 entry 差集,未销账号重新入账并投递(已销账零重发)。
171
+ /** ④ 重启恢复:扫 ledger/ack/abandoned entry 差集,未销账且未放弃号重新入账并投递
172
+ * (已销账零重发;已放弃号不复活——[T4③/PS-6] 止损终态跨重启生效)。
147
173
  * @returns 重放条数。 */
148
174
  recoverFromSession(): number;
149
- /** compaction 降级(P-B4 未验证):检测 ledger/ack entry 被清除 → 按内存态补写。
175
+ /** compaction 降级(P-B4 未验证):检测 ledger/ack/abandoned entry 被清除 → 按内存态补写。
150
176
  * @returns 补写条数。 */
151
177
  compactionCheck(): number;
152
178
  /** 诊断/测试:pending(已记账未投递)条数。 */
153
179
  pendingCount(): number;
180
+ /**
181
+ * [T4④/PS-5] pending 只读快照(notifyId/content/record,副本非活引用):
182
+ * 供 SubagentService.dispose 在「shutdown flush 被 isIdle 门拦」时把未投递
183
+ * pending 复写落盘(同一 ledger entry 通道,notifyId 幂等)供重启 replay。
184
+ * 消费侧配合面仅此只读方法——不改变 attemptDeliver/abandon 既有语义。
185
+ */
186
+ pendingEntries(): ReadonlyArray<{ notifyId: string; content: string; record: object }>;
154
187
  /** 诊断/测试:已投递待回执条数。 */
155
188
  waitingReceiptCount(): number;
156
189
  /** U4 诊断:三桶计数快照(副本;增量同时经 extensionLogger 通道落日志)。 */
@@ -166,13 +199,15 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
166
199
  return typeof v === "object" && v !== null && !Array.isArray(v);
167
200
  }
168
201
 
169
- /** 扫 ledger/ack 两列 plain custom entry(恢复 / compaction 检查共用)。 */
202
+ /** 扫 ledger/ack/abandoned 三列 plain custom entry(恢复 / compaction 检查共用)。 */
170
203
  function scanSessionLedgerEntries(entries: readonly unknown[]): {
171
204
  ledger: Map<string, NotifyLedgerEntryData>;
172
205
  acked: Set<string>;
206
+ abandoned: Set<string>;
173
207
  } {
174
208
  const ledger = new Map<string, NotifyLedgerEntryData>();
175
209
  const acked = new Set<string>();
210
+ const abandoned = new Set<string>();
176
211
  for (const entry of entries) {
177
212
  if (!isPlainObject(entry) || entry["type"] !== "custom") continue;
178
213
  const customType = entry["customType"];
@@ -189,9 +224,11 @@ function scanSessionLedgerEntries(entries: readonly unknown[]): {
189
224
  }
190
225
  } else if (customType === NOTIFY_ACK_CUSTOM_TYPE) {
191
226
  acked.add(notifyId);
227
+ } else if (customType === NOTIFY_ABANDONED_CUSTOM_TYPE) {
228
+ abandoned.add(notifyId);
192
229
  }
193
230
  }
194
- return { ledger, acked };
231
+ return { ledger, acked, abandoned };
195
232
  }
196
233
 
197
234
  /** 收集 wanted 集合中已送达(custom_message entry 出现)的 notifyId。
@@ -232,6 +269,11 @@ export function createNotifyLedger(
232
269
  const items = new Map<string, NotifyLedgerItem>();
233
270
  /** 已销账内存索引(notifyId 幂等判重 + compaction 补写源;权威 = ack entry 列)。 */
234
271
  const ackedIds = new Set<string>();
272
+ /**
273
+ * [T4③/PS-6] 已放弃终态内存索引(幂等判重 + compaction 补写源;权威 = abandoned
274
+ * entry 列)。与 ackedIds 同型:放弃是终态,同 notifyId 绝不再入账/重投/复活。
275
+ */
276
+ const abandonedIds = new Set<string>();
235
277
  /** U4 投递计数三桶(诊断快照源;增量经 emitBucketLog 落 extensionLogger)。 */
236
278
  const buckets: NotifyDeliveryBucketMetrics = {
237
279
  settleRejected: 0,
@@ -244,7 +286,8 @@ export function createNotifyLedger(
244
286
  const api: NotifyLedger = {
245
287
  record(notifyId, content, record): boolean {
246
288
  if (disposed) return false;
247
- if (items.has(notifyId) || ackedIds.has(notifyId)) return false;
289
+ // 幂等去重:在账(pending/sent)/ 已销账 / 已放弃([T4③] 终态绝不重发)→ false
290
+ if (items.has(notifyId) || ackedIds.has(notifyId) || abandonedIds.has(notifyId)) return false;
248
291
  host.appendLedgerEntry(NOTIFY_LEDGER_CUSTOM_TYPE, {
249
292
  v: 1,
250
293
  notifyId,
@@ -297,9 +340,13 @@ export function createNotifyLedger(
297
340
  if (disposed) return 0;
298
341
  const state = scanSessionLedgerEntries(host.readSessionEntries());
299
342
  for (const notifyId of state.acked) ackedIds.add(notifyId);
343
+ // [T4③/PS-6] 放弃终态跨重启生效:abandoned entry 视同已销账,下方差集重放跳过
344
+ // ——放弃号重启后不复活不重投(止损语义不以进程边界失效)。
345
+ for (const notifyId of state.abandoned) abandonedIds.add(notifyId);
300
346
  let replayed = 0;
301
347
  for (const entry of state.ledger.values()) {
302
348
  if (state.acked.has(entry.notifyId)) continue; // 已销账零重发
349
+ if (abandonedIds.has(entry.notifyId)) continue; // 已放弃不复活(止损终态)
303
350
  if (items.has(entry.notifyId) || ackedIds.has(entry.notifyId)) continue; // 幂等
304
351
  items.set(entry.notifyId, {
305
352
  ...entry,
@@ -345,6 +392,14 @@ export function createNotifyLedger(
345
392
  rewritten += 1;
346
393
  }
347
394
  }
395
+ // [T4③/PS-6] 放弃终态同享补写:内存是权威,compaction 清掉 abandoned entry 后
396
+ // 不补写会让放弃号在下次重启按差集复活(重投重启)。
397
+ for (const notifyId of abandonedIds) {
398
+ if (!state.abandoned.has(notifyId)) {
399
+ host.appendLedgerEntry(NOTIFY_ABANDONED_CUSTOM_TYPE, { v: 1, notifyId } satisfies NotifyAbandonedEntryData);
400
+ rewritten += 1;
401
+ }
402
+ }
348
403
  return rewritten;
349
404
  },
350
405
 
@@ -356,6 +411,16 @@ export function createNotifyLedger(
356
411
  return n;
357
412
  },
358
413
 
414
+ pendingEntries(): ReadonlyArray<{ notifyId: string; content: string; record: object }> {
415
+ const out: Array<{ notifyId: string; content: string; record: object }> = [];
416
+ for (const item of items.values()) {
417
+ if (item.sentAt !== undefined) continue;
418
+ // 逐条浅拷贝:消费方(dispose 落盘复写)不得持有内部可变态。
419
+ out.push({ notifyId: item.notifyId, content: item.content, record: { ...item.record } });
420
+ }
421
+ return out;
422
+ },
423
+
359
424
  waitingReceiptCount(): number {
360
425
  let n = 0;
361
426
  for (const item of items.values()) {
@@ -408,6 +473,35 @@ export function createNotifyLedger(
408
473
  };
409
474
  }
410
475
 
476
+ /** [T4③/PS-6] 放弃终态:appendEntry(abandoned) 落盘终态标记(重启恢复不复活)+
477
+ * warn 恢复指引。放弃后账面摘除(pending/waiting 计数归零、看门狗可停),同
478
+ * notifyId 被 record 幂等拒绝——「确认不可达」的止损上半场;通知内容本身仍可
479
+ * 经 subagents action:"list" 手动核对(账本 entry 与 result 落盘不受影响)。
480
+ * 放弃不计入 watchdogReplays 桶(该桶口径 = 实际发生重投的条数)。 */
481
+ function abandonItem(item: NotifyLedgerItem): void {
482
+ host.appendLedgerEntry(NOTIFY_ABANDONED_CUSTOM_TYPE, { v: 1, notifyId: item.notifyId } satisfies NotifyAbandonedEntryData);
483
+ items.delete(item.notifyId);
484
+ abandonedIds.add(item.notifyId);
485
+ // 消息含 subagent 标识与恢复指引(S-E 验收面);notifyId/attempts 等动态值按
486
+ // D4 约定放 data 参数(msg 近固定 key,限流命中面)。
487
+ logger.warn(
488
+ `Subagent "${itemLabel(item)}" notification abandoned - no receipt after ` +
489
+ `${NOTIFY_REDELIVERY_MAX_ATTEMPTS} delivery attempts; verify manually via subagents action:"list"`,
490
+ { notifyId: item.notifyId, attempts: item.attempts },
491
+ );
492
+ maybeStopWatchdog();
493
+ }
494
+
495
+ /** 放弃 warn 的 subagent 标识:优先 record.agent(人可读);record 形态异常时
496
+ * (ledger 对 record 不透明,见 NotifyLedgerEntryData)退回 notifyId(仍可检索)。 */
497
+ function itemLabel(item: NotifyLedgerItem): string {
498
+ if (isPlainObject(item.record)) {
499
+ const agent = item.record["agent"];
500
+ if (typeof agent === "string" && agent.length > 0) return agent;
501
+ }
502
+ return item.notifyId;
503
+ }
504
+
411
505
  function ensureWatchdog(): void {
412
506
  if (watchdogTimer !== undefined || disposed) return;
413
507
  // 看门狗(D5 ②兜底触发面):回执检查 + 超时重投 + pending 补投。
@@ -418,12 +512,19 @@ export function createNotifyLedger(
418
512
  // attemptDeliver 即重投——正常时序 settled 边沿早已销账,只有消息真丢失才到这
419
513
  const cutoff = Date.now() - NOTIFY_WATCHDOG_MS;
420
514
  let timedOut = 0;
515
+ // [T4③/PS-6] 止损:attempts 已达上限仍超期 = 确认不可达 → 放弃转终态,
516
+ // 不再重投(同一条完成通知不无限重复注入唤醒 LLM)。
517
+ const givenUp: NotifyLedgerItem[] = [];
421
518
  for (const item of items.values()) {
422
- if (item.sentAt !== undefined && item.sentAt <= cutoff) {
423
- item.sentAt = undefined;
424
- timedOut += 1;
519
+ if (item.sentAt === undefined || item.sentAt > cutoff) continue;
520
+ if (item.attempts >= NOTIFY_REDELIVERY_MAX_ATTEMPTS) {
521
+ givenUp.push(item);
522
+ continue;
425
523
  }
524
+ item.sentAt = undefined;
525
+ timedOut += 1;
426
526
  }
527
+ for (const item of givenUp) abandonItem(item);
427
528
  if (timedOut > 0) {
428
529
  // U4 ①watchdogReplays 桶:busy 窗口滞留兜底的重投条数(同一消息反复超时累计)
429
530
  buckets.watchdogReplays += timedOut;
@@ -467,7 +568,7 @@ export function createNotifyLedger(
467
568
  // (P-B0 相邻机制已源码级锚定,agent-session.js:327-331,PS-07)。
468
569
  // [MF-5] registerSettledListener=false(bind 路径)时跳过注册:pi.on("agent_settled")
469
570
  // 无退订语义——createExtensionAPI.on 只 push 进 extension.handlers、无 off
470
- //(pi 0.84.1 dist/core/extensions/loader.js:209-214)。注意消歧两类订阅面:此处的
571
+ //(pi 0.84.4 dist/core/extensions/loader.js:209-214)。注意消歧两类订阅面:此处的
471
572
  // pi.on 是生命周期事件订阅(无退订);ctx.events.on(channel) 走 eventBus、有
472
573
  // trackEventBusSubscription 退订通路(loader.js:174、:338)——勿按后者的可退订
473
574
  // 语义「修复」此处。per-bind 注册会随 session 切换累积死 handler
@@ -543,7 +644,7 @@ function settledEdgeDispatch(): void {
543
644
  /**
544
645
  * session_start 装配(index.ts):构造新 ledger 并绑定为模块级单例(notifier.notify
545
646
  * 经 getBoundNotifyLedger 消费)。重复 bind(/resume /fork /new 的 session_start)
546
- * 替换旧实例——内存态清零符合「内存不承担销账职责」,权威 = entry 两列差集
647
+ * 替换旧实例——内存态清零符合「内存不承担销账职责」,权威 = entry 多列差集
547
648
  * (调用方随后 recoverFromSession 重建)。
548
649
  *
549
650
  * [MF-5] 监听单例化:首次 bind 经当次 host 注册模块级单例 handler
@@ -21,7 +21,9 @@ import type {
21
21
  } from "./types.ts";
22
22
 
23
23
  /** 自描述 record entry 的 customType。写点字面量与本常量的等值由
24
- * __tests__/record-store.test.ts 断言钉住(消费方引用本常量,勿用裸字符串)。 */
24
+ * __tests__/record-store.test.ts 断言钉住(消费方引用本常量,勿用裸字符串)。
25
+ *
26
+ * @experimental execution 运行时面(U10① D6):一个 minor 周期内允许签名微调。 */
25
27
  export const SUBAGENT_RECORD_CUSTOM_TYPE = "subagent-record";
26
28
 
27
29
  /**
@@ -34,6 +36,8 @@ export const SUBAGENT_RECORD_CUSTOM_TYPE = "subagent-record";
34
36
  * - worktreeHandle:不可 JSON 序列化的运行时句柄(布尔投影 worktree 保留)。
35
37
  *
36
38
  * undefined 字段经 JSON.stringify 自然缺省(与 SubagentRecord 重建侧语义一致)。
39
+ *
40
+ * @experimental execution 运行时面(U10① D6):一个 minor 周期内允许签名微调。
37
41
  */
38
42
  export interface SubagentRecordEntryData {
39
43
  /** schema 版本(W16 起 v1)。消费方按 v 判别解析,不认识的版本跳过而非猜测。 */
@@ -95,7 +99,9 @@ export interface SubagentRecordEntryData {
95
99
  engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
96
100
  }
97
101
 
98
- /** SubagentRecord → 自描述 entry data(快照投影,不 mutate 源)。 */
102
+ /** SubagentRecord → 自描述 entry data(快照投影,不 mutate 源)。
103
+ *
104
+ * @experimental execution 运行时面(U10① D6):一个 minor 周期内允许签名微调。 */
99
105
  export function toSubagentRecordEntry(record: SubagentRecord): SubagentRecordEntryData {
100
106
  return {
101
107
  v: 1,
@@ -352,6 +352,12 @@ export type RecordStorePi = {
352
352
  * 读时从 sessions/*.jsonl 重建([perf] light 头部扫描 + per-file 缓存)。
353
353
  *
354
354
  * 任何 mutate → notifyChange()(仅通知监听器;磁盘缓存靠 stat 戳自校验,不清空)。
355
+ *
356
+ * record 状态查询面(U10① D6):按状态枚举 listRunning/collectRecords(statusFilter)、
357
+ * 按 id 查询 getMutable/findLightById/getFullRecord——方法签名即导出形态,本类零改动。
358
+ *
359
+ * @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
360
+ * 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
355
361
  */
356
362
  export class RecordStore {
357
363
  private readonly records = new Map<string, ExecutionRecord>();
@@ -698,11 +704,11 @@ export class RecordStore {
698
704
  ...(chatMode
699
705
  ? { resumable: true }
700
706
  : {
701
- status: "closed" as const,
702
- closedReason: "gc" as const,
703
- endedAt: Date.now(),
704
- error: "orphan recovery: no child session file (spawn interrupted or file removed externally)",
705
- }),
707
+ status: "closed" as const,
708
+ closedReason: "gc" as const,
709
+ endedAt: Date.now(),
710
+ error: "orphan recovery: no child session file (spawn interrupted or file removed externally)",
711
+ }),
706
712
  });
707
713
  }
708
714
 
@@ -740,9 +746,19 @@ export class RecordStore {
740
746
  this.indexHigherVersion = false;
741
747
  }
742
748
 
743
- /** /resume /fork /new 后复活(dispose 的逆操作)。 */
749
+ /**
750
+ * /resume /fork /new 后复活(dispose 的逆操作)。
751
+ *
752
+ * [PS-10/T6④] 同步复位 orphanJudged 防重缓存:resumable 形态(IO-error 保守分支 /
753
+ * chatMode 分流)没有 .finalized sidecar 锚,重判资格完全由本缓存承载——dispose 时
754
+ * 有 clear(session 结束),但 revive 此前不复位,导致「同进程内曾经的 IO 失败记录
755
+ * 永久停留 resumable」,与本文件 recoverOrphanRecords 注释承诺的「IO 恢复后重开可重判」
756
+ * 不符。/new 复活正是「重开」语义:IO 已恢复的记录下次 recoverOrphanRecords 重新判定
757
+ * 收敛终态;仍不可读的记录重判再落一次 resumable entry(幂等,末条语义不变)。
758
+ */
744
759
  revive(): void {
745
760
  this._disposed = false;
761
+ this.orphanJudged.clear();
746
762
  }
747
763
 
748
764
  // ── 内部 ──────────────────────────────────────────────────
@@ -971,7 +987,10 @@ export class RecordStore {
971
987
  })
972
988
  .catch((err: unknown) => {
973
989
  this.indexDirty = true; // 失败恢复 dirty,下轮过窗重试
974
- logger.debug("[subagents] sessions-index write failed", {
990
+ // [PS-14/T7③] warn:索引反复写失败(权限/磁盘满)曾仅 debug 级,排障时
991
+ // 无线索。失败会跨轮重试(dirty 恢复),warn 每次过窗写失败都会出现——
992
+ // 正是「反复写失败需要可见」的信号面。
993
+ logger.warn("[subagents] sessions-index write failed", {
975
994
  detail: { dir: encDir, error: err instanceof Error ? err.message : String(err) },
976
995
  });
977
996
  });
@@ -1171,14 +1190,9 @@ export class RecordStore {
1171
1190
  return rec;
1172
1191
  }
1173
1192
 
1174
- /** 从缓存与索引移除某文件(文件删除时;负缓存条目无 id,仅删缓存项)。 */
1175
- private dropFileCache(file: string): void {
1176
- const entry = this.fileCache.get(file);
1177
- if (entry) {
1178
- if (!entry.negative) this.idToFile.delete(entry.light.id);
1179
- this.fileCache.delete(file);
1180
- }
1181
- }
1193
+ // [PS-15/T7⑤] 「从缓存与索引移除单文件」的旧私有方法已整体删除:全仓无调用方
1194
+ // 的死代码(设计 §4.3 PS-15 实锤,顺手清理,无行为影响)。「文件删除时移除缓存」
1195
+ // 的职责实际由 reconstructAll 的消失文件修剪路径承担。
1182
1196
 
1183
1197
  /** 排序比较器:status priority(running<failed<cancelled<done)+ startedAt desc。 */
1184
1198
  private static compareRecords(a: SubagentRecord, b: SubagentRecord): number {