@bolloon/bolloon-agent 0.4.33 → 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 (214) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +580 -40
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/model-policy.js +651 -0
  31. package/dist/agents/network-pulse.js +304 -18
  32. package/dist/agents/pi-sdk-tools.js +84 -33
  33. package/dist/agents/pi-sdk.js +157 -42
  34. package/dist/agents/run-store.js +71 -5
  35. package/dist/agents/skill-readiness.js +52 -10
  36. package/dist/agents/subagent-manager.js +209 -3
  37. package/dist/agents/task/task-runner.js +17 -4
  38. package/dist/agents/task-group.js +8 -51
  39. package/dist/agents/task-public-text.js +89 -0
  40. package/dist/agents/workflow-pivot-loop.js +19 -12
  41. package/dist/cli/clipboard.js +65 -0
  42. package/dist/cli/commands/index.js +1 -0
  43. package/dist/cli/commands/tasks.js +350 -2
  44. package/dist/cli/identity-command.js +177 -0
  45. package/dist/cli/ink-app.js +145 -17
  46. package/dist/cli/log-gate.js +386 -0
  47. package/dist/cli/mention-data.js +2 -0
  48. package/dist/cli/model-selector.js +538 -0
  49. package/dist/cli/reply-hygiene.js +54 -0
  50. package/dist/cli/setup-wizard.js +757 -69
  51. package/dist/cli/startup-notice.js +160 -0
  52. package/dist/cli/stores.js +22 -1
  53. package/dist/cli/theme.js +49 -0
  54. package/dist/cli/tui-select.js +970 -0
  55. package/dist/cli/update-commands.js +150 -24
  56. package/dist/cli-entry.js +137 -20
  57. package/dist/electron-build/agents/contacts/chain.js +26 -6
  58. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  59. package/dist/electron-build/agents/execution-supervisor.js +578 -38
  60. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  61. package/dist/electron-build/agents/external-events.js +34 -5
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/goal-criteria.js +10 -3
  64. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  65. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  66. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  67. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  68. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  69. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  70. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  71. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  72. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  73. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  74. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  75. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  76. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  86. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  87. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  88. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  89. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  90. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  91. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  92. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  93. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  94. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  95. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  96. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  97. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  98. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  99. package/dist/electron-build/agents/goal-store.js +35 -1
  100. package/dist/electron-build/agents/goal-store.js.map +1 -1
  101. package/dist/electron-build/agents/model-policy.js +706 -0
  102. package/dist/electron-build/agents/model-policy.js.map +1 -0
  103. package/dist/electron-build/agents/network-pulse.js +307 -19
  104. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  105. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  106. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  107. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  108. package/dist/electron-build/agents/pi-sdk.js +156 -41
  109. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  110. package/dist/electron-build/agents/run-store.js +75 -6
  111. package/dist/electron-build/agents/run-store.js.map +1 -1
  112. package/dist/electron-build/agents/skill-readiness.js +52 -9
  113. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  114. package/dist/electron-build/agents/task/task-runner.js +16 -3
  115. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  116. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  117. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  118. package/dist/electron-build/llm/config-store.js +146 -6
  119. package/dist/electron-build/llm/config-store.js.map +1 -1
  120. package/dist/electron-build/llm/connection-probe.js +764 -0
  121. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  122. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  123. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  124. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  125. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  126. package/dist/electron-build/llm/model-catalog.js +778 -0
  127. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  128. package/dist/electron-build/llm/model-discovery.js +1202 -0
  129. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  130. package/dist/electron-build/llm/model-selection.js +1315 -0
  131. package/dist/electron-build/llm/model-selection.js.map +1 -0
  132. package/dist/electron-build/llm/pi-ai.js +70 -11
  133. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  134. package/dist/electron-build/llm/provider-catalog.js +969 -0
  135. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  136. package/dist/electron-build/llm/provider-registry.js +950 -0
  137. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  138. package/dist/electron-build/llm/tool-name.js +266 -0
  139. package/dist/electron-build/llm/tool-name.js.map +1 -0
  140. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  141. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  142. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  143. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  144. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  145. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  146. package/dist/electron-build/utils/dual-source.js +347 -0
  147. package/dist/electron-build/utils/dual-source.js.map +1 -0
  148. package/dist/electron-build/utils/update-manager.js +328 -104
  149. package/dist/electron-build/utils/update-manager.js.map +1 -1
  150. package/dist/electron-build/utils/update-state.js +9 -11
  151. package/dist/electron-build/utils/update-state.js.map +1 -1
  152. package/dist/electron-build/utils/version-identity.js +193 -0
  153. package/dist/electron-build/utils/version-identity.js.map +1 -0
  154. package/dist/electron-build/utils/version-info.js +57 -31
  155. package/dist/electron-build/utils/version-info.js.map +1 -1
  156. package/dist/index.js +385 -108
  157. package/dist/ios/bolloon-web.json +10 -0
  158. package/dist/ios/chain-explorer.js +617 -0
  159. package/dist/ios/explorer.html +163 -0
  160. package/dist/ios/index.html +96 -0
  161. package/dist/ios/mobile-agent.js +2 -2
  162. package/dist/ios/mobile-contacts.js +362 -0
  163. package/dist/ios/mobile-core.js +3102 -310
  164. package/dist/ios/mobile-privacy.js +185 -0
  165. package/dist/ios/mobile-tasks.js +339 -0
  166. package/dist/ios/mobile-update.js +1442 -0
  167. package/dist/ios/mobile.css +19 -0
  168. package/dist/ios/mobile.html +96 -0
  169. package/dist/ios/mobile.js +1013 -41
  170. package/dist/ios/routes-chain-index.js +108 -0
  171. package/dist/ios/routes-contacts.js +420 -0
  172. package/dist/ios/routes-llm-config.js +344 -52
  173. package/dist/ios/routes-mobile-tasks.js +353 -0
  174. package/dist/ios/routes-onchain-trade.js +99 -0
  175. package/dist/ios/routes-x402-info.js +1 -0
  176. package/dist/ios/server.js +921 -0
  177. package/dist/ios/sw.js +1 -1
  178. package/dist/ios/task-frame-server.js +211 -0
  179. package/dist/llm/config-store.js +146 -6
  180. package/dist/llm/connection-probe.js +744 -0
  181. package/dist/llm/custom-provider-store.js +224 -0
  182. package/dist/llm/data/provider-catalog-baked.js +19 -0
  183. package/dist/llm/model-catalog.js +717 -0
  184. package/dist/llm/model-discovery.js +1137 -0
  185. package/dist/llm/model-selection.js +1241 -0
  186. package/dist/llm/pi-ai.js +70 -11
  187. package/dist/llm/provider-catalog.js +898 -0
  188. package/dist/llm/provider-registry.js +917 -0
  189. package/dist/llm/tool-name.js +220 -0
  190. package/dist/orbitdb/cid-database.js +81 -16
  191. package/dist/orbitdb/ipfs-node.js +31 -2
  192. package/dist/setup/onboard.js +34 -8
  193. package/dist/utils/dual-source-facts.js +223 -0
  194. package/dist/utils/dual-source.js +297 -0
  195. package/dist/utils/update-cli.js +16 -21
  196. package/dist/utils/update-health.js +35 -5
  197. package/dist/utils/update-manager.js +325 -96
  198. package/dist/utils/update-state.js +13 -10
  199. package/dist/utils/version-identity.js +176 -0
  200. package/dist/utils/version-info.js +27 -22
  201. package/dist/web/bolloon-web.json +10 -0
  202. package/dist/web/mobile-contacts.js +44 -11
  203. package/dist/web/mobile-core.js +2901 -580
  204. package/dist/web/mobile-tasks.js +339 -0
  205. package/dist/web/mobile-update.js +1442 -0
  206. package/dist/web/mobile.css +19 -0
  207. package/dist/web/mobile.html +64 -0
  208. package/dist/web/mobile.js +838 -35
  209. package/dist/web/routes-llm-config.js +344 -52
  210. package/dist/web/routes-mobile-tasks.js +353 -0
  211. package/dist/web/server.js +163 -4
  212. package/dist/web/sw.js +1 -1
  213. package/package.json +3 -1
  214. package/scripts/build-web.ts +36 -0
@@ -0,0 +1,488 @@
1
+ /**
2
+ * goal-flywheel/work-monitor.ts — P3 阻塞监控 (WorkMonitor, 2026-09-25)
3
+ *
4
+ * 存在的理由: 现有 Watchdog **只看进程存活**, 不等于看**任务是否卡住**。
5
+ * 本层只回答三件事:
6
+ * ① 现在卡在哪 → `detectBlocks` → `BlockRecord[]`
7
+ * ② 该怎么处理 → `planBlockHandling` → `BlockResolutionAction`
8
+ * ③ 用户该看到什么 → `toUserVisibleState` → 五类之一
9
+ *
10
+ * 纪律 (与设计稿 `docs/wiki/goal-continuation-flywheel.md` §8 / §13 / §14 对齐):
11
+ * - **纯函数**: 时间一律 `now` 注入 (不读真实钟) · 不做 I/O (不 import fs) · 不 import 其它阶段的实现文件,
12
+ * 与 P0/P1/P2/P4 只通过 `types.ts` 的类型耦合 (依赖靠参数注入)。
13
+ * - **不自动绕过 Harness**: 工具/权限/预算被阻只产出「父改计划 / 转人工」, 永不产出 `takeover`/`replace_child`。
14
+ * - **用户视野只有五类**: `UserVisibleState` 是闭集; 内部词 (lease · reducer · retry counter · worker owner)
15
+ * 既不进返回值, 也不进 `BlockRecord.note`。
16
+ * - **只给动作不做动作**: 接管 / 替换 / 上报这些**副作用由接线层执行** —— 本文件不动任何状态。
17
+ * - **宁可漏报, 不假报**: 时间戳读不出 / 输入不足以证明时**不**判定阻塞 (判不出来就说判不出来)。
18
+ *
19
+ * 刻意**不做** (如实声明的边界):
20
+ * - `maxSteps` / `maxAmount` 的用量不在本函数输入里 (没有用量计数器) → 只能由子自报 `budget_blocked` 透传;
21
+ * 只有**时长型**硬底线 (`budget.maxDurationMs`, 以 `contract.issuedAt` 起算) 父侧能独立算出。
22
+ * - 单次 `report.status === 'failed'` **不**判 `repeated_failure` —— "反复"需要跨轮历史, 本函数只有单次快照;
23
+ * 该 kind 只经子自报透传。
24
+ * - 逐判据 checks / 越权工具 / 私改判据这些**报告合规判定**属 P2 `validateChildReport`; 本层只守一条硬底线:
25
+ * **无证据或缺必备证据的报告, 绝不接受为完成**。
26
+ */
27
+ import { BLOCK_RESOLUTION_ACTIONS } from './types.js';
28
+ // ============================================================================
29
+ // 阈值 —— 全部导出: 测试与接线层都不许自己编魔法数
30
+ // ============================================================================
31
+ /** 心跳超过 `heartbeatIntervalMs × 本倍率` 未到 → `no_heartbeat` */
32
+ export const HEARTBEAT_MISS_MULTIPLE = 2;
33
+ /** 无进展窗口 = `heartbeatIntervalMs × 本倍率` */
34
+ export const NO_PROGRESS_HEARTBEAT_MULTIPLE = 6;
35
+ /** 合同不要求心跳 (`heartbeatIntervalMs ≤ 0`) 时的无进展窗口 */
36
+ export const NO_PROGRESS_FALLBACK_MS = 30 * 60 * 1000;
37
+ /** 无进展已超过 `窗口 × 本倍率` = 调整指令已给过一轮 → 停 / 替换 */
38
+ export const ADJUSTMENT_ALREADY_GIVEN_MULTIPLE = 2;
39
+ /** 回报宽限 = `heartbeatIntervalMs × 本倍率` */
40
+ export const REPORT_GRACE_HEARTBEAT_MULTIPLE = 2;
41
+ /** 没有心跳周期可依据时的回报宽限 */
42
+ export const DEFAULT_REPORT_GRACE_MS = 60 * 1000;
43
+ /** 默认升级时限 (design: 不许无限等) */
44
+ export const DEFAULT_ESCALATION_MS = 10 * 60 * 1000;
45
+ /** 只有这几个目标状态是「已结束」 */
46
+ export const TERMINAL_GOAL_STATES = ['completed', 'failed', 'abandoned'];
47
+ /**
48
+ * Goal **自己的** status 里属于"已结束"的那些。
49
+ * 与 `TERMINAL_GOAL_STATES` 同名同义 (同一批值), 这里是给"手上只有字符串"的调用方用的窄门:
50
+ * 视图层 (`toUserVisibleState`) 不该为了比一个 `goalStatus` 字符串去依赖 `GoalLifecycleState` 类型。
51
+ *
52
+ * 为什么在 continuation 之外还要看它: continuation 是**派生**记录 (收尾写下的那一份),
53
+ * 而 `goal.status` 是**Goal 本体**的事实。任何一次派生记录的落后 (旧版本写下的、崩溃在两步之间、
54
+ * 别的写入者覆盖了它) 都不该让界面把**已经结束**的目标说成"正在执行" —— 界面只能比系统更保守,
55
+ * 不许比系统更乐观。M5-⑥ 真跑就是这么读到的: `goal.status=completed` 而 `goalVisibleState=executing`。
56
+ */
57
+ export const TERMINAL_GOAL_STATUSES = [...TERMINAL_GOAL_STATES];
58
+ /** Goal status 是否属于终态 (字符串口径, 与 TERMINAL_GOAL_STATES 同一批值) */
59
+ export function isTerminalGoalStatus(s) {
60
+ return TERMINAL_GOAL_STATUSES.includes(String(s ?? ''));
61
+ }
62
+ // ============================================================================
63
+ // 规则表 (按 kind 的默认处理) —— 上下文可细化, 但绝不越过 Harness
64
+ // ============================================================================
65
+ /**
66
+ * 每种阻塞的**默认** owner / 动作。上下文细化 (lease 空闲可接管 · 无进展二档换人 · 报告超时转人工)
67
+ * 由 `detectBlocks` 覆盖; `planBlockHandling` 在记录缺建议动作时回落到这张表。
68
+ *
69
+ * 三条硬规则 (与 §8 的「处理规则」表逐条对应):
70
+ * - 子无心跳 → 先查执行权: 空闲且合同允许才能 `takeover`, 否则 `escalate_parent`
71
+ * - 子无进展 → **先发一次调整指令** (`send_adjustment`), 仍无进展才 `replace_child`
72
+ * - 报告不完整 → **不接受为完成** → `request_report`, 超时 `needs_human`
73
+ * - 工具/资源被阻 → `change_plan` / `needs_human`, **永不** 接管/替换/绕过 Harness
74
+ */
75
+ export const BLOCK_HANDLING_DEFAULTS = {
76
+ no_heartbeat: { owner: 'parent', action: 'escalate_parent' },
77
+ waiting_dependency: { owner: 'external', action: 'wait_dependency' },
78
+ repeated_failure: { owner: 'parent', action: 'escalate_parent' },
79
+ no_progress: { owner: 'child', action: 'send_adjustment' },
80
+ tool_blocked: { owner: 'parent', action: 'change_plan' },
81
+ budget_blocked: { owner: 'user', action: 'needs_human' },
82
+ permission_blocked: { owner: 'user', action: 'needs_human' },
83
+ runner_unavailable: { owner: 'parent', action: 'escalate_parent' },
84
+ external_timeout: { owner: 'parent', action: 'escalate_parent' },
85
+ report_missing: { owner: 'child', action: 'request_report' },
86
+ };
87
+ /**
88
+ * 每种阻塞映射到的**用户可见**状态。
89
+ * 语义: 子/执行者侧卡死 → `child_blocked`; 没有新东西 → `no_progress`; 在等别人 → `waiting_external_reply`。
90
+ * 注意 `report_missing` 属「子侧卡住」(子该报没报), 不是"等外部"。
91
+ */
92
+ export const USER_VISIBLE_STATE_FOR_BLOCK_KIND = {
93
+ no_heartbeat: 'child_blocked',
94
+ waiting_dependency: 'waiting_external_reply',
95
+ repeated_failure: 'child_blocked',
96
+ no_progress: 'no_progress',
97
+ tool_blocked: 'child_blocked',
98
+ budget_blocked: 'child_blocked',
99
+ permission_blocked: 'child_blocked',
100
+ runner_unavailable: 'child_blocked',
101
+ external_timeout: 'waiting_external_reply',
102
+ report_missing: 'child_blocked',
103
+ };
104
+ /** 未解决阻塞的「信息量」顺序: 子被阻塞 > 无进展 > 等外部 (用户态按这个顺序取第一个命中的) */
105
+ export const USER_VISIBLE_BLOCK_PRIORITY = [
106
+ 'child_blocked',
107
+ 'no_progress',
108
+ 'waiting_external_reply',
109
+ ];
110
+ // ============================================================================
111
+ // 时间与阈值工具
112
+ // ============================================================================
113
+ /** 解析时间戳; 读不出 → null (调用方据此"不判定", 而不是当成 0) */
114
+ function toMs(iso) {
115
+ const ms = Date.parse(iso);
116
+ return Number.isFinite(ms) ? ms : null;
117
+ }
118
+ function toIso(ms) {
119
+ return new Date(ms).toISOString();
120
+ }
121
+ /** 心跳窗口 (null = 该合同不要求心跳) */
122
+ export function heartbeatWindowMs(contract) {
123
+ const hb = contract.heartbeatIntervalMs;
124
+ return Number.isFinite(hb) && hb > 0 ? hb * HEARTBEAT_MISS_MULTIPLE : null;
125
+ }
126
+ /** 无进展窗口 */
127
+ export function noProgressWindowMs(contract) {
128
+ const hb = contract.heartbeatIntervalMs;
129
+ return Number.isFinite(hb) && hb > 0 ? hb * NO_PROGRESS_HEARTBEAT_MULTIPLE : NO_PROGRESS_FALLBACK_MS;
130
+ }
131
+ /** 回报宽限 (deadline 过后多久内还只是"要求补齐") */
132
+ export function reportGraceMs(contract) {
133
+ const hb = contract.heartbeatIntervalMs;
134
+ return Number.isFinite(hb) && hb > 0 ? hb * REPORT_GRACE_HEARTBEAT_MULTIPLE : DEFAULT_REPORT_GRACE_MS;
135
+ }
136
+ /**
137
+ * 把草稿变成冻结的 `BlockRecord`。
138
+ *
139
+ * `blockId` 刻意**确定性** (`blk:<workId>:<kind>`): 同一 (工作, 阻塞类型) 反复检测得到同一个 id,
140
+ * 接线层据此幂等去重, 而不是制造一堆"新阻塞"。
141
+ */
142
+ function materialize(contract, draft) {
143
+ return {
144
+ blockId: `blk:${contract.workId}:${draft.kind}`,
145
+ kind: draft.kind,
146
+ goalId: contract.goalId,
147
+ runId: contract.parentRunId,
148
+ workId: contract.workId,
149
+ childAgentId: contract.childAgentId,
150
+ blockedAt: toIso(draft.blockedAtMs),
151
+ lastProgressAt: toIso(draft.lastProgressMs ?? draft.blockedAtMs),
152
+ owner: draft.owner,
153
+ dependency: draft.dependency,
154
+ suggestedAction: draft.action,
155
+ escalationAt: draft.escalationMs === null ? null : toIso(draft.escalationMs),
156
+ resolvedAt: null,
157
+ resolution: null,
158
+ note: draft.note,
159
+ };
160
+ }
161
+ /**
162
+ * 报告的**不完整项** (空数组 = 报告完整)。
163
+ *
164
+ * 只做「能不能接受为完成」的硬底线, 不做 P2 的完整合规判定:
165
+ * ① 报告张冠李戴 (workId 与合同不符)
166
+ * ② `evidence` 为空 —— 漂亮但无证据
167
+ * ③ `contract.requiredEvidence` 没被证据的 `ref` / `kind` **精确**覆盖
168
+ * ④ 自称 `blocked` 却没带 `blockReason`
169
+ */
170
+ export function missingReportParts(contract, report) {
171
+ const missing = [];
172
+ if (report.workId !== contract.workId) {
173
+ missing.push(`报告 workId (${report.workId}) 与合同 (${contract.workId}) 不符`);
174
+ }
175
+ if (report.evidence.length === 0) {
176
+ missing.push('evidence 为空 —— 漂亮但无证据的报告不接受为完成');
177
+ }
178
+ const covered = new Set();
179
+ for (const e of report.evidence) {
180
+ covered.add(e.ref);
181
+ covered.add(e.kind);
182
+ }
183
+ const uncovered = contract.requiredEvidence.filter((r) => !covered.has(r));
184
+ if (uncovered.length > 0) {
185
+ missing.push(`必备证据未覆盖: ${uncovered.join(', ')}`);
186
+ }
187
+ if (report.status === 'blocked' && report.blockReason === null) {
188
+ missing.push('status=blocked 但 blockReason 为空');
189
+ }
190
+ return missing;
191
+ }
192
+ /**
193
+ * 一次检测, 产出全部阻塞 (无阻塞 = 空数组)。
194
+ *
195
+ * 判定顺序固定 (便于对表与复现): 执行器不可用 → 时长硬底线 → 无心跳 → 无进展 → 报告(缺失/不完整) → 子自报透传。
196
+ * 同一 `kind` 只留**第一条** (父侧观测优先于子自报)。
197
+ */
198
+ export function detectBlocks(input) {
199
+ const { contract, report, lastHeartbeatAt, lastProgressAt, now, leaseOwner, runnerAvailable } = input;
200
+ const nowMs = toMs(now);
201
+ // `now` 读不出 → 无法证明任何阻塞: 宁可漏报, 不假报
202
+ if (nowMs === null)
203
+ return [];
204
+ const progressMs = toMs(lastProgressAt);
205
+ const issuedMs = toMs(contract.issuedAt);
206
+ const out = [];
207
+ const seen = new Set();
208
+ const push = (draft) => {
209
+ if (seen.has(draft.kind))
210
+ return;
211
+ seen.add(draft.kind);
212
+ out.push(materialize(contract, draft));
213
+ };
214
+ // ---- 1. 执行器不可用 (只诊断, 不假装跑过) --------------------------------
215
+ if (runnerAvailable === false) {
216
+ push({
217
+ kind: 'runner_unavailable',
218
+ owner: 'parent',
219
+ action: 'escalate_parent',
220
+ blockedAtMs: nowMs,
221
+ lastProgressMs: progressMs,
222
+ dependency: `runner:${contract.capability}`,
223
+ escalationMs: nowMs + DEFAULT_ESCALATION_MS,
224
+ note: `没有可用的执行器来跑能力「${contract.capability}」—— 只诊断, 不假装跑过; 需要父换执行器或改计划`,
225
+ });
226
+ }
227
+ // ---- 2. 时长型硬底线 (父侧可独立算出; 用量型只能靠子自报) ----------------
228
+ const maxDurationMs = contract.budget.maxDurationMs;
229
+ if (maxDurationMs !== null && Number.isFinite(maxDurationMs) && maxDurationMs >= 0 && issuedMs !== null) {
230
+ const deadlineBudgetMs = issuedMs + maxDurationMs;
231
+ if (nowMs > deadlineBudgetMs) {
232
+ const hasReport = report !== null;
233
+ push({
234
+ kind: 'budget_blocked',
235
+ owner: hasReport ? 'user' : 'child',
236
+ action: hasReport ? 'needs_human' : 'request_report',
237
+ blockedAtMs: deadlineBudgetMs,
238
+ lastProgressMs: progressMs,
239
+ dependency: null,
240
+ escalationMs: deadlineBudgetMs + DEFAULT_ESCALATION_MS,
241
+ note: hasReport
242
+ ? `本轮时长预算已用尽 (${maxDurationMs}ms) —— 加预算不许 Agent 自动批, 需人决定`
243
+ : `本轮时长预算已用尽 (${maxDurationMs}ms) 且子还没回报 —— 先要一份收尾报告`,
244
+ });
245
+ }
246
+ }
247
+ // ---- 3. 子无心跳 → 先查执行权: 可接管则接管 / 不可则上报父 ---------------
248
+ const hbWindow = heartbeatWindowMs(contract);
249
+ if (hbWindow !== null) {
250
+ const baseMs = lastHeartbeatAt === null ? issuedMs : toMs(lastHeartbeatAt);
251
+ if (baseMs !== null && nowMs - baseMs > hbWindow) {
252
+ const overdueAtMs = baseMs + hbWindow;
253
+ // 执行权空闲 **且** 合同明确允许接管, 才判定可接管;
254
+ // 是否"持有者已死 / 执行权过期可回收"属执行权层, 本层不猜。
255
+ const canTakeover = contract.failurePolicy.onHeartbeatMiss === 'takeover' && leaseOwner === null;
256
+ push({
257
+ kind: 'no_heartbeat',
258
+ owner: 'parent',
259
+ action: canTakeover ? 'takeover' : 'escalate_parent',
260
+ blockedAtMs: overdueAtMs,
261
+ lastProgressMs: progressMs,
262
+ dependency: leaseOwner === null ? null : '互斥执行权正在被占用',
263
+ escalationMs: overdueAtMs + hbWindow,
264
+ note: canTakeover
265
+ ? `子 Agent 心跳超时 (合同间隔 ${contract.heartbeatIntervalMs}ms) 且执行权空闲 → 父可接管`
266
+ : `子 Agent 心跳超时 (合同间隔 ${contract.heartbeatIntervalMs}ms) 但不可抢占 → 上报父处理`,
267
+ });
268
+ }
269
+ }
270
+ // ---- 4. 子无进展 (活着但没动) → 先发一次调整指令, 二档才换人 -------------
271
+ const npWindow = noProgressWindowMs(contract);
272
+ if (progressMs !== null && nowMs - progressMs > npWindow) {
273
+ const overdueAtMs = progressMs + npWindow;
274
+ const adjusted = nowMs - progressMs > npWindow * ADJUSTMENT_ALREADY_GIVEN_MULTIPLE;
275
+ push({
276
+ kind: 'no_progress',
277
+ owner: adjusted ? 'parent' : 'child',
278
+ action: adjusted ? 'replace_child' : 'send_adjustment',
279
+ blockedAtMs: overdueAtMs,
280
+ lastProgressMs: progressMs,
281
+ dependency: null,
282
+ escalationMs: overdueAtMs + npWindow,
283
+ note: adjusted
284
+ ? `已无进展 ${nowMs - progressMs}ms (窗口 ${npWindow}ms) —— 调整指令已给过一轮, 仍无进展 → 停或替换执行者`
285
+ : `已无进展 ${nowMs - progressMs}ms (窗口 ${npWindow}ms) —— 先发一次调整指令, 不是立刻换人`,
286
+ });
287
+ }
288
+ // ---- 5. 报告: 根本没回 / 不完整 → 不接受为完成 ---------------------------
289
+ if (report === null) {
290
+ const dlMs = contract.deadline === null ? null : toMs(contract.deadline);
291
+ if (dlMs !== null && nowMs > dlMs) {
292
+ const overdueMs = nowMs - dlMs;
293
+ const grace = reportGraceMs(contract);
294
+ if (overdueMs > grace) {
295
+ push({
296
+ kind: 'report_missing',
297
+ owner: 'user',
298
+ action: 'needs_human',
299
+ blockedAtMs: dlMs,
300
+ lastProgressMs: progressMs,
301
+ dependency: `report:${contract.reportSchema}`,
302
+ escalationMs: dlMs + grace,
303
+ note: `子已超期 ${overdueMs}ms (宽限 ${grace}ms) 仍未补齐报告 → 转人工`,
304
+ });
305
+ }
306
+ else {
307
+ push({
308
+ kind: 'report_missing',
309
+ owner: 'child',
310
+ action: 'request_report',
311
+ blockedAtMs: dlMs,
312
+ lastProgressMs: progressMs,
313
+ dependency: `report:${contract.reportSchema}`,
314
+ escalationMs: dlMs + grace,
315
+ note: `子到期未回报 (超期 ${overdueMs}ms, 宽限 ${grace}ms) → 要求补齐, 不接受为完成`,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ else {
321
+ const missingParts = missingReportParts(contract, report);
322
+ if (missingParts.length > 0) {
323
+ const reportedMs = toMs(report.reportedAt);
324
+ const blockedAtMs = reportedMs ?? nowMs;
325
+ const grace = reportGraceMs(contract);
326
+ push({
327
+ kind: 'report_missing',
328
+ owner: 'child',
329
+ action: 'request_report',
330
+ blockedAtMs,
331
+ lastProgressMs: progressMs,
332
+ dependency: `report:${contract.reportSchema}`,
333
+ escalationMs: blockedAtMs + grace,
334
+ note: `报告不完整 (${missingParts.join('; ')}) → 不接受为完成, 要求补充; ${grace}ms 内不补则转人工`,
335
+ });
336
+ }
337
+ // ---- 6. 子自报阻塞: 透传 kind, 但 owner/动作按**父的**规则表定 ----------
338
+ const childBlock = report.blockReason;
339
+ if (childBlock !== null) {
340
+ const childBlockedMs = toMs(childBlock.blockedAt) ?? nowMs;
341
+ const childProgressMs = toMs(childBlock.lastProgressAt);
342
+ const childEscalationMs = toIsoMsOrNull(childBlock.escalationAt) ?? childBlockedMs + DEFAULT_ESCALATION_MS;
343
+ const defaults = BLOCK_HANDLING_DEFAULTS[childBlock.kind];
344
+ push({
345
+ kind: childBlock.kind,
346
+ // 子自报的 owner/建议只当线索: 由父的规则表定 owner 与动作 (子不许私改处置)
347
+ owner: defaults.owner,
348
+ action: defaults.action,
349
+ blockedAtMs: childBlockedMs,
350
+ lastProgressMs: childProgressMs,
351
+ dependency: childBlock.dependency,
352
+ escalationMs: childEscalationMs,
353
+ note: `子 Agent 自报阻塞「${childBlock.kind}」${childBlock.note === '' ? ' (子未写说明)' : ` (子自报原文: ${childBlock.note})`}`,
354
+ });
355
+ }
356
+ }
357
+ return out;
358
+ }
359
+ function toIsoMsOrNull(iso) {
360
+ return iso === null ? null : toMs(iso);
361
+ }
362
+ // ============================================================================
363
+ // ② planBlockHandling
364
+ // ============================================================================
365
+ function isKnownAction(value) {
366
+ return BLOCK_RESOLUTION_ACTIONS.includes(value);
367
+ }
368
+ /**
369
+ * 该拿这条阻塞怎么办 (只给动作, 不动状态; 副作用由接线层执行)。
370
+ *
371
+ * 优先级:
372
+ * ① 已处理的记录 → 以**实际**动作为准 (记录既是事实也是结论)
373
+ * ② 记录自带的 `suggestedAction` (detectBlocks 已按执行权/二段/超时上下文细化过) → 采用
374
+ * ③ 外部构造 (磁盘/JSON) 的记录可能带着非法动作 → 回落按 kind 的默认处理表
375
+ *
376
+ * 硬约束: 工具/权限/预算被阻**永不**返回 `takeover` / `replace_child` (不自动绕过 Harness)。
377
+ */
378
+ export function planBlockHandling(b) {
379
+ if (b.resolvedAt !== null && b.resolution !== null)
380
+ return b.resolution;
381
+ if (isKnownAction(b.suggestedAction))
382
+ return b.suggestedAction;
383
+ return BLOCK_HANDLING_DEFAULTS[b.kind].action;
384
+ }
385
+ // ============================================================================
386
+ // ③ toUserVisibleState
387
+ // ============================================================================
388
+ function isTerminalGoalState(state) {
389
+ return TERMINAL_GOAL_STATES.includes(state);
390
+ }
391
+ /**
392
+ * 盘上的 continuation → 判定层能安全读的记录 (**唯一**归一化点, 2026-09-25 跨阶段修复)。
393
+ *
394
+ * 为什么要有它 (真问题, 不是防御性编程):
395
+ * `goal-store.setContinuation()` 是**部分覆盖** —— 一次只写几个字段 (例如唤醒时只写
396
+ * `{wakeReason:'active', wakeAt:undefined}`), 所以 `goal.continuation` 上
397
+ * `pendingReports` / `unresolvedItems` 这类**列表字段在运行期确实可能缺席** (类型上也是 optional)。
398
+ * 而调用方直传 `{...goal.continuation}` 是**合法**的写法 (`goalVisibleState` / `/api/goals` 都在用),
399
+ * 于是判定层里那句 `c.pendingReports.length` 会直接抛 TypeError —— 表现为"界面拿不到状态",
400
+ * 而不是"少了一个字段"。
401
+ *
402
+ * 两条纪律:
403
+ * · **缺的按"没有"补** (空数组), 与 `goal-store.continuationView` 同一口径 —— 不是编一个状态,
404
+ * 只是把"没记过"读成"没有";
405
+ * · **不发明字段值**: `state` 缺就保持缺席, 由判定层当"没有终态声明"读 (绝不默认成某个终态/进行态)。
406
+ * `isTerminalGoalState(undefined)` 天然为 false, 所以缺席不会被读成 `ended` 或 `executing`。
407
+ */
408
+ export function normalizeContinuationRecord(c) {
409
+ if (!c || typeof c !== 'object')
410
+ return null;
411
+ return {
412
+ ...c,
413
+ pendingReports: Array.isArray(c.pendingReports) ? [...c.pendingReports] : [],
414
+ unresolvedItems: Array.isArray(c.unresolvedItems) ? [...c.unresolvedItems] : [],
415
+ };
416
+ }
417
+ /**
418
+ * 内部状态 → 用户可见**六类** (其余内部状态一律不外露: lease · reducer · internal status · retry counter · worker owner)。
419
+ *
420
+ * 优先级 (确定性, 不随 blocks 数组顺序变化):
421
+ * ① 未解决阻塞的处置就是 `needs_human` (或决策/继续记录显式要人) → `needs_your_decision`
422
+ * ② 目标**终态** → `ended` (2026-09-25 补的第 6 类):
423
+ * 此前五类里没有"已结束", 只能借用 `needs_your_decision`, 且**绝不能**返回 `executing` ——
424
+ * 把一个已经结束的目标显示成"正在执行", 正是 P3 要防的假象 ("看着在跑其实没在跑")。
425
+ * 汇报层仍应用 `conclusion` 表达"已完成/已失败", 不得把本返回值当"还在跑"的依据。
426
+ * ③ 未解决阻塞 → 子被阻塞 > 无进展 > 等外部
427
+ * ④ 决策/继续记录描述的等待或无进展
428
+ * ⑤ 还有子任务没回报 → 等回复
429
+ * ⑥ 默认 `executing` (没有已知阻塞/等待/无进展)
430
+ */
431
+ export function toUserVisibleState(raw, blocks, decision,
432
+ /**
433
+ * ★ M5-⑥ (2026-09-25): Goal **自己的** status (本体事实, 权威)。
434
+ * 给了它就把"已结束"钉死: 终态目标在任何情况下都不许投影成"正在执行" ——
435
+ * 哪怕 continuation 落后/缺失 (`goalVisibleState` / `/api/goals` 都能拿到这个字段, 就该传)。
436
+ * 不传 = 老行为 (只看 continuation + decision), 不影响既有调用方的口径。
437
+ */
438
+ goalStatus) {
439
+ // 归一化在这里做 (唯一入口) —— 见 normalizeContinuationRecord 的注释:
440
+ // 盘上的 continuation 允许缺字段, 判定函数不该把"少一个字段"变成抛错。
441
+ const c = normalizeContinuationRecord(raw);
442
+ const unresolved = blocks.filter((b) => b.resolvedAt === null);
443
+ // ① 已结束 (Goal 本体说结束了, 就是结束了)
444
+ // 放在最前: 终态是**最高优先**的事实 —— 派生记录 (continuation) 或历史阻塞清单都不许
445
+ // 把它改写成"正在执行"/"需要你决定"。第 6 类 `ended` 的存在意义就是这个。
446
+ if (isTerminalGoalStatus(goalStatus))
447
+ return 'ended';
448
+ // ② 需要人
449
+ if (unresolved.some((b) => b.suggestedAction === 'needs_human'))
450
+ return 'needs_your_decision';
451
+ if (decision !== null &&
452
+ (decision.decision === 'ask_human' || decision.decision === 'pause' || decision.state === 'needs_decision')) {
453
+ return 'needs_your_decision';
454
+ }
455
+ if (c !== null && (c.state === 'needs_human' || c.state === 'paused'))
456
+ return 'needs_your_decision';
457
+ // ② 终态 (第 6 类 `ended`: 已结束绝不说成"正在执行", 也不再冒充"需要你决定")
458
+ if (c !== null && isTerminalGoalState(c.state))
459
+ return 'ended';
460
+ if (decision !== null && (decision.state === 'completed' || decision.state === 'failed')) {
461
+ return 'ended';
462
+ }
463
+ // ③ 未解决阻塞 (子被阻塞 > 无进展 > 等外部)
464
+ for (const state of USER_VISIBLE_BLOCK_PRIORITY) {
465
+ if (unresolved.some((b) => USER_VISIBLE_STATE_FOR_BLOCK_KIND[b.kind] === state))
466
+ return state;
467
+ }
468
+ // ④ 决策描述的状态
469
+ if (decision !== null) {
470
+ if (decision.state === 'no_progress')
471
+ return 'no_progress';
472
+ if (decision.state === 'waiting_external' || decision.state === 'waiting_agent') {
473
+ return 'waiting_external_reply';
474
+ }
475
+ }
476
+ // ⑤ 继续记录描述的等待 / 无进展
477
+ if (c !== null) {
478
+ if (c.state === 'stalled')
479
+ return 'no_progress';
480
+ if (c.state === 'awaiting_external' || c.state === 'retry_wait')
481
+ return 'waiting_external_reply';
482
+ }
483
+ // ⑥ 还有子任务没回报 → 等回复 (比"正在执行"更具体)
484
+ if (c !== null && c.pendingReports.length > 0)
485
+ return 'waiting_external_reply';
486
+ // ⑦ 默认: 没有已知阻塞/等待/无进展
487
+ return 'executing';
488
+ }