@bolloon/bolloon-agent 0.5.4 → 0.6.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 (222) hide show
  1. package/dist/agents/agent-identity-store.js +41 -5
  2. package/dist/agents/background-notices.js +124 -0
  3. package/dist/agents/channel-identity.js +54 -0
  4. package/dist/agents/code-write-gate.js +47 -0
  5. package/dist/agents/deny-pipeline.js +11 -2
  6. package/dist/agents/egress-sanitize.js +69 -0
  7. package/dist/agents/error-classifier.js +67 -5
  8. package/dist/agents/experience-review.js +226 -0
  9. package/dist/agents/gateway-group.js +153 -18
  10. package/dist/agents/iteration-budget.js +64 -0
  11. package/dist/agents/lesson-to-skill.js +211 -0
  12. package/dist/agents/managed-services.js +64 -0
  13. package/dist/agents/parse-tool-call.js +43 -32
  14. package/dist/agents/pi-sdk-session-factory.js +36 -3
  15. package/dist/agents/pi-sdk-session-manager.js +72 -3
  16. package/dist/agents/pi-sdk-tools.js +201 -32
  17. package/dist/agents/pi-sdk.js +798 -206
  18. package/dist/agents/plan-store.js +36 -0
  19. package/dist/agents/process-runner.js +64 -2
  20. package/dist/agents/run-context.js +20 -0
  21. package/dist/agents/runner-resolver.js +2 -1
  22. package/dist/agents/shell-guard.js +109 -13
  23. package/dist/agents/skill-health.js +90 -0
  24. package/dist/agents/skill-ledger.js +129 -0
  25. package/dist/agents/tool-loop-guard.js +303 -0
  26. package/dist/agents/tool-path-args.js +53 -0
  27. package/dist/agents/tool-result-gate.js +50 -0
  28. package/dist/agents/tool-subset.js +109 -0
  29. package/dist/agents/tool-telemetry.js +97 -0
  30. package/dist/bootstrap/context-collector.js +37 -7
  31. package/dist/bootstrap/lifecycle-hooks.js +12 -3
  32. package/dist/bootstrap/persona-init.js +174 -0
  33. package/dist/bootstrap/persona-loader.js +5 -1
  34. package/dist/cli/ink-app.js +153 -6
  35. package/dist/cli/input-paste.js +218 -0
  36. package/dist/cli/loading-tui.js +3 -1
  37. package/dist/cli/log-gate.js +7 -0
  38. package/dist/cli/mention-data.js +30 -0
  39. package/dist/cli/reasoning-view.js +103 -0
  40. package/dist/cli/startup-notice.js +25 -0
  41. package/dist/cli/status-segments.js +87 -2
  42. package/dist/cli/trace-line.js +54 -0
  43. package/dist/context-compaction/auto-compact.js +37 -2
  44. package/dist/electron-build/agents/agent-identity-store.js +43 -5
  45. package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
  46. package/dist/electron-build/agents/background-notices.js +138 -0
  47. package/dist/electron-build/agents/background-notices.js.map +1 -0
  48. package/dist/electron-build/agents/channel-identity.js +64 -0
  49. package/dist/electron-build/agents/channel-identity.js.map +1 -0
  50. package/dist/electron-build/agents/code-write-gate.js +55 -0
  51. package/dist/electron-build/agents/code-write-gate.js.map +1 -0
  52. package/dist/electron-build/agents/deny-pipeline.js +11 -2
  53. package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
  54. package/dist/electron-build/agents/egress-sanitize.js +76 -0
  55. package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
  56. package/dist/electron-build/agents/error-classifier.js +68 -5
  57. package/dist/electron-build/agents/error-classifier.js.map +1 -1
  58. package/dist/electron-build/agents/experience-review.js +243 -0
  59. package/dist/electron-build/agents/experience-review.js.map +1 -0
  60. package/dist/electron-build/agents/iteration-budget.js +73 -0
  61. package/dist/electron-build/agents/iteration-budget.js.map +1 -0
  62. package/dist/electron-build/agents/lesson-to-skill.js +228 -0
  63. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
  64. package/dist/electron-build/agents/managed-services.js +69 -0
  65. package/dist/electron-build/agents/managed-services.js.map +1 -0
  66. package/dist/electron-build/agents/parse-tool-call.js +43 -32
  67. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  68. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  69. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
  71. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  72. package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
  73. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  74. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  75. package/dist/electron-build/agents/pi-sdk.js +799 -206
  76. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  77. package/dist/electron-build/agents/plan-store.js +37 -0
  78. package/dist/electron-build/agents/plan-store.js.map +1 -1
  79. package/dist/electron-build/agents/process-runner.js +71 -2
  80. package/dist/electron-build/agents/process-runner.js.map +1 -1
  81. package/dist/electron-build/agents/run-context.js +24 -0
  82. package/dist/electron-build/agents/run-context.js.map +1 -0
  83. package/dist/electron-build/agents/runner-resolver.js +2 -1
  84. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  85. package/dist/electron-build/agents/shell-guard.js +110 -14
  86. package/dist/electron-build/agents/shell-guard.js.map +1 -1
  87. package/dist/electron-build/agents/skill-health.js +103 -0
  88. package/dist/electron-build/agents/skill-health.js.map +1 -0
  89. package/dist/electron-build/agents/skill-ledger.js +146 -0
  90. package/dist/electron-build/agents/skill-ledger.js.map +1 -0
  91. package/dist/electron-build/agents/tool-loop-guard.js +319 -0
  92. package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
  93. package/dist/electron-build/agents/tool-path-args.js +63 -0
  94. package/dist/electron-build/agents/tool-path-args.js.map +1 -0
  95. package/dist/electron-build/agents/tool-result-gate.js +59 -0
  96. package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
  97. package/dist/electron-build/agents/tool-subset.js +116 -0
  98. package/dist/electron-build/agents/tool-subset.js.map +1 -0
  99. package/dist/electron-build/agents/tool-telemetry.js +108 -0
  100. package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
  101. package/dist/electron-build/bootstrap/context-collector.js +38 -7
  102. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  103. package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
  104. package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
  105. package/dist/electron-build/bootstrap/persona-init.js +216 -0
  106. package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
  107. package/dist/electron-build/bootstrap/persona-loader.js +5 -1
  108. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  109. package/dist/electron-build/context-compaction/auto-compact.js +38 -2
  110. package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
  111. package/dist/electron-build/external-engines/delegate.js +22 -0
  112. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  113. package/dist/electron-build/kernel/channel-actor.js +314 -0
  114. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  115. package/dist/electron-build/orbitdb/cid-database.js +55 -0
  116. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  117. package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
  118. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  119. package/dist/electron-build/web/server-storage.js +26 -5
  120. package/dist/electron-build/web/server-storage.js.map +1 -1
  121. package/dist/external-engines/delegate.js +21 -0
  122. package/dist/index.js +526 -95
  123. package/dist/kernel/channel-actor.js +297 -0
  124. package/dist/kernel/control.js +95 -0
  125. package/dist/kernel/gate-scan.js +1036 -0
  126. package/dist/kernel/model-runtime.js +472 -0
  127. package/dist/kernel/plan-channel-actor.js +298 -0
  128. package/dist/kernel/plan-constraint.js +165 -0
  129. package/dist/kernel/plan-deletion.js +32 -0
  130. package/dist/kernel/plan-modelruntime.js +60 -0
  131. package/dist/kernel/plan-runcontext.js +163 -0
  132. package/dist/kernel/plan.js +153 -0
  133. package/dist/kernel/roster.js +242 -0
  134. package/dist/orbitdb/cid-database.js +55 -0
  135. package/dist/orbitdb/event-index.js +370 -0
  136. package/dist/orbitdb/event-query.js +131 -0
  137. package/dist/orbitdb/event-shell.js +510 -0
  138. package/dist/orbitdb/group-access.js +381 -0
  139. package/dist/orbitdb/group-shards.js +140 -0
  140. package/dist/orbitdb/ipfs-node.js +36 -11
  141. package/dist/web/bolloon-web.json +3 -3
  142. package/dist/web/client.js +4 -2
  143. package/dist/web/mobile-core.js +7762 -6289
  144. package/dist/web/routes-mobile-tasks.js +2 -1
  145. package/dist/web/routes-tasks.js +5 -1
  146. package/dist/web/server-storage.js +26 -5
  147. package/dist/web/server.js +60 -18
  148. package/dist/web/sw.js +1 -1
  149. package/dist/web/ui/message-renderer.js +473 -29
  150. package/dist/web/ui/step-timeline.js +13 -12
  151. package/package.json +2 -1
  152. package/scripts/build-web.ts +74 -1
  153. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  154. package/dist/constraint-runtime/_archive_helper.js +0 -9
  155. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  156. package/dist/constraint-runtime/assistant/index.js +0 -12
  157. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  158. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  159. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  160. package/dist/constraint-runtime/bridge/index.js +0 -12
  161. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  162. package/dist/constraint-runtime/buddy/index.js +0 -12
  163. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  164. package/dist/constraint-runtime/cli/index.js +0 -12
  165. package/dist/constraint-runtime/components/index.d.ts +0 -11
  166. package/dist/constraint-runtime/components/index.js +0 -12
  167. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  168. package/dist/constraint-runtime/constants/index.js +0 -12
  169. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  170. package/dist/constraint-runtime/coordinator/index.js +0 -12
  171. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  172. package/dist/constraint-runtime/cost_hook.js +0 -4
  173. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  174. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  175. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  176. package/dist/constraint-runtime/execution_registry.js +0 -44
  177. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  178. package/dist/constraint-runtime/hooks/index.js +0 -12
  179. package/dist/constraint-runtime/ink.d.ts +0 -1
  180. package/dist/constraint-runtime/ink.js +0 -4
  181. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  182. package/dist/constraint-runtime/keybindings/index.js +0 -12
  183. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  184. package/dist/constraint-runtime/memdir/index.js +0 -12
  185. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  186. package/dist/constraint-runtime/migrations/index.js +0 -12
  187. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  188. package/dist/constraint-runtime/moreright/index.js +0 -12
  189. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  190. package/dist/constraint-runtime/native_ts/index.js +0 -12
  191. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  192. package/dist/constraint-runtime/output_styles/index.js +0 -12
  193. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  194. package/dist/constraint-runtime/plugins/index.js +0 -12
  195. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  196. package/dist/constraint-runtime/port_manifest.js +0 -11
  197. package/dist/constraint-runtime/query.d.ts +0 -6
  198. package/dist/constraint-runtime/query.js +0 -1
  199. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  200. package/dist/constraint-runtime/remote/index.js +0 -12
  201. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  202. package/dist/constraint-runtime/schemas/index.js +0 -12
  203. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  204. package/dist/constraint-runtime/screens/index.js +0 -12
  205. package/dist/constraint-runtime/server/index.d.ts +0 -11
  206. package/dist/constraint-runtime/server/index.js +0 -12
  207. package/dist/constraint-runtime/services/index.d.ts +0 -11
  208. package/dist/constraint-runtime/services/index.js +0 -12
  209. package/dist/constraint-runtime/state/index.d.ts +0 -11
  210. package/dist/constraint-runtime/state/index.js +0 -12
  211. package/dist/constraint-runtime/system_init.d.ts +0 -1
  212. package/dist/constraint-runtime/system_init.js +0 -20
  213. package/dist/constraint-runtime/types/index.d.ts +0 -11
  214. package/dist/constraint-runtime/types/index.js +0 -12
  215. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  216. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  217. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  218. package/dist/constraint-runtime/utils/index.js +0 -12
  219. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  220. package/dist/constraint-runtime/vim/index.js +0 -12
  221. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  222. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -44,12 +44,32 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.PiAgentSession = exports.runSelfImproveLoop = exports.resetAgentSession = exports.getAgentSession = exports.createAgentSession = exports.PiSessionManager = exports.TOOL_DEFINITIONS = void 0;
47
+ exports.PiAgentSession = exports.PROACTIVE_WORK_DISCIPLINE = exports.runSelfImproveLoop = exports.resetAgentSession = exports.getAgentSession = exports.createAgentSession = exports.PiSessionManager = exports.TOOL_DEFINITIONS = void 0;
48
48
  exports.decideUnreported = decideUnreported;
49
49
  exports.writeBackCurrentTurnInto = writeBackCurrentTurnInto;
50
+ exports.verifyWriteOutcome = verifyWriteOutcome;
51
+ const fs = __importStar(require("fs/promises"));
50
52
  const fsSync = __importStar(require("fs"));
51
53
  const os = __importStar(require("os"));
52
54
  const path = __importStar(require("path"));
55
+ const run_context_js_1 = require("./run-context.js");
56
+ const channel_actor_js_1 = require("../kernel/channel-actor.js");
57
+ const tool_path_args_js_1 = require("./tool-path-args.js");
58
+ const background_notices_js_1 = require("./background-notices.js");
59
+ const skill_ledger_js_1 = require("./skill-ledger.js");
60
+ const node_crypto_1 = require("node:crypto");
61
+ const experience_review_js_1 = require("./experience-review.js");
62
+ const lesson_to_skill_js_1 = require("./lesson-to-skill.js");
63
+ const managed_services_js_1 = require("./managed-services.js");
64
+ const iteration_budget_js_1 = require("./iteration-budget.js");
65
+ const tool_telemetry_js_1 = require("./tool-telemetry.js");
66
+ const tool_result_gate_js_1 = require("./tool-result-gate.js");
67
+ const tool_subset_js_1 = require("./tool-subset.js");
68
+ const code_write_gate_js_1 = require("./code-write-gate.js");
69
+ // 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
70
+ // require 是 undefined ⇒ 抛错被 catch 静默吞掉 ⇒ "自愈"根本没跑, DID 一直是空的 ✗。
71
+ const agent_identity_js_1 = require("./agent-identity.js");
72
+ const channel_identity_js_1 = require("./channel-identity.js");
53
73
  const context_manager_js_1 = require("../bootstrap/context-manager.js");
54
74
  const module_1 = require("module");
55
75
  const version_info_js_1 = require("../utils/version-info.js");
@@ -174,6 +194,62 @@ function writeBackCurrentTurnInto(history, wire) {
174
194
  return 0; // 写回失败绝不影响对话本身
175
195
  }
176
196
  }
197
+ const tool_loop_guard_js_1 = require("./tool-loop-guard.js");
198
+ /**
199
+ * 写操作"读回自证" (2026-10-01 优化 #4): 写类工具成功后**自动**核一次, 把事实拼进结果。
200
+ * 为什么: 过程纪律写了"读回一次", 但**没人执行** ✗ ⇒ 现在由机制执行: "工具说成功 ≠ 任务成功"。
201
+ * 只做**便宜**的核对(存在性/大小); 失败静默(核对不该把工具搞失败)。
202
+ */
203
+ function verifyWriteOutcome(toolName, args, cwd) {
204
+ try {
205
+ const fsMod = require('node:fs');
206
+ const pathMod = require('node:path');
207
+ const WRITE = new Set(['write_file', 'edit_file', 'mkdir', 'move_file', 'copy_file']);
208
+ if (!WRITE.has(String(toolName)))
209
+ return null;
210
+ const rel = String(args?.path ?? args?.to ?? args?.dir ?? '').trim();
211
+ if (!rel)
212
+ return null;
213
+ const abs = pathMod.isAbsolute(rel) ? rel : pathMod.resolve(cwd, rel);
214
+ const st = fsMod.statSync(abs);
215
+ if (st.isDirectory())
216
+ return `[已核对] 目录存在: ${rel}`;
217
+ return `[已核对] 文件已落盘: ${rel} (${st.size} 字节, ${new Date(st.mtimeMs).toISOString()})`;
218
+ }
219
+ catch (e) {
220
+ return `[未核对] 读回失败: ${String(e?.message || e).slice(0, 80)} —— 别急着说"已完成", 先确认路径/权限`;
221
+ }
222
+ }
223
+ /**
224
+ * 过程纪律 (2026-10-01, 用户: 「智能体回复方式没有主动性 … 在过程里面更加主动考虑」)。
225
+ *
226
+ * 诊断: 系统提示里只有"理解→分析→调用→观察"这种**反应式**循环描述 ⇒ 模型容易"问一句答一句、
227
+ * 试一次就收尾、把方案当交付"。主动不是靠一句"请主动"能给的, 要落成**可判定的规矩**。
228
+ * 落地方式 (7 条, 都是"做/不做"能验收的): 先动手别先问 · 做到底别停在半成品 · 一个工具不够就换法继续 ·
229
+ * 工具说成功 ≠ 任务成功(读回验证) · 被阻塞如实说绝不编 · 每轮要么推进要么交付 · 顺手想影响面并给下一步建议。
230
+ */
231
+ exports.PROACTIVE_WORK_DISCIPLINE = `
232
+ 过程纪律 (比"答得漂亮"更重要, 逐条可判定):
233
+ 0. **先动手, 别先问**: 请求有显而易见的默认解释时, 按它做, 不要为了确认而停下 —— 只有"不同解释会让我做不同的事"时才问。
234
+ 0.5 **别把"主动"用在闲聊上**: 寒暄 / 感受 / 纯闲聊这类**没有任务**的消息 ⇒ 直接回答, **不要**为此调工具
235
+ (用户实测: 一句「你好」去跑了 git_status 花 4.1s ✗ —— 那是浪费)。"主动"= 接到任务后**想在你前面**,
236
+ 不是"每句话都先把环境查一遍" ✗。
237
+ 1. **做到底**: 交付的是**能跑的东西 + 真实的工具输出**, 不是"我打算怎么做"。只写方案、只搭一半、只报告打算做什么, 都算没做完。
238
+ 2. **一个工具不够就换法继续**: 同一件事试到确实不通为止 —— 换参数/换命令/换路径/换工具接着试, 不要试一次就收尾。
239
+ 3. **工具说成功 ≠ 任务成功**: 写入/生成/发布这类动作, **读回一次**再声称完成(内容/存在/大小/哈希都可)。
240
+ 4. **被阻塞就如实说**: 讲清卡在哪一步、为什么、还缺什么; **绝不**用编造的结果顶替(编一个"看起来对"的输出比说"没做成"更糟)。
241
+ 5. **每一轮要么用工具推进, 要么给出结论**: 不要把"下一步我打算…"当成回答。
242
+ 6. **收尾时说三句**: 改了什么 · 验过什么(拿什么读到的) · 还剩什么。不重述过程。
243
+ 7.4 **技能是"要用时先读再照做"**: 库里有一批已装载技能(名字+说明), 用 list_skills <关键词> 找,
244
+ 用 read_skill <名字> 读全文 —— **相关时先读它再动手**, 别凭印象做(技能里往往写着踩过的坑与判据 ✓)。
245
+ 7.5 **一轮里可以同时调多个独立工具** (并行执行已支持): 互不依赖的读取/查询**一次发出去**, 别一个一个来回 —
246
+ 零碎调用既慢又费预算(批量工具还会退还额度 ✓)。
247
+ 7.6 **收尾标记的语义**(别猜, 就按这条): 写完那个结束标记 = "**我已收尾, 且自己验过**(改了什么/拿什么验证的), 请复核"。
248
+ 只在这两种情况下写: ① 任务真的做完了(不是"我打算做") · ② 纯对话/问答也**不用**写(直接答即可)。
249
+ 没做完就**别写** —— 写了会被当成"已完成"进入复核, 反而浪费一轮 ✓。
250
+ 7. **过程中主动考虑**: 动手前先想这个改动的**影响面**(同类调用点 · 相邻功能 · 已有数据/契约), 发现关联问题就说出来,
251
+ 并给出你建议的下一步 —— 主动是指"想在你前面", 不是"多问几句"。
252
+ `;
177
253
  class PiAgentSession {
178
254
  cwd;
179
255
  peerId;
@@ -186,7 +262,90 @@ class PiAgentSession {
186
262
  sessionManager;
187
263
  agentsManager;
188
264
  socialHeartbeat = null;
189
- messageHistory = [];
265
+ /**
266
+ * **K5 第 4 步 (所有权转移)**: history 的**本体**。
267
+ * · 未绑定 actor ⇒ 本体在这里 (绑定前的暂存);
268
+ * · 绑定 actor 后 ⇒ 本体是 `actor.state.messageHistory`, 本地那份被**收养**并清空。
269
+ * 绑定由 `attachActor()` 做, 且**只在会话身份已知时**发生 (见 session factory) —— 没有身份就不归属。
270
+ */
271
+ /**
272
+ * **K5 第 4 步 — history 写入的唯一漏斗 (push)**。
273
+ * 为什么必须同步: 调用点遍布 ReAct 循环 / 工具回灌 / 压缩, 而且**写完立刻读** (length / 索引 / slice),
274
+ * 改成 await 会改变同拍可见性。所以这里有意识地**不排队** —— 归属收敛 + 可数 (门能断言"零处直写"),
275
+ * 并发安全由入口投递进 mailbox 负责 (K5 第 5 步)。
276
+ */
277
+ pushHistory(...msgs) {
278
+ for (const m of msgs) {
279
+ // 步骤⑧ 起**只有一条路径**: 写 actor (实例侧暂存字段已删除) —— 也彻底消除了"自递归"的可能
280
+ this.actor.appendMessageSync(m);
281
+ }
282
+ }
283
+ /** **K5 第 4 步 — history 写入漏斗 (pop)** —— 同 pushHistory 的性质 */
284
+ popHistory() {
285
+ return this.actor.popMessageSync();
286
+ }
287
+ /** **K5 第 4 步 — history 写入漏斗 (整体替换)**: hydrate 回灌 / 压缩后的整块赋值都走这里 */
288
+ /**
289
+ * **整体替换**历史 —— 唯一漏斗之一, 步骤⑧ 起是**唯一**的"种历史"入口 (公开)。
290
+ * 公开的理由: 上层/测试要种一份历史时必须走这里; 直接给对象赋一个新数组会**换掉数组身份**
291
+ * (actor 里那份仍是空的) —— 那是静默丢数据 (实测: 删掉 setter 后 `session.messageHistory = [...]`
292
+ * 变成写在一个凭空出现的自有属性上, 测试于是看到 0 条)。
293
+ */
294
+ replaceHistory(next) {
295
+ this.actor.replaceHistory(next);
296
+ }
297
+ /**
298
+ * **K5 步骤⑦ — 把一次性请求里的绑定落到位** (入口不再散着设字段: 只交一个请求)。
299
+ * 写入都走访问器 ⇒ 本体落进 `actor.state` (步骤③ 起就是这样); 没给的不覆盖 (与环境变量"只覆盖显式给出的"同秩)。
300
+ */
301
+ applyExecutionRequest(req) {
302
+ if (req.channelId)
303
+ this.actor.state.channelId = req.channelId;
304
+ if (req.agentId)
305
+ this.actor.state.agentId = req.agentId;
306
+ if (req.goalId)
307
+ this.actor.state.goalBinding = req.goalId;
308
+ if (req.resumeRunId)
309
+ this.resumeRunId = req.resumeRunId;
310
+ }
311
+ /**
312
+ * **K5 步骤⑦ — Pi 的唯一执行入口**: 只接收一次性 `ExecutionRequest`。
313
+ * 有 `onStream` ⇒ 走 `promptStream` (流式); 否则走 `prompt`。绑定由 `applyExecutionRequest` 落位。
314
+ * 入口侧只需: `deliverThroughActor(session, () => session.runExecution(req))`。
315
+ */
316
+ async runExecution(req) {
317
+ this.applyExecutionRequest(req);
318
+ if (req.onStream) {
319
+ return this.promptStream(req.input, req.onStream, req.signal, req.channelId);
320
+ }
321
+ return this.prompt(req.input, { signal: req.signal, channelId: req.channelId });
322
+ }
323
+ /**
324
+ * **K5 第 4 步**: 绑定 actor 并**收养**绑定前已存在的本地历史。
325
+ * 收养规则: actor 侧为空 且 本地非空 ⇒ 搬过去 (任何绑定时序都不丢历史); 收养后清空本地。
326
+ * 约束 (全量回归打出来的): 调用方必须用**会话身份**当注册键; 没有身份就**不要调**这个方法。
327
+ */
328
+ attachActor(actor) {
329
+ const prev = this.actor;
330
+ this.actor = actor;
331
+ if (!prev || prev === actor)
332
+ return;
333
+ // **收养**: 把上一个 actor (通常是"出生时的私有 actor") 的状态搬进新家。
334
+ // 新家已有值时不覆盖 —— 同一会话身份以先到者为准 (禁止"后到的调用把先建的状态冲掉")。
335
+ const from = prev.state;
336
+ const to = actor.state;
337
+ if (from.messageHistory.length > 0 && to.messageHistory.length === 0) {
338
+ to.messageHistory.push(...from.messageHistory);
339
+ }
340
+ if (from.channelId && !to.channelId)
341
+ to.channelId = from.channelId;
342
+ if (from.agentId && !to.agentId)
343
+ to.agentId = from.agentId;
344
+ if (from.goalBinding && !to.goalBinding)
345
+ to.goalBinding = from.goalBinding;
346
+ if (from.activeRun && !to.activeRun)
347
+ to.activeRun = from.activeRun;
348
+ }
190
349
  tools = new Map();
191
350
  /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
192
351
  _toolRegistry = new tool_registry_js_1.ToolRegistry();
@@ -194,7 +353,51 @@ class PiAgentSession {
194
353
  /** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
195
354
  cachedToolDefinitions = '';
196
355
  /** M2.4: 缓存 persona section */
356
+ /**
357
+ * 当前活跃计划 (待办) 的**重注入段** (2026-10-01 落实④)。
358
+ * 每轮都带 —— 因为上下文压缩会把早先注入的计划文本丢掉, 一次性注入等于"压完就忘"。
359
+ * 有界 (条数/字符都封顶); 读不到就返回空串, 绝不影响主流程。
360
+ */
361
+ async renderActivePlansSection() {
362
+ // 2026-10-01: 后台委派的结果**自动回灌** —— 完成后的第一轮在这里给一行提示(只提一次)
363
+ let bg = '';
364
+ try {
365
+ bg = (0, background_notices_js_1.renderDelegateNotices)();
366
+ }
367
+ catch { /* 提示失败不打断主流程 */ }
368
+ try {
369
+ const block = (0, background_notices_js_1.renderNoticeBlock)(); // 通用回流(复盘产出/后台任务) —— 空闲排空 ⇒ 新一轮 ✓
370
+ if (block)
371
+ bg = bg ? `${bg}\n${block}` : block;
372
+ }
373
+ catch { /* 回流失败不打断 */ }
374
+ let plans = '';
375
+ try {
376
+ const { listActivePlans, formatPlansForPrompt } = await Promise.resolve().then(() => __importStar(require('./plan-store.js')));
377
+ const list = await listActivePlans();
378
+ plans = formatPlansForPrompt(list, { maxChars: 1200, maxItems: 3 });
379
+ }
380
+ catch {
381
+ return bg;
382
+ }
383
+ return bg ? `${bg}\n${plans}` : plans;
384
+ }
197
385
  cachedPersonaSection = '';
386
+ /** 本轮迭代预算 (每轮重置; 批处理工具会退还 —— 见 iteration-budget.ts) */
387
+ /** 上一个工具调用的参数指纹 (遥测判"是否重复调用") */
388
+ lastToolSig = null;
389
+ /** 最近一条用户消息 (按需子集档用来判 intent) */
390
+ lastUserIntent = '';
391
+ /** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
392
+ tsTouchedThisTurn = [];
393
+ /** 本回合是否已跑过类型检查 (一轮只跑一次) */
394
+ typecheckRanThisTurn = false;
395
+ iterBudget = null;
396
+ iterBudgetWarned = false;
397
+ /** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
398
+ lastReviewedTaskSig = undefined;
399
+ /** 上一次回合后自审的时间 (节流; 0 = 从未) */
400
+ lastExperienceReviewAt = 0;
198
401
  /** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
199
402
  _sessionStore;
200
403
  /** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
@@ -305,11 +508,29 @@ class PiAgentSession {
305
508
  // 拼到 systemPrompt 末尾, 别再混进 user message
306
509
  contextHintAddition = '';
307
510
  /**
308
- * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
309
- * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
511
+ * **K2 收尾 (leo 2026-10-02 口径)**: **唯一一处**把"已有的恢复身份"播种进 RunContext。
512
+ *
513
+ * · `currentRunId` 属 **run-boundary**, 不是循环内部的可变 RunContext 状态 ⇒ 只允许这一次显式播种读取;
514
+ * · 冻结 `CURRENT_RUN_ID_SEED_READS = 1` (见 src/kernel/plan-runcontext.ts), 由门强制;
515
+ * · 只发生在**进入一次 Run 时**; 不得在循环中新增读取点; 不得成为新的写入口;
516
+ * · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
517
+ */
518
+ seedRunContext(extra = {}) {
519
+ return (0, run_context_js_1.createRunContext)({ runId: this.actor.state.activeRun, ...extra });
520
+ }
521
+ /**
522
+ * **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
523
+ * 现在只做**归属** —— 会话状态 (messageHistory / channelId / agentId / goalId) **仍然**在实例字段上,
524
+ * 逐项迁入 Actor 见 `src/kernel/plan-channel-actor.ts` 的 8 步。
310
525
  */
311
- currentOnStream = null;
312
- currentSignal = null;
526
+ actor;
527
+ /**
528
+ * **K2 迁移中**: 一次 Run 的显式状态载体 (见 src/agents/run-context.ts)。
529
+ * 入口 (prompt / promptStream / promptWithPivotLoop) 用 createRunContext() 快照一次, 用完即清。
530
+ * **已外置**: `eventSink` (原 currentOnStream) · `abortSignal` (原 currentSignal)。
531
+ * 其余 6 个字段仍以本类实例字段为准, 逐格迁移。
532
+ */
533
+ runCtx = (0, run_context_js_1.createRunContext)();
313
534
  /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
314
535
  bootstrapAddition = '';
315
536
  /** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
@@ -323,12 +544,17 @@ class PiAgentSession {
323
544
  - 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
324
545
  /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
325
546
  promptStartTime = 0;
326
- /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
327
- currentChannelId = '';
328
- /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
329
- currentAgentId = '';
330
- /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
331
- currentIntent = 'chitchat';
547
+ /**
548
+ * **K5 步骤③ (会话绑定迁入 Actor)**: 三处绑定 (channelId / agentId / goalBinding) 的本体住进
549
+ * `actor.state`, 实例上只留**绑定前的暂存** (`_channelId` 等), 由 `attachActor()` 收养后清空。
550
+ * 用访问器而不是改调用点: 写入点 (prompt 入口 / Goal 绑定 / snapshot) 分散且语义各异, 逐个改风险大;
551
+ * 访问器让所有读写自动落到 actor (同一个值对象), 调用点零改动。
552
+ * 注意: 这三个字段的**访问数不变** (台账按 "self.currentX" 形态计数) —— 迁的是**所有权**, 不是删访问。
553
+ * 本注释刻意不写出那种带 `this.` 前缀的字面形态: 计数口径只剥 `//` 行注释, 块注释里的同形串会被算进去
554
+ * (曾因此把 currentChannelId 的计数虚增 1, 被门照出)。
555
+ */
556
+ // **K5 步骤⑧ 批次4**: channelId 的访问器已删 —— 本体 `actor.state.channelId`, 直接影响本体。
557
+ // M2.2 intent 已外置到 runCtx.intent (K2); 拼 systemPrompt 时读 this.runCtx.intent
332
558
  /** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
333
559
  currentUserInput = '';
334
560
  /**
@@ -336,7 +562,16 @@ class PiAgentSession {
336
562
  * 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
337
563
  * 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
338
564
  */
339
- currentRunId = '';
565
+ /**
566
+ * **K5 步骤⑥ (Run 身份归属 Actor)**: 本体是 `actor.state.activeRun`。
567
+ * · 未绑定 actor ⇒ 住在这里 (`_runId`, 绑定前的暂存);
568
+ * · 绑定后 ⇒ 读写全落 actor (同一个值), 由 `attachActor()` 收养绑定前的值。
569
+ * 语义: "本会话当前活跃的 Run" —— 一个会话身份同时只有一个活跃 Run ✓ 与 Actor 的粒度一致。
570
+ * 注意: 访问数**不变** —— 迁的是**所有权**不是删访问 (与步骤③同理)。
571
+ * 本注释刻意不写出"带 self. 前缀 + 该字段名"的那种字面形态: K2 的计数只剥 `//` 行注释,
572
+ * 块注释里的同形串会被算进去 (同类踩过三次, 每次都被门照出)。
573
+ */
574
+ // **K5 步骤⑧ 批次5 (收尾)**: currentRunId 访问器已删 —— Run 身份的本体是 `actor.state.activeRun`, 直接读写本体。
340
575
  /** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
341
576
  lastRunId = '';
342
577
  runSurface = 'cli';
@@ -353,14 +588,14 @@ class PiAgentSession {
353
588
  currentIntentHint = '';
354
589
  /**
355
590
  * 算 judgment 注入门: 失败静默, 不阻塞主对话
356
- * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
591
+ * 期间通过 runCtx.eventSink 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
357
592
  * 调用方负责用完即清 (judgmentGateAddition='')
358
593
  */
359
594
  async computeJudgmentGate(input) {
360
595
  const safePhase = (phase, extra = {}) => {
361
596
  try {
362
- if (this.currentOnStream) {
363
- this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
597
+ if (this.runCtx.eventSink) {
598
+ this.runCtx.eventSink({ type: 'phase', phase, ...extra, content: '' });
364
599
  }
365
600
  }
366
601
  catch { /* 静默 */ }
@@ -402,17 +637,51 @@ class PiAgentSession {
402
637
  this.judgmentGateNegativeUsedIds = [];
403
638
  }
404
639
  constructor(config) {
640
+ // **K5 步骤⑧**: actor 从出生就在 —— 工厂通常在**构造前**就把身份 actor 注进来 (`config.actor`);
641
+ // 没有身份时用一份**私有 actor** (不注册 ⇒ 谁也拿不到, 隔离性优先)。
642
+ // ⇒ 实例侧不再需要"绑定前暂存"字段 (那些字段本步删除)。
643
+ this.actor = config.actor ?? (0, channel_actor_js_1.createPrivateActor)();
405
644
  this.cwd = config.cwd;
406
645
  this.peerId = config.peerId || 'local';
407
646
  this.identity = config.identityDoc || this.createDefaultIdentity();
408
647
  this.minimaxAvailable = this.checkMinimax();
409
648
  // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
410
- this.currentAgentId = config.agentId || '';
649
+ this.actor.state.agentId = config.agentId || '';
650
+ // 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
651
+ // 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
652
+ // 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
653
+ // 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
654
+ // 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
655
+ try {
656
+ if (this.actor.state.agentId) {
657
+ const cur = this.identity || {};
658
+ const curDid = String(cur.did || '');
659
+ const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
660
+ const missingName = !String(cur.name || '').trim();
661
+ // 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
662
+ // 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
663
+ // 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
664
+ const mine = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(this.actor.state.agentId);
665
+ const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
666
+ if (badDid || missingName || mismatch) {
667
+ if (badDid || mismatch) {
668
+ console.warn(`[identity] 身份纠正: ${this.actor.state.agentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
669
+ }
670
+ // 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
671
+ // 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
672
+ Object.assign(this.identity, {
673
+ ...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
674
+ ...(missingName ? { name: (0, channel_identity_js_1.agentPersonaName)(this.actor.state.agentId) || this.actor.state.agentId } : {}),
675
+ });
676
+ }
677
+ }
678
+ }
679
+ catch { /* 自愈失败不致命 */ }
411
680
  // 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
412
681
  this._sessionStore = config.sessionStore ?? session_store_js_1.sessionStore;
413
682
  this.constraintLayer = new constraint_layer_js_1.ConstraintLayer();
414
683
  this.workflowEngine = new workflow_engine_js_1.WorkflowEngine(this.constraintLayer);
415
- this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd);
684
+ this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
416
685
  this.agentsManager = new heartbeat_js_1.DiscoveredAgentsManager();
417
686
  this.usePivotLoop = config.usePivotLoop ?? false;
418
687
  this.pivotLoopConfig = config.pivotLoopConfig;
@@ -498,6 +767,18 @@ class PiAgentSession {
498
767
  */
499
768
  async hydrateMessageHistory(sessionKey, maxMessages) {
500
769
  try {
770
+ // **K5 第 4 步**: 实现体 (load → filter → 截断 → 替换) 已搬进 Actor —— 走邮箱 ⇒ 与 append/persist 串行。
771
+ // 业务侧只交两个**纯回调** (内核因此不必 import 业务模块); 未绑定 actor 的会话走下面原路径 (行为不变)。
772
+ if (this.actor) {
773
+ const n = await this.actor.hydrateHistory({
774
+ load: () => this._sessionStore.loadMessages(sessionKey),
775
+ filter: (loaded) => this._filterToMessage(loaded),
776
+ maxMessages,
777
+ });
778
+ if (n > 0)
779
+ console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${n} 条历史 (经 Channel Actor)`);
780
+ return;
781
+ }
501
782
  const loaded = await this._sessionStore.loadMessages(sessionKey);
502
783
  if (!loaded) {
503
784
  console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
@@ -505,7 +786,7 @@ class PiAgentSession {
505
786
  }
506
787
  const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
507
788
  if (hydrated.length > 0) {
508
- this.messageHistory = hydrated;
789
+ this.replaceHistory(hydrated);
509
790
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
510
791
  }
511
792
  }
@@ -525,7 +806,10 @@ class PiAgentSession {
525
806
  * 即可获得"重启 / 跨进程接续"的语义.
526
807
  */
527
808
  async saveCurrentSession(key) {
528
- const persisted = this.messageHistory.map((m) => ({
809
+ // **K5 第 4 步**: 取数拍走 Actor 的快照 (走邮箱 ⇒ 与 append 串行, 不会抓到"边写边读"的半截状态);
810
+ // 未绑定 actor 的会话走原路径 (行为不变)。
811
+ const source = this.actor ? await this.actor.historySnapshot() : this.actor.state.messageHistory;
812
+ const persisted = source.map((m) => ({
529
813
  role: m.role,
530
814
  content: m.content,
531
815
  toolCall: m.toolCall,
@@ -542,9 +826,9 @@ class PiAgentSession {
542
826
  * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
543
827
  */
544
828
  async resumeSession(key, maxMessages = 30) {
545
- const before = this.messageHistory.length;
829
+ const before = this.actor.state.messageHistory.length;
546
830
  await this.hydrateMessageHistory(key, maxMessages);
547
- return this.messageHistory.length - before;
831
+ return this.actor.state.messageHistory.length - before;
548
832
  }
549
833
  /**
550
834
  * 2026-06-30: 读历史不修改 messageHistory.
@@ -650,6 +934,59 @@ class PiAgentSession {
650
934
  this._inboxMessages = [];
651
935
  const toolCtx = {
652
936
  tools: this.tools,
937
+ // 2026-10-01 (用户: 「process 也要可以管理群聊和去中心化交流进程」):
938
+ // 把**常驻的交流类服务**也挂进 process —— 能启停的给启停(社交心跳 ✓), 只能看的如实标"仅状态" ✓(不假装能停 ✗)
939
+ managedServices: (() => {
940
+ const ms = managed_services_js_1.managedServices;
941
+ ms.register({
942
+ name: 'social-heartbeat',
943
+ description: '社交心跳: 主动发现节点/发起对话/组织群聊的常驻循环(去中心化交流用)',
944
+ status: () => (this.socialHeartbeat ? '运行中' : '已停止'),
945
+ start: () => this.startSocialHeartbeat(),
946
+ stop: () => this.stopSocialHeartbeat(),
947
+ });
948
+ ms.register({
949
+ name: 'p2p-network',
950
+ description: 'P2P 网络: 与其他 bolloon 节点的连接(群聊/消息都走它)',
951
+ status: () => { try {
952
+ const peers = this.getPeers?.() ?? [];
953
+ return `已连接 ${peers.length} 个节点`;
954
+ }
955
+ catch {
956
+ return '状态不可用';
957
+ } },
958
+ });
959
+ ms.register({
960
+ name: 'orbitdb-groups',
961
+ description: 'OrbitDB 群聊存储: 本地群列表与消息持久化',
962
+ status: () => { try {
963
+ const g = this.listGroupsShallow?.();
964
+ return g ? `本地群 ${g}` : '已就绪(用 /group 看列表)';
965
+ }
966
+ catch {
967
+ return '状态不可用';
968
+ } },
969
+ });
970
+ ms.register({
971
+ name: 'document-receiver',
972
+ description: '文档接收器: 监听并接收其他节点发来的文档分片',
973
+ status: () => '监听中(随进程启动)',
974
+ });
975
+ return ms;
976
+ })(),
977
+ // 2026-10-01: 技能发现(list_skills / read_skill)的实现 —— 技能原先只注册不露面 ✗, 现在模型能自己找
978
+ listSkills: () => this.skillRegistry.list().map((sk) => ({ name: sk.name, description: String(sk.description || '') })),
979
+ getSkillBody: async (name) => {
980
+ const sk = this.skillRegistry.get(name);
981
+ if (!sk)
982
+ return null;
983
+ try {
984
+ return await sk.execute({});
985
+ }
986
+ catch {
987
+ return null;
988
+ }
989
+ },
653
990
  cwd: this.cwd,
654
991
  identity: this.identity,
655
992
  persona: this.persona,
@@ -663,7 +1000,7 @@ class PiAgentSession {
663
1000
  const { CHANNELS_PATH } = await Promise.resolve().then(() => __importStar(require('../web/server-types.js')));
664
1001
  const { loadChannels } = await Promise.resolve().then(() => __importStar(require('../web/server-storage.js')));
665
1002
  const channels = await loadChannels();
666
- const ch = channels.find((c) => c.id === this.currentChannelId);
1003
+ const ch = channels.find((c) => c.id === this.actor.state.channelId);
667
1004
  if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
668
1005
  return {
669
1006
  encryptedPrivateKey: ch.encryptedPrivateKey,
@@ -712,16 +1049,18 @@ class PiAgentSession {
712
1049
  // 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
713
1050
  if (this.cachedToolDefinitions)
714
1051
  return this.cachedToolDefinitions;
715
- const defs = ['可用工具 (name(params) - 简介):'];
716
1052
  // 2026-07-29: 使用 allowedTools() 过滤掉拒绝列表中的工具
717
- for (const tool of this.allowedTools()) {
718
- // 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
719
- // 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
720
- // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
721
- const paramNames = Object.keys(tool.parameters).join(',');
722
- defs.push(`- ${tool.name}(${paramNames})`);
723
- }
724
- this.cachedToolDefinitions = defs.join('\n');
1053
+ const allowed = Array.from(this.allowedTools());
1054
+ const header = '可用工具 (按类分组; name(params) - 简介):';
1055
+ // 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
1056
+ // 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
1057
+ // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
1058
+ // 2026-10-01 (优化 #2 v2): 按类**分组**列 + 说明"未列出的也能按名字调" ⇒ 治选择过载, 且不丢能力。
1059
+ // BOLLOON_TOOL_SUBSET=on 时改走"按 intent 只展开相关桶"(仍带 list_tools + 能力提示 ✓)。
1060
+ const subsetOn = String(process.env.BOLLOON_TOOL_SUBSET || '').toLowerCase() === 'on';
1061
+ const intentText = subsetOn ? this.lastUserIntent : '';
1062
+ const rendered = (0, tool_subset_js_1.renderToolListWithParams)(allowed, intentText, { subset: subsetOn, perBucket: 14 });
1063
+ this.cachedToolDefinitions = `${header}\n${rendered.text}`;
725
1064
  return this.cachedToolDefinitions;
726
1065
  }
727
1066
  async initSession() {
@@ -733,12 +1072,28 @@ class PiAgentSession {
733
1072
  }
734
1073
  }
735
1074
  createDefaultIdentity() {
736
- return {
737
- did: `did:pi:${this.peerId.substring(0, 16)}`,
738
- name: `Agent-${this.peerId.substring(0, 8)}`,
739
- publicKey: this.peerId,
740
- createdAt: Date.now()
741
- };
1075
+ // 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
1076
+ // 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
1077
+ // 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
1078
+ const scope = this.actor.state.agentId || this.peerId || 'default';
1079
+ try {
1080
+ const real = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(scope);
1081
+ return {
1082
+ did: real.did,
1083
+ name: `Agent-${this.peerId.substring(0, 8)}`,
1084
+ publicKey: real.publicKey || this.peerId,
1085
+ createdAt: Date.now(),
1086
+ };
1087
+ }
1088
+ catch {
1089
+ // 真身份生成失败时**不编假 DID**: 如实留空, 由 /did 与 get_identity 报"未绑定"
1090
+ return {
1091
+ did: '',
1092
+ name: `Agent-${this.peerId.substring(0, 8)}`,
1093
+ publicKey: this.peerId,
1094
+ createdAt: Date.now(),
1095
+ };
1096
+ }
742
1097
  }
743
1098
  checkMinimax() {
744
1099
  try {
@@ -779,9 +1134,9 @@ class PiAgentSession {
779
1134
  const did = await resolveUserDid();
780
1135
  const model = this.llmConfig?.model || this.model || '';
781
1136
  return new TrajectoryRecorder({
782
- agentId: this.currentAgentId || 'default',
1137
+ agentId: this.actor.state.agentId || 'default',
783
1138
  input,
784
- channelId: channelId || this.currentChannelId,
1139
+ channelId: channelId || this.actor.state.channelId,
785
1140
  did: did || undefined,
786
1141
  model: typeof model === 'string' && model ? model : undefined,
787
1142
  });
@@ -823,8 +1178,8 @@ class PiAgentSession {
823
1178
  if (gate !== 'ready') {
824
1179
  const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
825
1180
  const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
826
- this.messageHistory.push({ role: 'user', content: input });
827
- this.messageHistory.push({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1181
+ this.pushHistory({ role: 'user', content: input });
1182
+ this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
828
1183
  console.warn(`[PiAgent] 拒绝执行: ${why}`);
829
1184
  return `[初始化未就绪] ${why}\n${hint}`;
830
1185
  }
@@ -832,13 +1187,13 @@ class PiAgentSession {
832
1187
  catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
833
1188
  }
834
1189
  this.minimaxAvailable = this.checkMinimax();
835
- this.currentChannelId = options?.channelId ?? this.currentChannelId;
1190
+ this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
836
1191
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
837
1192
  const trajRec = await this.createTrajectoryRecorder(input, options?.channelId);
838
1193
  if (trajRec && options?.onStream) {
839
1194
  options = { ...options, onStream: this.wrapTrajectoryStream(options.onStream, trajRec) };
840
1195
  }
841
- this.messageHistory.push({
1196
+ this.pushHistory({
842
1197
  role: 'user',
843
1198
  content: input
844
1199
  });
@@ -848,23 +1203,23 @@ class PiAgentSession {
848
1203
  // 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
849
1204
  const response = await this.handleFallback(input);
850
1205
  try {
851
- let boundGoalId = this.currentGoalId;
1206
+ let boundGoalId = this.actor.state.goalBinding;
852
1207
  if (!boundGoalId) {
853
1208
  const g = await (0, goal_store_js_1.createGoal)({
854
1209
  objective: this.currentUserInput || input.slice(0, 200),
855
- channelId: this.currentChannelId || undefined,
856
- agentId: this.currentAgentId || undefined,
1210
+ channelId: this.actor.state.channelId || undefined,
1211
+ agentId: this.actor.state.agentId || undefined,
857
1212
  createdBy: this.runSurface,
858
1213
  });
859
1214
  boundGoalId = g.goalId;
860
1215
  }
861
- this.currentGoalId = boundGoalId;
1216
+ this.actor.state.goalBinding = boundGoalId;
862
1217
  const rec = await (0, run_store_js_1.startRun)({
863
1218
  surface: this.runSurface,
864
1219
  goal: this.currentUserInput || input.slice(0, 200),
865
1220
  goalId: boundGoalId,
866
- channelId: this.currentChannelId || undefined,
867
- agentId: this.currentAgentId || undefined,
1221
+ channelId: this.actor.state.channelId || undefined,
1222
+ agentId: this.actor.state.agentId || undefined,
868
1223
  modelConfig: await this.runModelSnapshot(),
869
1224
  });
870
1225
  this.lastRunId = rec.runId;
@@ -903,25 +1258,27 @@ class PiAgentSession {
903
1258
  catch (err) {
904
1259
  await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
905
1260
  }
906
- this.messageHistory.push({ role: 'assistant', content: response });
1261
+ this.pushHistory({ role: 'assistant', content: response });
907
1262
  this.reportUsageToContextManager();
908
1263
  this.finishTrajectory(trajRec, response);
909
1264
  return response;
910
1265
  }
911
1266
  // P0 注入门
912
- this.currentSignal = options?.signal ?? null;
913
- this.currentOnStream = options?.onStream ?? null;
1267
+ // K2: eventSink + abortSignal 已外置, 入口一次建好本轮 Context。
1268
+ // 其余字段**不在这里抄一份** —— 抄写会新增对旧实例字段的读, 违反方向判据
1269
+ // 「新层出现后旧写口调用点数只许不变或减少」。逐字段迁移时再各自搬进来。
1270
+ this.runCtx = this.seedRunContext({ eventSink: options?.onStream ?? null, abortSignal: options?.signal ?? null });
914
1271
  await this.computeJudgmentGate(input);
915
1272
  // M2.2 (2026-06-17): intent 分类 — prompt() 路径也跑 (跟 promptStream 对齐)
916
1273
  try {
917
1274
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
918
- this.currentIntent = classifyIntent(input);
919
- this.currentIntentHint = intentHint(this.currentIntent);
1275
+ this.runCtx.intent = classifyIntent(input);
1276
+ this.currentIntentHint = intentHint(this.runCtx.intent);
920
1277
  this.currentUserInput = input;
921
1278
  }
922
1279
  catch (err) {
923
1280
  console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
924
- this.currentIntent = 'chitchat';
1281
+ this.runCtx.intent = 'chitchat';
925
1282
  this.currentIntentHint = '';
926
1283
  }
927
1284
  // P2: 解析当前 permission mode
@@ -946,14 +1303,113 @@ class PiAgentSession {
946
1303
  (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
947
1304
  }
948
1305
  this.clearJudgmentGate();
949
- this.currentSignal = null;
950
- this.currentOnStream = null;
1306
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
951
1307
  this.reportUsageToContextManager();
952
1308
  }
953
1309
  }
954
1310
  try {
955
1311
  // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
956
- const loopResult = await this.runReActLoop(this.currentOnStream ?? undefined, options?.signal);
1312
+ const loopResult = await this.runReActLoop(this.runCtx.eventSink ?? undefined, options?.signal);
1313
+ // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
1314
+ if ((0, code_write_gate_js_1.decideTypecheck)(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
1315
+ this.typecheckRanThisTurn = true;
1316
+ try {
1317
+ const tscTool = this.tools.get('tsc_check');
1318
+ if (tscTool?.execute) {
1319
+ const r = await tscTool.execute({});
1320
+ const line = (0, code_write_gate_js_1.formatTypecheckResult)(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
1321
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1322
+ }
1323
+ }
1324
+ catch (e) {
1325
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1326
+ }
1327
+ this.tsTouchedThisTurn = [];
1328
+ }
1329
+ // 2026-10-01: **回合后自审 (纯旁路)** —— 沉淀可复用经验, 对应"结束后沉淀"那一段。
1330
+ // 纪律: fire-and-forget (不阻塞主回合) · 只写 ~/.bolloon/experience/ (**不碰主对话/prompt 缓存**)
1331
+ // · 节流 (默认 10 分钟/agent) · 失败只记一行日志, 绝不影响本回合结果。
1332
+ try {
1333
+ const reviewNow = Date.now();
1334
+ const turnSummary = `用户: ${String(this.currentUserInput || '').slice(0, 2000)}\n助手: ${String(loopResult?.reply || '').slice(0, 4000)}`;
1335
+ // 任务签名: 用户这次要的东西 + 用了哪些工具 ⇒ 换任务立刻复盘(同一任务才节流) ✓
1336
+ const taskSig = (0, node_crypto_1.createHash)('sha256')
1337
+ .update(`${String(this.currentUserInput || '').slice(0, 300)}|${this.totalToolCallsThisTurn || ''}`)
1338
+ .digest('hex').slice(0, 12);
1339
+ if ((0, experience_review_js_1.shouldReviewTask)(reviewNow, this.lastExperienceReviewAt, this.lastReviewedTaskSig, taskSig)) {
1340
+ this.lastExperienceReviewAt = reviewNow; // 先记账, 免得多路并发各起一次
1341
+ this.lastReviewedTaskSig = taskSig;
1342
+ // 2026-10-01 (用户: 「我没有看到执行完命令后 bolloon 知道选中更新 skills,是不是没有触发」):
1343
+ // 实测**确实没触发** ✓ —— 两个原因: ① 被调方又自己判了一次节流 ⇒ 换任务也被 throttled 掉 ✗;
1344
+ // ② 反馈走 `console.warn`(TUI 里被吞 ✗) + `void … .catch(() => {})`(失败无声 ✗)
1345
+ // ⇒ 你什么都看不到 ✓。现在: 传 force 尊重调用方的决定 ✓ · 复盘过程/结果/失败**都进对话流** ✓ ·
1346
+ // 同时落一份 `~/.bolloon/logs/experience-review.log` 供事后核 ✓。
1347
+ const reviewLog = (m) => {
1348
+ const line = `[${new Date().toISOString()}] ${m}`;
1349
+ try {
1350
+ const p = path.join(os.homedir(), '.bolloon', 'logs', 'experience-review.log');
1351
+ void fs.mkdir(path.dirname(p), { recursive: true })
1352
+ .then(() => fs.appendFile(p, line + '\n'))
1353
+ .catch(() => { });
1354
+ }
1355
+ catch { /* 落日志失败不影响主流程 */ }
1356
+ try {
1357
+ this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1358
+ }
1359
+ catch { /* 上屏失败不打断 */ }
1360
+ };
1361
+ reviewLog('开始(换了任务 ⇒ 立刻复盘)');
1362
+ void (0, experience_review_js_1.runExperienceReview)({
1363
+ turnSummary,
1364
+ force: true,
1365
+ chat: async (prompt) => {
1366
+ const r = await (0, index_js_1.getMinimax)().chat(prompt);
1367
+ return typeof r === 'string' ? r : String(r?.content ?? r?.text ?? '');
1368
+ },
1369
+ log: reviewLog,
1370
+ // 2026-10-01: 写了经验之后再找"能沉淀进哪个已有技能"的候选 —— **只记候选, 不自动改技能** ✓
1371
+ // (实测: 拿真实教训撞真实 1300 个技能, 最高分只有 2 且 top 命中是瞎的 ✗ ⇒ 不替人决定 ✓)
1372
+ onLesson: (lesson) => {
1373
+ try {
1374
+ // ⓐ 管理: 找"能沉淀进哪个已有技能"的候选(只记, 不自动改 ✗)
1375
+ const dirs = (0, skill_loader_js_1.defaultSkillPaths)(os.homedir(), process.cwd());
1376
+ const decision = (0, lesson_to_skill_js_1.decideLessonSink)(lesson, (0, lesson_to_skill_js_1.skillsFromDirs)(dirs));
1377
+ (0, lesson_to_skill_js_1.logLessonSuggestions)(lesson, decision);
1378
+ // ⓐ' 用户要求「**都进去**」⇒ 每条教训都落进技能库: 强命中写那个技能 ✓, 否则写沉淀技能 `lessons-learned` ✓
1379
+ // 2026-10-01 **写来源隔离**: 复盘/自审产生的写, 打上 review 标记 ✓ ⇒
1380
+ // 它只许治理**自己造出来**的技能 ✓(用户在 skills 里点名要的归用户 ✓)
1381
+ (0, skill_ledger_js_1.runWithWriteOrigin)('review', () => (0, lesson_to_skill_js_1.routeLessonToSkill)(lesson, (0, lesson_to_skill_js_1.skillsFromDirs)(dirs)));
1382
+ // 2026-10-01 (用户: 「复盘任务能否触发 loop?」): 教训**回流成下一轮的任务源** ✓ ——
1383
+ // 只"记下来"等于闭环断在这 ✗; 投进共享队列 ⇒ 空闲时排空 ⇒ 以**新一轮**浮现 ✓(绝不中途插队 ✓)。
1384
+ try {
1385
+ (0, background_notices_js_1.pushNotice)('review', `${lesson.title} —— ${String(lesson.body || '').replace(/\s+/g, ' ').slice(0, 120)}`);
1386
+ }
1387
+ catch { /* 回流失败不打断 */ }
1388
+ // ⓑ 接入**判断力系统**: 同一条教训也进 HumanJudgment(带 source/confidence/revisable ⇒ 可被后续演化取代 ✓)
1389
+ // 这样经验库与判断力库**同一份来源**, 判断力注入(gate)时就能用上 ✓
1390
+ Promise.resolve().then(() => __importStar(require('../pi-ecosystem-judgment/human-value-store.js'))).then((m) => m.storeHumanJudgment({
1391
+ decision: lesson.title,
1392
+ decision_type: 'modify',
1393
+ reasons: [lesson.body],
1394
+ values_derived: [],
1395
+ context: { domain: lesson.klass || 'general', complexity: 'simple', stakes: 'medium', time_pressure: 'low' },
1396
+ outcome: { approved: true },
1397
+ metadata: { source: 'trajectory', confidence: 0.7, revisable: true },
1398
+ })).catch(() => { });
1399
+ }
1400
+ catch { /* 这一段整体是锦上添花, 绝不外泄错误 */ }
1401
+ },
1402
+ }).then((r) => {
1403
+ // 结果**如实上屏** ✓: 审没审 / 写没写 / 为什么没写 —— 不再无声 ✓
1404
+ reviewLog(r?.reviewed
1405
+ ? (r?.applied ? `已沉淀经验 ✓${r?.file ? ` → ${path.basename(String(r.file))}` : ''}` : `审了但没写: ${r?.reason || '无'}`)
1406
+ : `没审: ${r?.reason || '无'}`);
1407
+ }).catch((e) => {
1408
+ reviewLog(`失败(已如实记录, 不外泄错误): ${String(e?.message || e).slice(0, 160)}`);
1409
+ });
1410
+ }
1411
+ }
1412
+ catch { /* 挂点自身失败也不影响主流程 */ }
957
1413
  this.finishTrajectory(trajRec, loopResult.reply, loopResult.aiFailed ? 'error' : 'ok');
958
1414
  return loopResult.reply;
959
1415
  }
@@ -965,8 +1421,7 @@ class PiAgentSession {
965
1421
  (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
966
1422
  }
967
1423
  this.clearJudgmentGate();
968
- this.currentSignal = null;
969
- this.currentOnStream = null;
1424
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
970
1425
  this.reportUsageToContextManager();
971
1426
  }
972
1427
  }
@@ -974,7 +1429,7 @@ class PiAgentSession {
974
1429
  console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
975
1430
  this.minimaxAvailable = this.checkMinimax();
976
1431
  console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
977
- this.currentChannelId = channelId ?? this.currentChannelId;
1432
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
978
1433
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
979
1434
  const trajRec = await this.createTrajectoryRecorder(input, channelId);
980
1435
  if (trajRec)
@@ -987,7 +1442,7 @@ class PiAgentSession {
987
1442
  const userText = markerMatch ? markerMatch[1].trim() : input;
988
1443
  const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
989
1444
  console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
990
- this.messageHistory.push({
1445
+ this.pushHistory({
991
1446
  role: 'user',
992
1447
  content: userText
993
1448
  });
@@ -997,7 +1452,7 @@ class PiAgentSession {
997
1452
  // 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
998
1453
  try {
999
1454
  const { recallMemory } = await Promise.resolve().then(() => __importStar(require('./memory-recall.js')));
1000
- const recalled = await recallMemory({ query: userText, agentId: this.currentAgentId || this.peerId || '' });
1455
+ const recalled = await recallMemory({ query: userText, agentId: this.actor.state.agentId || this.peerId || '' });
1001
1456
  if (recalled) {
1002
1457
  this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
1003
1458
  }
@@ -1006,29 +1461,29 @@ class PiAgentSession {
1006
1461
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1007
1462
  if (!this.minimaxAvailable) {
1008
1463
  const response = await this.handleFallback(userText);
1009
- this.messageHistory.push({ role: 'assistant', content: response });
1464
+ this.pushHistory({ role: 'assistant', content: response });
1010
1465
  onStream({ type: 'done', content: '' });
1011
1466
  this.reportUsageToContextManager();
1012
1467
  this.finishTrajectory(trajRec, response);
1013
1468
  return response;
1014
1469
  }
1015
- // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1016
- this.currentOnStream = onStream;
1017
- this.currentSignal = signal ?? null;
1470
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 runCtx.eventSink 广播 phase
1471
+ // K2: eventSink + abortSignal 一起建进本轮 Context (只搬已外置的字段, 不抄未迁移的)
1472
+ this.runCtx = this.seedRunContext({ eventSink: onStream, abortSignal: signal ?? null });
1018
1473
  await this.computeJudgmentGate(userText);
1019
1474
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1020
1475
  try {
1021
1476
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1022
- this.currentIntent = classifyIntent(userText);
1023
- this.currentIntentHint = intentHint(this.currentIntent);
1477
+ this.runCtx.intent = classifyIntent(userText);
1478
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1024
1479
  this.currentUserInput = userText;
1025
- if (this.currentIntent !== 'chitchat') {
1026
- onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
1480
+ if (this.runCtx.intent !== 'chitchat') {
1481
+ onStream({ type: 'phase', phase: 'intent_classified', detail: this.runCtx.intent, content: '' });
1027
1482
  }
1028
1483
  }
1029
1484
  catch (err) {
1030
1485
  console.warn('[PiAgent] classifyIntent failed (non-fatal):', err);
1031
- this.currentIntent = 'chitchat';
1486
+ this.runCtx.intent = 'chitchat';
1032
1487
  this.currentIntentHint = '';
1033
1488
  }
1034
1489
  // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
@@ -1045,8 +1500,8 @@ class PiAgentSession {
1045
1500
  let bootstrapAddition = '';
1046
1501
  try {
1047
1502
  const ss = await (0, human_value_pipeline_js_1.onSessionStart)({
1048
- channelId: this.currentChannelId || undefined,
1049
- agentId: this.currentAgentId || undefined,
1503
+ channelId: this.actor.state.channelId || undefined,
1504
+ agentId: this.actor.state.agentId || undefined,
1050
1505
  });
1051
1506
  bootstrapAddition = ss.systemAddition || '';
1052
1507
  }
@@ -1055,10 +1510,10 @@ class PiAgentSession {
1055
1510
  }
1056
1511
  // 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
1057
1512
  // 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
1058
- if (this.currentChannelId) {
1513
+ if (this.actor.state.channelId) {
1059
1514
  try {
1060
1515
  const { getRecentEvents } = await Promise.resolve().then(() => __importStar(require('../bootstrap/event-log.js')));
1061
- const events = await getRecentEvents(this.currentChannelId, 5);
1516
+ const events = await getRecentEvents(this.actor.state.channelId, 5);
1062
1517
  if (events.length > 0) {
1063
1518
  const eventBlock = [
1064
1519
  '## 最近项目事件 (最近 5 条, 倒序)',
@@ -1073,7 +1528,7 @@ class PiAgentSession {
1073
1528
  // 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
1074
1529
  try {
1075
1530
  const { readState, formatStateForPrompt } = await Promise.resolve().then(() => __importStar(require('../bootstrap/project-state.js')));
1076
- const state = await readState({ channelId: this.currentChannelId });
1531
+ const state = await readState({ channelId: this.actor.state.channelId });
1077
1532
  const stateText = formatStateForPrompt(state);
1078
1533
  if (stateText) {
1079
1534
  bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
@@ -1085,7 +1540,7 @@ class PiAgentSession {
1085
1540
  // 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
1086
1541
  try {
1087
1542
  const { searchIndex } = await Promise.resolve().then(() => __importStar(require('../bootstrap/vector-index.js')));
1088
- const indexName = `channel-${this.currentChannelId}`;
1543
+ const indexName = `channel-${this.actor.state.channelId}`;
1089
1544
  const results = await searchIndex({
1090
1545
  indexName,
1091
1546
  query: userText,
@@ -1146,13 +1601,12 @@ class PiAgentSession {
1146
1601
  (0, monitor_gate_js_1.monitorAfterReply)(userText, pivotResult);
1147
1602
  const stopStartTime = this.promptStartTime || Date.now();
1148
1603
  (0, human_value_pipeline_js_1.onStop)({
1149
- channelId: this.currentChannelId || 'unknown',
1604
+ channelId: this.actor.state.channelId || 'unknown',
1150
1605
  durationMs: Date.now() - stopStartTime,
1151
1606
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1152
1607
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1153
1608
  this.clearJudgmentGate();
1154
- this.currentOnStream = null;
1155
- this.currentSignal = null;
1609
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1156
1610
  this.bootstrapAddition = '';
1157
1611
  this.contextHintAddition = '';
1158
1612
  this.promptStartTime = 0;
@@ -1182,8 +1636,7 @@ class PiAgentSession {
1182
1636
  }
1183
1637
  catch (err) {
1184
1638
  // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1185
- this.currentOnStream = null;
1186
- this.currentSignal = null;
1639
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1187
1640
  throw err;
1188
1641
  }
1189
1642
  attempt++;
@@ -1221,8 +1674,8 @@ class PiAgentSession {
1221
1674
  // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
1222
1675
  // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
1223
1676
  // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
1224
- if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1225
- this.messageHistory.pop();
1677
+ if (this.actor.state.messageHistory.length > 0 && this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1].role === 'assistant') {
1678
+ this.popHistory();
1226
1679
  }
1227
1680
  }
1228
1681
  onStream({ type: 'done', content: '' });
@@ -1240,15 +1693,14 @@ class PiAgentSession {
1240
1693
  // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1241
1694
  const stopStartTime = this.promptStartTime || Date.now();
1242
1695
  (0, human_value_pipeline_js_1.onStop)({
1243
- channelId: this.currentChannelId || 'unknown',
1696
+ channelId: this.actor.state.channelId || 'unknown',
1244
1697
  durationMs: Date.now() - stopStartTime,
1245
1698
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1246
1699
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1247
1700
  // 用完即清, 避免污染下一轮
1248
1701
  this.clearJudgmentGate();
1249
- this.currentOnStream = null;
1702
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1250
1703
  this.reportUsageToContextManager();
1251
- this.currentSignal = null;
1252
1704
  this.bootstrapAddition = '';
1253
1705
  this.promptStartTime = 0;
1254
1706
  // 2026-08-08: 轨迹收尾 — 落盘 + OrbitDB (失败静默, 不阻塞回复)
@@ -1256,7 +1708,7 @@ class PiAgentSession {
1256
1708
  return result;
1257
1709
  }
1258
1710
  async promptWithPivotLoop(input, config, channelId) {
1259
- this.currentChannelId = channelId ?? this.currentChannelId;
1711
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1260
1712
  if (!this.minimaxAvailable) {
1261
1713
  const response = await this.handleFallback(input);
1262
1714
  return {
@@ -1288,8 +1740,8 @@ class PiAgentSession {
1288
1740
  // M2.2 (2026-06-17): intent 分类 — pivot loop 也要拿到 hint
1289
1741
  try {
1290
1742
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1291
- this.currentIntent = classifyIntent(input);
1292
- this.currentIntentHint = intentHint(this.currentIntent);
1743
+ this.runCtx.intent = classifyIntent(input);
1744
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1293
1745
  this.currentUserInput = input;
1294
1746
  }
1295
1747
  catch (err) {
@@ -1312,6 +1764,10 @@ ${this.getToolDefinitions()}
1312
1764
 
1313
1765
  ${PiAgentSession.TOOL_SELECTION_GUIDE}
1314
1766
 
1767
+ ${exports.PROACTIVE_WORK_DISCIPLINE}
1768
+
1769
+ ${await this.renderActivePlansSection()}
1770
+
1315
1771
  工作模式:
1316
1772
  1. 理解用户自然语言请求
1317
1773
  2. 分析需要哪些工具来完成
@@ -1332,21 +1788,21 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1332
1788
  - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
1333
1789
  - 工具调用后必须等结果, 不要在同一个回复里继续输出
1334
1790
  - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1335
- // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1791
+ // 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1336
1792
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1337
1793
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1338
1794
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
1339
1795
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
1340
1796
  // 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
1341
- // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
1797
+ // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 actor 的 messageHistory 同步.
1342
1798
  // 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
1343
1799
  // 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
1344
1800
  // 不再撞预算. 这条路径留作技术债.
1345
1801
  // 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
1346
- // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 this.messageHistory 隔离,
1802
+ // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
1347
1803
  // 不注入的话 LLM 看不到历史对话, 每次都是新对话.
1348
1804
  const historyLines = [];
1349
- const historyToInject = this.messageHistory.slice(-20, -1);
1805
+ const historyToInject = this.actor.state.messageHistory.slice(-20, -1);
1350
1806
  for (const m of historyToInject) {
1351
1807
  const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
1352
1808
  const text = (m.content || '').slice(0, 2000);
@@ -1359,9 +1815,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1359
1815
  const onCompact = async () => {
1360
1816
  // no-op (best-effort hook for future pi-sdk/pivot history sync)
1361
1817
  };
1362
- const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
1818
+ const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
1363
1819
  if (result.response) {
1364
- this.messageHistory.push({ role: 'assistant', content: result.response });
1820
+ this.pushHistory({ role: 'assistant', content: result.response });
1365
1821
  }
1366
1822
  // 回溯 + 清场
1367
1823
  if (this.judgmentGateUsedIds.length > 0) {
@@ -1377,7 +1833,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1377
1833
  */
1378
1834
  _harness = null;
1379
1835
  /** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
1380
- currentGoalId = '';
1836
+ // **K5 步骤③/⑧**: 会话的 Goal 默认绑定本体在 `actor.state.goalBinding` (访问器已删, 直接读本体)。
1837
+ // leo 口径不变: 这只是默认值; 每次执行开始必须把最终绑定写进 RunContext/Run 记录,
1838
+ // 运行中重绑必须走显式 Goal Binding 操作, 不许靠裸字段隐式生效。
1381
1839
  /** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
1382
1840
  resumeRunId = '';
1383
1841
  resumePlan = null;
@@ -1400,7 +1858,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1400
1858
  }
1401
1859
  /** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
1402
1860
  setGoalId(goalId) {
1403
- this.currentGoalId = String(goalId || '');
1861
+ this.actor.state.goalBinding = String(goalId || '');
1404
1862
  }
1405
1863
  /** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
1406
1864
  async safeSetRunStatus(runId, to) {
@@ -1419,13 +1877,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1419
1877
  * 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
1420
1878
  */
1421
1879
  async wireToolFailure(tool, errorText, args) {
1422
- if (!this.currentRunId)
1880
+ if (!this.actor.state.activeRun)
1423
1881
  return;
1424
1882
  const cls = (0, run_store_js_1.classifyError)(errorText);
1425
1883
  const digest = (0, run_store_js_1.argsDigestOf)(args);
1426
1884
  let repeats = 1;
1427
1885
  try {
1428
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
1886
+ const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
1429
1887
  // recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
1430
1888
  const seen = rec ? (0, run_store_js_1.repeatedFailureCount)(rec, tool, digest) : 0;
1431
1889
  repeats = Math.max(seen, 1);
@@ -1436,7 +1894,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1436
1894
  : cls === 'external_no_reply' ? 'pause'
1437
1895
  : 'retry';
1438
1896
  try {
1439
- await (0, run_store_js_1.recordRecovery)(this.currentRunId, {
1897
+ await (0, run_store_js_1.recordRecovery)(this.actor.state.activeRun, {
1440
1898
  errorClass: cls,
1441
1899
  message: `${tool}: ${errorText}`.slice(0, 200),
1442
1900
  action,
@@ -1445,20 +1903,20 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1445
1903
  });
1446
1904
  }
1447
1905
  catch (err) {
1448
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.currentRunId, message: String(err?.message || err) }).catch(() => { });
1906
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
1449
1907
  }
1450
1908
  // 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
1451
1909
  if (cls === 'external_no_reply') {
1452
1910
  this.awaitingExternal = true;
1453
- await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
1911
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
1454
1912
  // 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
1455
1913
  try {
1456
- if (this.currentGoalId) {
1914
+ if (this.actor.state.goalBinding) {
1457
1915
  const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await Promise.resolve().then(() => __importStar(require('./external-events.js')));
1458
1916
  const isDelegate = /delegate/i.test(String(tool || ''));
1459
- await bindExternalWait(this.currentGoalId, {
1460
- requestId: this.pendingExternalRequestId || `${this.currentRunId}:${Date.now().toString(36)}`,
1461
- continuationId: newContinuationId(this.currentGoalId),
1917
+ await bindExternalWait(this.actor.state.goalBinding, {
1918
+ requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
1919
+ continuationId: newContinuationId(this.actor.state.goalBinding),
1462
1920
  expectedSource: isDelegate ? 'delegate' : 'p2p',
1463
1921
  expectedEvent: 'result',
1464
1922
  createdAt: new Date().toISOString(),
@@ -1468,31 +1926,31 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1468
1926
  }
1469
1927
  }
1470
1928
  catch (e) {
1471
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.currentRunId, message: String(e?.message || e) }).catch(() => { });
1929
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
1472
1930
  }
1473
1931
  return;
1474
1932
  }
1475
1933
  // 鉴权类: 不重试, 直接交人
1476
1934
  if (cls === 'auth') {
1477
1935
  this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
1478
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1936
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1479
1937
  return;
1480
1938
  }
1481
1939
  // 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
1482
1940
  if (repeats >= MAX_SAME_TOOL_FAILURES) {
1483
1941
  this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
1484
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1942
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1485
1943
  }
1486
1944
  }
1487
1945
  getRunId() {
1488
- return this.currentRunId;
1946
+ return this.actor.state.activeRun;
1489
1947
  }
1490
1948
  /**
1491
1949
  * 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
1492
1950
  * 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
1493
1951
  */
1494
1952
  getLastRunId() {
1495
- return this.lastRunId || this.currentRunId;
1953
+ return this.lastRunId || this.actor.state.activeRun;
1496
1954
  }
1497
1955
  /**
1498
1956
  * 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
@@ -1539,7 +1997,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1539
1997
  }
1540
1998
  this.resumeRunId = runId;
1541
1999
  this.resumePlan = prep.plan;
1542
- this.currentGoalId = prep.plan.goalId || this.currentGoalId;
2000
+ this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
1543
2001
  try {
1544
2002
  const reply = await this.prompt((0, run_store_js_1.buildResumeInstruction)(prep.plan), {});
1545
2003
  return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
@@ -1558,8 +2016,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1558
2016
  // pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
1559
2017
  preToolUse: async (o) => (0, human_value_pipeline_js_1.onPreToolUse)({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
1560
2018
  // 事件写 Run: 观测级 (记账失败不改变已做出的决策)
1561
- events: (e) => { if (this.currentRunId)
1562
- void (0, run_store_js_1.recordHarnessEvent)(this.currentRunId, e); },
2019
+ events: (e) => { if (this.actor.state.activeRun)
2020
+ void (0, run_store_js_1.recordHarnessEvent)(this.actor.state.activeRun, e); },
1563
2021
  });
1564
2022
  }
1565
2023
  return this._harness;
@@ -1567,10 +2025,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1567
2025
  /** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
1568
2026
  harnessCtx() {
1569
2027
  return {
1570
- runId: this.currentRunId || undefined,
1571
- goalId: this.currentGoalId || undefined,
1572
- agentId: this.currentAgentId || undefined,
1573
- channelId: this.currentChannelId || undefined,
2028
+ runId: this.actor.state.activeRun || undefined,
2029
+ goalId: this.actor.state.goalBinding || undefined,
2030
+ agentId: this.actor.state.agentId || undefined,
2031
+ channelId: this.actor.state.channelId || undefined,
1574
2032
  surface: this.runSurface,
1575
2033
  };
1576
2034
  }
@@ -1596,6 +2054,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1596
2054
  const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
1597
2055
  let totalToolCallsThisLoop = 0;
1598
2056
  const lastNTools = []; // 最近 MAX_IDEMPOTENT_TOOL 次工具名, 检测重复
2057
+ // 2026-10-01: 工具停滞观测状态 (分类 + 温和引导 + 重复结果引用) —— 见 agents/tool-loop-guard.ts
2058
+ // 与上面的"同工具 5 次就提示"不同: 它按 (工具名 + 参数 + 结果) 签名判定, 并抓 A→B→A→B 的循环
2059
+ const stallState = new tool_loop_guard_js_1.LoopStallState();
1599
2060
  // 2026-08-10: unreported 循环逃生门 — LLM 反复不把工具结果写进回复时, 3 次后强制 final (不死板)
1600
2061
  const MAX_UNREPORTED_RETRIES = 3;
1601
2062
  let unreportedRetries = 0;
@@ -1634,26 +2095,26 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1634
2095
  this.awaitingExternal = false;
1635
2096
  if (this.resumeRunId) {
1636
2097
  // ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
1637
- this.currentRunId = this.resumeRunId;
2098
+ this.actor.state.activeRun = this.resumeRunId;
1638
2099
  this.lastRunId = this.resumeRunId;
1639
2100
  try {
1640
- await (0, run_store_js_1.markRunRunning)(this.currentRunId);
2101
+ await (0, run_store_js_1.markRunRunning)(this.actor.state.activeRun);
1641
2102
  }
1642
2103
  catch (err) {
1643
2104
  runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
1644
2105
  }
1645
2106
  const doneN = this.resumePlan?.completedSteps.length ?? 0;
1646
2107
  const guards = this.resumePlan?.replayGuards.length ?? 0;
1647
- onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
2108
+ onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.actor.state.activeRun} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
1648
2109
  }
1649
2110
  else {
1650
2111
  // 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
1651
2112
  // 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
1652
2113
  // (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
1653
- let boundGoalId = this.currentGoalId;
2114
+ let boundGoalId = this.actor.state.goalBinding;
1654
2115
  if (!boundGoalId) {
1655
2116
  try {
1656
- const active = await (0, goal_store_js_1.findActiveGoal)({ channelId: this.currentChannelId || undefined, agentId: this.currentAgentId || undefined });
2117
+ const active = await (0, goal_store_js_1.findActiveGoal)({ channelId: this.actor.state.channelId || undefined, agentId: this.actor.state.agentId || undefined });
1657
2118
  if (active?.currentRunId) {
1658
2119
  const prev = await (0, run_store_js_1.readRun)(active.currentRunId);
1659
2120
  const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
@@ -1668,8 +2129,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1668
2129
  try {
1669
2130
  const g = await (0, goal_store_js_1.createGoal)({
1670
2131
  objective: this.currentUserInput || '(未记录目标)',
1671
- channelId: this.currentChannelId || undefined,
1672
- agentId: this.currentAgentId || undefined,
2132
+ channelId: this.actor.state.channelId || undefined,
2133
+ agentId: this.actor.state.agentId || undefined,
1673
2134
  createdBy: this.runSurface,
1674
2135
  });
1675
2136
  boundGoalId = g.goalId;
@@ -1678,25 +2139,25 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1678
2139
  console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
1679
2140
  }
1680
2141
  }
1681
- this.currentGoalId = boundGoalId;
2142
+ this.actor.state.goalBinding = boundGoalId;
1682
2143
  }
1683
2144
  try {
1684
2145
  const rec = await (0, run_store_js_1.startRun)({
1685
2146
  surface: this.runSurface,
1686
2147
  goal: this.currentUserInput || '(未记录目标)',
1687
2148
  goalId: boundGoalId || undefined,
1688
- channelId: this.currentChannelId || undefined,
1689
- agentId: this.currentAgentId || undefined,
2149
+ channelId: this.actor.state.channelId || undefined,
2150
+ agentId: this.actor.state.agentId || undefined,
1690
2151
  modelConfig: await this.runModelSnapshot(),
1691
2152
  });
1692
- this.currentRunId = rec.runId;
2153
+ this.actor.state.activeRun = rec.runId;
1693
2154
  this.lastRunId = rec.runId;
1694
- this.currentGoalId = rec.goalId || this.currentGoalId;
1695
- if (this.currentGoalId) {
2155
+ this.actor.state.goalBinding = rec.goalId || this.actor.state.goalBinding;
2156
+ if (this.actor.state.goalBinding) {
1696
2157
  // Run → Goal 反查链: runId → goalId → objective / success criteria
1697
- await (0, goal_store_js_1.attachRun)(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
2158
+ await (0, goal_store_js_1.attachRun)(this.actor.state.goalBinding, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
1698
2159
  }
1699
- onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
2160
+ onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.actor.state.goalBinding ? `, goal=${this.actor.state.goalBinding}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
1700
2161
  }
1701
2162
  catch (err) {
1702
2163
  // 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
@@ -1726,18 +2187,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1726
2187
  break;
1727
2188
  }
1728
2189
  // 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
1729
- if (this.currentRunId) {
2190
+ if (this.actor.state.activeRun) {
1730
2191
  try {
1731
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
2192
+ const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
1732
2193
  if (!rec)
1733
- throw new Error(`运行记录读不到: ${this.currentRunId}`);
2194
+ throw new Error(`运行记录读不到: ${this.actor.state.activeRun}`);
1734
2195
  // 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
1735
2196
  // 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
1736
2197
  if (rec.status === 'paused' || rec.status === 'aborted') {
1737
2198
  runExternallyPaused = rec.status === 'paused';
1738
2199
  runExternallyAborted = rec.status === 'aborted';
1739
2200
  runStopReason = `外部请求: ${rec.status}`;
1740
- onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.currentRunId})`, tool: 'harness' });
2201
+ onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.actor.state.activeRun})`, tool: 'harness' });
1741
2202
  finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
1742
2203
  break;
1743
2204
  }
@@ -1760,7 +2221,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1760
2221
  }
1761
2222
  // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
1762
2223
  // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
1763
- const maxIterDecision = (0, react_loop_js_1.decideMaxIterations)(iteration, this.MAX_REACT_ITERATIONS);
2224
+ // 2026-10-01 (落实②: 可退还的迭代预算): 惩罚零碎调用, **奖励批处理** ——
2225
+ // 程序化工具(execute_code, 一次能顶多次)调用后归还一次迭代 ⇒ 有效寿命被延长。
2226
+ // 退出条件仍由 decideMaxIterations 决定(读**净**用量), 不做硬刹车。
2227
+ if (iteration === 0 || !this.iterBudget)
2228
+ this.iterBudget = new iteration_budget_js_1.IterationBudget(this.MAX_REACT_ITERATIONS);
2229
+ const iterBudget = this.iterBudget;
2230
+ iterBudget.consume();
2231
+ if (iterBudget.warn(0.8) && !this.iterBudgetWarned) {
2232
+ this.iterBudgetWarned = true;
2233
+ onStream?.({ type: 'status', internal: true, content: `⏳ 迭代预算已用 ${iterBudget.describe()} (批量工具会退还)` });
2234
+ }
2235
+ const maxIterDecision = (0, react_loop_js_1.decideMaxIterations)(iterBudget.used, iterBudget.maxTotal);
1764
2236
  if (maxIterDecision.shouldExit) {
1765
2237
  console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
1766
2238
  onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
@@ -1778,14 +2250,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1778
2250
  if (totalToolCallsThisLoop >= MAX_TOOL_CALLS_PER_LOOP) {
1779
2251
  console.warn(`[PiAgent] 单轮工具调用已达 ${MAX_TOOL_CALLS_PER_LOOP}, 注入 hint 让 LLM 总结`);
1780
2252
  onStream?.({ type: 'error', content: `⏹️ 工具调用已达上限 (${MAX_TOOL_CALLS_PER_LOOP}), 请基于已有结果回答`, tool: 'loop' });
1781
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2253
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
1782
2254
  totalToolCallsThisLoop = 0; // 重置计数器, 只防连续死循环
1783
2255
  }
1784
2256
  if (lastNTools.length >= MAX_IDEMPOTENT_TOOL && new Set(lastNTools).size === 1) {
1785
2257
  const repeatedTool = lastNTools[0];
1786
2258
  console.warn(`[PiAgent] 同工具 ${repeatedTool} 连续成功调 ${MAX_IDEMPOTENT_TOOL} 次, 注入 hint 让 LLM 总结`);
1787
2259
  onStream?.({ type: 'error', content: `⏹️ 工具 ${repeatedTool} 重复调用 ${MAX_IDEMPOTENT_TOOL} 次, 请基于已有结果回答`, tool: 'loop' });
1788
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2260
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
1789
2261
  lastNTools.length = 0; // 重置计数器
1790
2262
  // 不 break — 让 LLM 在下一轮用已有信息回答
1791
2263
  }
@@ -1889,6 +2361,10 @@ ${toolDefs}
1889
2361
 
1890
2362
  ${PiAgentSession.TOOL_SELECTION_GUIDE}
1891
2363
 
2364
+ ${exports.PROACTIVE_WORK_DISCIPLINE}
2365
+
2366
+ ${await this.renderActivePlansSection()}
2367
+
1892
2368
  工作模式:
1893
2369
  1. 理解用户自然语言请求
1894
2370
  2. 分析需要哪些工具来完成
@@ -1984,7 +2460,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1984
2460
  break;
1985
2461
  }
1986
2462
  // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
1987
- this.messageHistory.push({
2463
+ this.pushHistory({
1988
2464
  role: 'system',
1989
2465
  content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
1990
2466
  });
@@ -2057,7 +2533,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2057
2533
  }
2058
2534
  if (toolCalls.length > 0) {
2059
2535
  // 把原始 LLM 回复 push 进 history (仅一次)
2060
- this.messageHistory.push({
2536
+ this.pushHistory({
2061
2537
  role: 'assistant',
2062
2538
  content: reply,
2063
2539
  toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
@@ -2087,12 +2563,12 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2087
2563
  consecutiveErrors++;
2088
2564
  totalErrors++;
2089
2565
  const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
2090
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2566
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2091
2567
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2092
2568
  // 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
2093
2569
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2094
2570
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2095
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) });
2571
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) });
2096
2572
  if (onStream)
2097
2573
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2098
2574
  console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
@@ -2126,7 +2602,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2126
2602
  consecutiveErrors++;
2127
2603
  totalErrors++;
2128
2604
  const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
2129
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2605
+ this.pushHistory({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2130
2606
  this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
2131
2607
  return;
2132
2608
  }
@@ -2138,7 +2614,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2138
2614
  ? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
2139
2615
  : `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
2140
2616
  };
2141
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2617
+ this.pushHistory({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2142
2618
  this.logToHarness(toolCall.name, toolCall.args, deniedResult);
2143
2619
  if (onStream) {
2144
2620
  const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
@@ -2167,13 +2643,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2167
2643
  ? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
2168
2644
  : `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
2169
2645
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2170
- this.messageHistory.push({ role: 'system', content: systemMaxMsg });
2646
+ this.pushHistory({ role: 'system', content: systemMaxMsg });
2171
2647
  lastFailedTool = '';
2172
2648
  lastFailedToolCount = 0;
2173
2649
  consecutiveErrors = 0;
2174
2650
  }
2175
2651
  else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2176
- this.messageHistory.push({ role: 'system', content: systemConsecMsg });
2652
+ this.pushHistory({ role: 'system', content: systemConsecMsg });
2177
2653
  consecutiveErrors = 0;
2178
2654
  }
2179
2655
  return;
@@ -2199,7 +2675,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2199
2675
  }
2200
2676
  let result = replaySkip
2201
2677
  ? { success: true, output: `[恢复保护] ${toolCall.name} 在中断前已成功执行过, 本次不重复执行 (避免重复副作用)。当时结果: ${replaySkip}`, _replaySkipped: true }
2202
- : await tool.execute(toolCall.args);
2678
+ : await (toolCall.__t0 = Date.now(), tool.execute((0, tool_path_args_js_1.expandHomeArgs)(toolCall.args)));
2203
2679
  const toolDurationMs = Date.now() - toolStart;
2204
2680
  if (replaySkip) {
2205
2681
  console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
@@ -2207,18 +2683,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2207
2683
  }
2208
2684
  console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
2209
2685
  // 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
2210
- if (!result.success && this.currentRunId && !replaySkip) {
2686
+ if (!result.success && this.actor.state.activeRun && !replaySkip) {
2211
2687
  await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
2212
2688
  }
2213
- else if (result.success && this.currentRunId && this.awaitingExternal) {
2689
+ else if (result.success && this.actor.state.activeRun && this.awaitingExternal) {
2214
2690
  // 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
2215
2691
  this.awaitingExternal = false;
2216
- await this.safeSetRunStatus(this.currentRunId, 'running');
2692
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'running');
2217
2693
  }
2218
2694
  // 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
2219
- if (this.currentRunId) {
2695
+ if (this.actor.state.activeRun) {
2220
2696
  try {
2221
- await (0, run_store_js_1.recordStep)(this.currentRunId, {
2697
+ await (0, run_store_js_1.recordStep)(this.actor.state.activeRun, {
2222
2698
  tool: toolCall.name,
2223
2699
  ok: !!result.success,
2224
2700
  ms: toolDurationMs,
@@ -2249,7 +2725,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2249
2725
  ok: !!result.success,
2250
2726
  });
2251
2727
  if (after.routeHint?.systemAddition) {
2252
- this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2728
+ this.pushHistory({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2253
2729
  }
2254
2730
  if (after.outputBlocked) {
2255
2731
  if (onStream) {
@@ -2258,7 +2734,39 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2258
2734
  console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${after.outputBlocked.reason}`);
2259
2735
  result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${after.outputBlocked.reason}]`, _harnessDenied: true };
2260
2736
  }
2261
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2737
+ // 2026-10-01: 落库前做一次停滞观测 —— 命中的是"引导/引用", 不是"拒绝执行":
2738
+ // 完全相同的返回从第 2 次起折叠成引用 (省上下文); 连续 3 次同参数同结果或检测到循环 ⇒
2739
+ // 在结果尾部追加一条系统提示 (保留模型的选择权, 不硬停 —— 硬停那条已被用户否决)。
2740
+ try {
2741
+ const obs = (0, tool_loop_guard_js_1.observeToolCall)(stallState, {
2742
+ toolName: toolCall.name,
2743
+ sameSignatureBefore: this.lastToolSig === (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args),
2744
+ args: toolCall.args,
2745
+ resultText: String(result.output || (result.success ? '' : String(result.error || ''))),
2746
+ ok: !!result.success,
2747
+ seenResultBefore: stallState.hasSeenResult(String(result.output || '')),
2748
+ });
2749
+ if (obs.stub) {
2750
+ result = { ...result, output: obs.stub, _stubbed: true };
2751
+ }
2752
+ else if (obs.notice) {
2753
+ result = { ...result, output: `${String(result.output || '')}\n\n${obs.notice}` };
2754
+ }
2755
+ if (obs.action === 'warn') {
2756
+ console.warn(`[PiAgent] 工具停滞引导 (${obs.code}, 第 ${obs.count} 次): ${toolCall.name}`);
2757
+ onStream?.({ type: 'status', internal: true, content: `🩺 检测到重复调用 ${toolCall.name} (${obs.code}), 已提示模型换法`, tool: 'loop' });
2758
+ }
2759
+ }
2760
+ catch { /* 观测失败绝不影响主路径 */ }
2761
+ this.pushHistory({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2762
+ // 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
2763
+ // 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
2764
+ // 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
2765
+ if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
2766
+ const __batchHint = (0, tool_loop_guard_js_1.batchHint)(toolCall.name, 1);
2767
+ if (__batchHint)
2768
+ this.pushHistory({ role: 'system', content: __batchHint.trim() });
2769
+ }
2262
2770
  this.logToHarness(toolCall.name, toolCall.args, result);
2263
2771
  // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
2264
2772
  // 去重: 同一工具同 args 连续成功只记一次 (防 LLM 重复 react 刷屏)
@@ -2287,10 +2795,48 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2287
2795
  onStream({ type: 'step_error', content: `${toolCall.name} 执行失败`, tool: toolCall.name, error: result.error });
2288
2796
  }
2289
2797
  }
2798
+ // 2026-10-01 (优化 #1/#3/#4): 工具结果的**进上下文闸** + **遥测** + **写操作读回自证**
2799
+ {
2800
+ const __ms = Date.now() - Number(toolCall.__t0 || Date.now());
2801
+ try {
2802
+ // #1 结果闸: 超上限 ⇒ 头尾 + 完整结果落文件(给路径) ⇒ 上下文不再被一条大输出长期占住
2803
+ const capped = (0, tool_result_gate_js_1.capToolResult)(String(result.output ?? ''), { tool: toolCall.name });
2804
+ if (capped.capped) {
2805
+ result.output = capped.text;
2806
+ console.warn(`[PiAgent] ${toolCall.name} 结果过长(${capped.originalChars})已截断` + (capped.spilledTo ? `, 完整内容: ${capped.spilledTo}` : ''));
2807
+ }
2808
+ // #4 写操作"读回自证": 写类工具成功后自动核一次(存在? 大小?) ⇒ "工具说成功≠任务成功"从规矩变成机制
2809
+ // 2026-10-01: 改了 TS 源码就记账 ⇒ 回合收尾自动跑类型检查 (把"靠自觉"变成机制)
2810
+ {
2811
+ const tsFile = (0, code_write_gate_js_1.codeWriteTarget)(toolCall.name, toolCall.args);
2812
+ if (tsFile && !this.tsTouchedThisTurn.includes(tsFile))
2813
+ this.tsTouchedThisTurn.push(tsFile);
2814
+ }
2815
+ const verified = verifyWriteOutcome(toolCall.name, toolCall.args, this.cwd);
2816
+ if (verified)
2817
+ result.output = `${String(result.output ?? '')}\n${verified}`;
2818
+ // #3 遥测: 记一行(工具/指纹/耗时/成败/结果大小/是否与上一次同签名) ⇒ 重复率可算
2819
+ (0, tool_telemetry_js_1.recordToolCall)({
2820
+ tool: toolCall.name,
2821
+ sig: (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args),
2822
+ ms: __ms,
2823
+ ok: true,
2824
+ resultChars: String(result.output ?? '').length,
2825
+ prevSig: this.lastToolSig ?? null,
2826
+ });
2827
+ this.lastToolSig = (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args);
2828
+ }
2829
+ catch { /* 任何优化项失败都不影响工具结果本身 */ }
2830
+ }
2290
2831
  if (result.success) {
2291
2832
  consecutiveErrors = 0;
2292
2833
  // 2026-07-29: Hermes 风格硬限制计数
2293
2834
  totalToolCallsThisLoop++;
2835
+ // 2026-10-01: 批处理工具(一次顶多次) ⇒ **退还**这次迭代 (奖励批处理, 压零碎调用)
2836
+ if (this.iterBudget && (0, iteration_budget_js_1.isRefundableTool)(toolCall.name)) {
2837
+ this.iterBudget.refund();
2838
+ console.warn(`[PiAgent] ${toolCall.name} 是批量工具 ⇒ 退还 1 次迭代 (现 ${this.iterBudget.describe()})`);
2839
+ }
2294
2840
  lastNTools.push(toolCall.name);
2295
2841
  if (lastNTools.length > MAX_IDEMPOTENT_TOOL)
2296
2842
  lastNTools.shift();
@@ -2311,6 +2857,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2311
2857
  }
2312
2858
  else {
2313
2859
  consecutiveErrors++;
2860
+ // 2026-10-01 (优化 #3): 失败也记一行(带错误类别) ⇒ "错工具率"从这类错误里看得出来
2861
+ try {
2862
+ const __cls = (0, error_classifier_js_1.classifyError)(String(result.error || '')).label || '未分类';
2863
+ (0, tool_telemetry_js_1.recordToolCall)({ tool: toolCall.name, sig: (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args), ms: 0,
2864
+ ok: false, resultChars: 0, prevSig: this.lastToolSig ?? null, errorClass: __cls });
2865
+ this.lastToolSig = (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args);
2866
+ }
2867
+ catch { /* 遥测失败不影响 */ }
2314
2868
  totalErrors++;
2315
2869
  if (toolCall.name === lastFailedTool) {
2316
2870
  lastFailedToolCount++;
@@ -2323,18 +2877,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2323
2877
  // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
2324
2878
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, { success: false, error: result.error });
2325
2879
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, result.error, totalErrors, lastFailedToolCount);
2326
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2880
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2327
2881
  if (onStream)
2328
2882
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
2329
2883
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2330
- this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2884
+ this.pushHistory({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2331
2885
  lastFailedTool = '';
2332
2886
  lastFailedToolCount = 0;
2333
2887
  consecutiveErrors = 0;
2334
2888
  return;
2335
2889
  }
2336
2890
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2337
- this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2891
+ this.pushHistory({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2338
2892
  consecutiveErrors = 0;
2339
2893
  }
2340
2894
  }
@@ -2343,11 +2897,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2343
2897
  consecutiveErrors++;
2344
2898
  totalErrors++;
2345
2899
  const errorResult = { success: false, error: String(execError) };
2346
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2900
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2347
2901
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2348
2902
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2349
2903
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2350
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2904
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2351
2905
  if (onStream)
2352
2906
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2353
2907
  console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
@@ -2358,7 +2912,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2358
2912
  }
2359
2913
  else {
2360
2914
  // LLM 返回的不是 tool call 格式
2361
- this.messageHistory.push({
2915
+ this.pushHistory({
2362
2916
  role: 'assistant',
2363
2917
  content: reply,
2364
2918
  reasoningContent: response?.reasoningContent,
@@ -2387,7 +2941,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2387
2941
  unreportedRetries++;
2388
2942
  const unreported = this.successfulToolResults.length;
2389
2943
  console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
2390
- this.messageHistory.push({
2944
+ this.pushHistory({
2391
2945
  role: 'system',
2392
2946
  content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
2393
2947
  });
@@ -2400,7 +2954,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2400
2954
  // 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
2401
2955
  console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
2402
2956
  this.successfulToolResults = [];
2403
- this.messageHistory.push({
2957
+ this.pushHistory({
2404
2958
  role: 'system',
2405
2959
  content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
2406
2960
  });
@@ -2412,7 +2966,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2412
2966
  // 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
2413
2967
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
2414
2968
  console.log(`[PiAgent] final gen 质量 ${lastQualityScore.toFixed(2)} < ${this.QUALITY_THRESHOLD}, 注入 refine hint`);
2415
- this.messageHistory.push({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2969
+ this.pushHistory({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2416
2970
  refineAttempts++;
2417
2971
  continue;
2418
2972
  }
@@ -2427,13 +2981,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2427
2981
  userIntent: this.currentUserInput,
2428
2982
  completedTools: Array.from(loopReviewCompletedTools),
2429
2983
  actionLog: loopActionLog,
2430
- runId: this.currentRunId || undefined,
2431
- goalId: this.currentGoalId || undefined,
2984
+ runId: this.actor.state.activeRun || undefined,
2985
+ goalId: this.actor.state.goalBinding || undefined,
2432
2986
  }, loop_review_js_1.DEFAULT_MAX_REVIEWS);
2433
2987
  if (reviewDecision.kind === 'continue-review') {
2434
2988
  loopReviewCount++;
2435
2989
  console.log(`[PiAgent] review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
2436
- this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
2990
+ this.pushHistory({ role: 'system', content: reviewDecision.hint });
2437
2991
  if (onStream) {
2438
2992
  onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
2439
2993
  }
@@ -2486,16 +3040,16 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2486
3040
  if (onStream) {
2487
3041
  onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2488
3042
  }
2489
- this.messageHistory.push({ role: 'assistant', content: finalResponse });
3043
+ this.pushHistory({ role: 'assistant', content: finalResponse });
2490
3044
  // React Harness: 循环结束
2491
3045
  // 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
2492
3046
  await this.piHarness().sessionEnd(this.harnessCtx());
2493
3047
  // 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
2494
- if (this.currentRunId && !runExternallyPaused && !runExternallyAborted) {
3048
+ if (this.actor.state.activeRun && !runExternallyPaused && !runExternallyAborted) {
2495
3049
  try {
2496
3050
  // 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
2497
3051
  // 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
2498
- const recBefore = await (0, run_store_js_1.readRun)(this.currentRunId).catch(() => null);
3052
+ const recBefore = await (0, run_store_js_1.readRun)(this.actor.state.activeRun).catch(() => null);
2499
3053
  const steps = recBefore?.steps || [];
2500
3054
  const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
2501
3055
  const lastStep = steps[steps.length - 1];
@@ -2516,7 +3070,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2516
3070
  : (trailingFailure || noEvidence)
2517
3071
  ? 'failed'
2518
3072
  : 'done';
2519
- await (0, run_store_js_1.finishRun)(this.currentRunId, {
3073
+ await (0, run_store_js_1.finishRun)(this.actor.state.activeRun, {
2520
3074
  status,
2521
3075
  summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
2522
3076
  error: errText
@@ -2529,7 +3083,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2529
3083
  // 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
2530
3084
  const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
2531
3085
  console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
2532
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
3086
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.actor.state.activeRun, message }).catch(() => { });
2533
3087
  onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
2534
3088
  }
2535
3089
  // ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
@@ -2541,14 +3095,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2541
3095
  //
2542
3096
  // 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
2543
3097
  // 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
2544
- if (this.currentGoalId) {
3098
+ if (this.actor.state.goalBinding) {
2545
3099
  try {
2546
3100
  const wiring = await Promise.resolve().then(() => __importStar(require('./goal-flywheel-wiring.js')));
2547
3101
  const { reduceGoalState } = await Promise.resolve().then(() => __importStar(require('./goal-state-reducer.js')));
2548
3102
  const nowIso = new Date().toISOString();
2549
3103
  const closed = await wiring.closeRunOnce({
2550
- goalId: this.currentGoalId,
2551
- runId: this.currentRunId,
3104
+ goalId: this.actor.state.goalBinding,
3105
+ runId: this.actor.state.activeRun,
2552
3106
  caller: 'runner',
2553
3107
  now: nowIso,
2554
3108
  finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
@@ -2559,7 +3113,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2559
3113
  else if (closed.outcome) {
2560
3114
  const view = closed.outcome;
2561
3115
  const applied = await reduceGoalState({
2562
- goalId: this.currentGoalId,
3116
+ goalId: this.actor.state.goalBinding,
2563
3117
  intent: 'closure_outcome',
2564
3118
  now: nowIso,
2565
3119
  by: 'runner',
@@ -2576,7 +3130,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2576
3130
  onStream?.({
2577
3131
  type: 'status', internal: true,
2578
3132
  content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
2579
- + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
3133
+ + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.actor.state.goalBinding})`,
2580
3134
  tool: 'harness',
2581
3135
  });
2582
3136
  }
@@ -2586,15 +3140,15 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2586
3140
  }
2587
3141
  }
2588
3142
  catch (err) {
2589
- await (0, run_store_js_1.recordDegradation)({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
3143
+ await (0, run_store_js_1.recordDegradation)({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.actor.state.activeRun, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
2590
3144
  }
2591
3145
  }
2592
- this.currentRunId = '';
3146
+ this.actor.state.activeRun = '';
2593
3147
  }
2594
- else if (this.currentRunId) {
3148
+ else if (this.actor.state.activeRun) {
2595
3149
  // 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
2596
3150
  onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
2597
- this.currentRunId = '';
3151
+ this.actor.state.activeRun = '';
2598
3152
  }
2599
3153
  // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
2600
3154
  return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
@@ -2648,7 +3202,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2648
3202
  // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
2649
3203
  //
2650
3204
  // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
2651
- const source = this.projectedHistory ?? this.messageHistory;
3205
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
2652
3206
  const recentMessages = this.compressHistorySync(source).slice(-10);
2653
3207
  return recentMessages.map(m => {
2654
3208
  if (m.role === 'user')
@@ -2675,7 +3229,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2675
3229
  // 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
2676
3230
  // 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
2677
3231
  // 导致 LLM 实际看到的还是未压缩的历史.
2678
- const source = this.projectedHistory ?? this.messageHistory;
3232
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
2679
3233
  const WINDOW = 15;
2680
3234
  const out = [];
2681
3235
  // 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
@@ -2754,11 +3308,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2754
3308
  /**
2755
3309
  * 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
2756
3310
  * 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
2757
- * 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3311
+ * 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
2758
3312
  */
2759
3313
  writeBackCurrentTurn(wire) {
2760
3314
  try {
2761
- return writeBackCurrentTurnInto(this.messageHistory, wire);
3315
+ return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
2762
3316
  }
2763
3317
  catch (err) {
2764
3318
  console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
@@ -2772,7 +3326,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2772
3326
  estimateHistoryTokens() {
2773
3327
  try {
2774
3328
  const { estimateTokens } = _piRequire('../context-compaction/index.js');
2775
- return estimateTokens(this.messageHistory);
3329
+ return estimateTokens(this.actor.state.messageHistory);
2776
3330
  }
2777
3331
  catch {
2778
3332
  return 0;
@@ -2795,8 +3349,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2795
3349
  console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
2796
3350
  onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
2797
3351
  try {
2798
- const compacted = this.compressHistorySync(this.messageHistory);
2799
- this.messageHistory = compacted;
3352
+ // 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
3353
+ // 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
3354
+ const compacted = this.compressHistorySync(this.actor.state.messageHistory);
3355
+ this.replaceHistory(compacted);
2800
3356
  if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
2801
3357
  await this.maybeAutoCompact(onStream, signal);
2802
3358
  }
@@ -2847,7 +3403,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2847
3403
  // currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
2848
3404
  // 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
2849
3405
  const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
2850
- // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
3406
+ // 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
3407
+ // **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
3408
+ try {
3409
+ const rc = String(response?.reasoningContent || '').trim();
3410
+ if (rc)
3411
+ this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
3412
+ }
3413
+ catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
2851
3414
  return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
2852
3415
  }
2853
3416
  catch (err) {
@@ -2885,8 +3448,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2885
3448
  if (Array.isArray(contextOrMessages)) {
2886
3449
  contextOrMessages = this.buildContext();
2887
3450
  // 也清除最近一轮的 toolCalls, 防止再触发
2888
- if (this.messageHistory.length > 1) {
2889
- const last = this.messageHistory[this.messageHistory.length - 1];
3451
+ if (this.actor.state.messageHistory.length > 1) {
3452
+ const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
2890
3453
  if (last.role === 'assistant' && last.toolCalls) {
2891
3454
  delete last.toolCalls;
2892
3455
  }
@@ -2949,7 +3512,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2949
3512
  * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
2950
3513
  */
2951
3514
  async maybeAutoCompact(onStream, signal) {
2952
- if (this.messageHistory.length < 10)
3515
+ if (this.actor.state.messageHistory.length < 10)
2953
3516
  return; // 历史太短, 不值得压
2954
3517
  onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
2955
3518
  // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
@@ -2968,14 +3531,17 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2968
3531
  const maxTokens = Math.max(4000, Math.round(cfg.maxTokens * cfg.compressionThreshold));
2969
3532
  const beforeTokens = this.estimateHistoryTokens();
2970
3533
  const { compactPipeline, isContextCollapseEnabled } = await Promise.resolve().then(() => __importStar(require('../context-compaction/index.js')));
2971
- const result = await compactPipeline(this.messageHistory, {
3534
+ // **K5 第 4 步**: 记下取快照时的长度 —— 压缩是 async, 从这一拍到 "落地" 之间是 await 窗口;
3535
+ // 期间 append 进来的消息必须由 `actor.rebaseHistory(…, snapshotLen)` 接回去, 否则会被整块替换丢掉。
3536
+ const snapshotLen = this.actor.state.messageHistory.length;
3537
+ const result = await compactPipeline(this.actor.state.messageHistory, {
2972
3538
  maxTokens,
2973
3539
  llmChat,
2974
3540
  collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
2975
- cacheScope: this.currentChannelId || 'default',
3541
+ cacheScope: this.actor.state.channelId || 'default',
2976
3542
  });
2977
- if (result.compacted && result.history.length < this.messageHistory.length) {
2978
- const saved = this.messageHistory.length - result.history.length;
3543
+ if (result.compacted && result.history.length < this.actor.state.messageHistory.length) {
3544
+ const saved = this.actor.state.messageHistory.length - result.history.length;
2979
3545
  const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
2980
3546
  const afterTokens = this.estimateHistoryTokens();
2981
3547
  const savedTokens = Math.max(0, beforeTokens - afterTokens);
@@ -2993,7 +3559,17 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2993
3559
  // messageHistory 不变 (非破坏)
2994
3560
  }
2995
3561
  else {
2996
- this.messageHistory = result.history; // 真破坏性更新
3562
+ // 真破坏性更新 —— **K5 第 4 步**: 绑定了 actor 就交它落地 (rebase: 保住变换期间的追加);
3563
+ // 未绑定的会话走原路径 (行为不变)。
3564
+ if (this.actor) {
3565
+ const { keptTail } = await this.actor.rebaseHistory(result.history, snapshotLen);
3566
+ if (keptTail > 0) {
3567
+ console.warn(`[PiAgent] 压缩落地: 保住变换期间新追加的 ${keptTail} 条消息 (快照后到达)`);
3568
+ }
3569
+ }
3570
+ else {
3571
+ this.replaceHistory(result.history);
3572
+ }
2997
3573
  this.projectedHistory = null;
2998
3574
  }
2999
3575
  // 2026-08-06: snapshot 记录 before/after + 摘要 (供恢复/调试/UI), 事件广播
@@ -3004,10 +3580,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
3004
3580
  afterTokens,
3005
3581
  summary: `压缩管道: ${summaryLine}; 节省 ${savedTokens} tokens / ${saved} 条消息`,
3006
3582
  preservedMemory: [
3007
- ...this.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3583
+ ...this.actor.state.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3008
3584
  ],
3009
- agentId: this.currentAgentId,
3010
- channelId: this.currentChannelId,
3585
+ agentId: this.actor.state.agentId,
3586
+ channelId: this.actor.state.channelId,
3011
3587
  });
3012
3588
  cm.markCompressComplete(snap);
3013
3589
  }
@@ -3393,10 +3969,16 @@ ${this.extractOperationsFromRef(operationsRef)}
3393
3969
  return { ...this.identity };
3394
3970
  }
3395
3971
  updateIdentity(updates) {
3396
- this.identity = { ...this.identity, ...updates };
3972
+ // 2026-10-01 **修切换频道后身份不换的根因**: 上一版是 `this.identity = { ...this.identity, ...updates }`
3973
+ // —— **换成新对象** ✗。而工具上下文是在 registerTools() 时按**对象引用**捕获的 (`identity: this.identity`),
3974
+ // 并且切频道时 session 是**复用**的(pi-sdk-session-factory 的模块级单例, 只调 updateIdentity) ⇒
3975
+ // 工具里的 ctx.identity 一直指向**旧对象** ⇒ `get_identity` 永远返回上一个频道的身份 ✗✗
3976
+ // (实测: xiaomi/智能体频道都答「233 的 DID + 小龙」, 因为 233 是进程启动时的频道)。
3977
+ // ⇒ 改成**原地改**: 所有持有者(工具上下文/会话/状态栏)都看到同一个对象的新值 ✓。
3978
+ Object.assign(this.identity, updates);
3397
3979
  }
3398
3980
  setCurrentChannelId(channelId) {
3399
- this.currentChannelId = channelId;
3981
+ this.actor.state.channelId = channelId;
3400
3982
  }
3401
3983
  getSessionState() {
3402
3984
  return this.sessionManager.getState();
@@ -3409,6 +3991,17 @@ ${this.extractOperationsFromRef(operationsRef)}
3409
3991
  }
3410
3992
  async setPersona(persona) {
3411
3993
  await this.sessionManager.savePersona(persona);
3994
+ // 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
3995
+ // (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
3996
+ if (this.actor.state.agentId) {
3997
+ try {
3998
+ const { applyPersonaToDocs } = await Promise.resolve().then(() => __importStar(require('../bootstrap/persona-init.js')));
3999
+ const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
4000
+ if (wrote.length)
4001
+ console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
4002
+ }
4003
+ catch { /* 非致命 */ }
4004
+ }
3412
4005
  this.persona = persona;
3413
4006
  if (persona.name) {
3414
4007
  this.identity.name = persona.name;