@bolloon/bolloon-agent 0.5.3 → 0.5.5

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 (144) 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 +38 -32
  14. package/dist/agents/pi-sdk-session-manager.js +72 -3
  15. package/dist/agents/pi-sdk-tools.js +268 -47
  16. package/dist/agents/pi-sdk.js +470 -22
  17. package/dist/agents/plan-store.js +36 -0
  18. package/dist/agents/process-runner.js +64 -2
  19. package/dist/agents/session-store.js +144 -3
  20. package/dist/agents/shell-guard.js +109 -13
  21. package/dist/agents/skill-health.js +90 -0
  22. package/dist/agents/skill-ledger.js +129 -0
  23. package/dist/agents/tool-loop-guard.js +303 -0
  24. package/dist/agents/tool-path-args.js +53 -0
  25. package/dist/agents/tool-result-gate.js +50 -0
  26. package/dist/agents/tool-subset.js +109 -0
  27. package/dist/agents/tool-telemetry.js +97 -0
  28. package/dist/agents/wallet-tools.js +63 -0
  29. package/dist/bootstrap/context-collector.js +37 -7
  30. package/dist/bootstrap/lifecycle-hooks.js +12 -3
  31. package/dist/bootstrap/persona-init.js +174 -0
  32. package/dist/bootstrap/persona-loader.js +5 -1
  33. package/dist/cli/ink-app.js +231 -32
  34. package/dist/cli/input-history.js +119 -0
  35. package/dist/cli/input-paste.js +218 -0
  36. package/dist/cli/loading-tui.js +69 -7
  37. package/dist/cli/log-gate.js +46 -1
  38. package/dist/cli/mention-data.js +30 -0
  39. package/dist/cli/reasoning-view.js +103 -0
  40. package/dist/cli/session-summary.js +129 -0
  41. package/dist/cli/session-tree.js +141 -0
  42. package/dist/cli/startup-notice.js +25 -0
  43. package/dist/cli/status-segments.js +155 -0
  44. package/dist/cli/stores.js +16 -0
  45. package/dist/cli/trace-line.js +54 -0
  46. package/dist/cli/wallet-store.js +159 -0
  47. package/dist/cli/wallet-table.js +46 -0
  48. package/dist/cli-entry.js +22 -0
  49. package/dist/context-compaction/auto-compact.js +37 -2
  50. package/dist/electron-build/agents/agent-identity-store.js +43 -5
  51. package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
  52. package/dist/electron-build/agents/background-notices.js +138 -0
  53. package/dist/electron-build/agents/background-notices.js.map +1 -0
  54. package/dist/electron-build/agents/channel-identity.js +64 -0
  55. package/dist/electron-build/agents/channel-identity.js.map +1 -0
  56. package/dist/electron-build/agents/code-write-gate.js +55 -0
  57. package/dist/electron-build/agents/code-write-gate.js.map +1 -0
  58. package/dist/electron-build/agents/deny-pipeline.js +11 -2
  59. package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
  60. package/dist/electron-build/agents/egress-sanitize.js +76 -0
  61. package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
  62. package/dist/electron-build/agents/error-classifier.js +68 -5
  63. package/dist/electron-build/agents/error-classifier.js.map +1 -1
  64. package/dist/electron-build/agents/experience-review.js +243 -0
  65. package/dist/electron-build/agents/experience-review.js.map +1 -0
  66. package/dist/electron-build/agents/iteration-budget.js +73 -0
  67. package/dist/electron-build/agents/iteration-budget.js.map +1 -0
  68. package/dist/electron-build/agents/lesson-to-skill.js +228 -0
  69. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
  70. package/dist/electron-build/agents/managed-services.js +69 -0
  71. package/dist/electron-build/agents/managed-services.js.map +1 -0
  72. package/dist/electron-build/agents/parse-tool-call.js +38 -32
  73. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  74. package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
  75. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  76. package/dist/electron-build/agents/pi-sdk-tools.js +268 -46
  77. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  78. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  79. package/dist/electron-build/agents/pi-sdk.js +471 -22
  80. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  81. package/dist/electron-build/agents/plan-store.js +37 -0
  82. package/dist/electron-build/agents/plan-store.js.map +1 -1
  83. package/dist/electron-build/agents/process-runner.js +71 -2
  84. package/dist/electron-build/agents/process-runner.js.map +1 -1
  85. package/dist/electron-build/agents/session-store.js +144 -3
  86. package/dist/electron-build/agents/session-store.js.map +1 -1
  87. package/dist/electron-build/agents/shell-guard.js +110 -14
  88. package/dist/electron-build/agents/shell-guard.js.map +1 -1
  89. package/dist/electron-build/agents/skill-health.js +103 -0
  90. package/dist/electron-build/agents/skill-health.js.map +1 -0
  91. package/dist/electron-build/agents/skill-ledger.js +146 -0
  92. package/dist/electron-build/agents/skill-ledger.js.map +1 -0
  93. package/dist/electron-build/agents/tool-loop-guard.js +319 -0
  94. package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
  95. package/dist/electron-build/agents/tool-path-args.js +63 -0
  96. package/dist/electron-build/agents/tool-path-args.js.map +1 -0
  97. package/dist/electron-build/agents/tool-result-gate.js +59 -0
  98. package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
  99. package/dist/electron-build/agents/tool-subset.js +116 -0
  100. package/dist/electron-build/agents/tool-subset.js.map +1 -0
  101. package/dist/electron-build/agents/tool-telemetry.js +108 -0
  102. package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
  103. package/dist/electron-build/agents/wallet-tools.js +102 -0
  104. package/dist/electron-build/agents/wallet-tools.js.map +1 -0
  105. package/dist/electron-build/bootstrap/context-collector.js +38 -7
  106. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  107. package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
  108. package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
  109. package/dist/electron-build/bootstrap/persona-init.js +216 -0
  110. package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
  111. package/dist/electron-build/bootstrap/persona-loader.js +5 -1
  112. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  113. package/dist/electron-build/cli/wallet-store.js +200 -0
  114. package/dist/electron-build/cli/wallet-store.js.map +1 -0
  115. package/dist/electron-build/context-compaction/auto-compact.js +38 -2
  116. package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
  117. package/dist/electron-build/external-engines/delegate.js +22 -0
  118. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  119. package/dist/electron-build/llm/pi-ai.js +25 -0
  120. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  121. package/dist/electron-build/orbitdb/cid-database.js +55 -0
  122. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  123. package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
  124. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  125. package/dist/electron-build/web/server-storage.js +26 -5
  126. package/dist/electron-build/web/server-storage.js.map +1 -1
  127. package/dist/external-engines/delegate.js +21 -0
  128. package/dist/index.js +1324 -173
  129. package/dist/llm/pi-ai.js +23 -0
  130. package/dist/orbitdb/cid-database.js +55 -0
  131. package/dist/orbitdb/event-index.js +370 -0
  132. package/dist/orbitdb/event-query.js +131 -0
  133. package/dist/orbitdb/event-shell.js +510 -0
  134. package/dist/orbitdb/group-access.js +381 -0
  135. package/dist/orbitdb/group-shards.js +140 -0
  136. package/dist/orbitdb/ipfs-node.js +36 -11
  137. package/dist/web/bolloon-web.json +3 -3
  138. package/dist/web/client.js +3 -2
  139. package/dist/web/mobile-core.js +7762 -6289
  140. package/dist/web/routes-mobile-tasks.js +2 -1
  141. package/dist/web/server-storage.js +26 -5
  142. package/dist/web/server.js +2 -0
  143. package/dist/web/sw.js +1 -1
  144. package/package.json +2 -1
@@ -9,9 +9,26 @@
9
9
  * - factory → ./pi-sdk-session-factory.ts (createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop)
10
10
  * - 本文件 (PiAgentSession 类: LLM 循环 / 系统提示 / 工具调用分发 / 压缩 / persistence)
11
11
  */
12
+ import * as fs from 'fs/promises';
12
13
  import * as fsSync from 'fs';
13
14
  import * as os from 'os';
14
15
  import * as path from 'path';
16
+ import { expandHomeArgs } from './tool-path-args.js';
17
+ import { renderDelegateNotices, pushNotice, renderNoticeBlock } from './background-notices.js';
18
+ import { runWithWriteOrigin } from './skill-ledger.js';
19
+ import { createHash } from 'node:crypto';
20
+ import { shouldReviewTask, runExperienceReview } from './experience-review.js';
21
+ import { decideLessonSink, skillsFromDirs, logLessonSuggestions, routeLessonToSkill } from './lesson-to-skill.js';
22
+ import { managedServices } from './managed-services.js';
23
+ import { IterationBudget, isRefundableTool } from './iteration-budget.js';
24
+ import { recordToolCall, argsFingerprint } from './tool-telemetry.js';
25
+ import { capToolResult } from './tool-result-gate.js';
26
+ import { renderToolListWithParams } from './tool-subset.js';
27
+ import { codeWriteTarget, decideTypecheck, formatTypecheckResult } from './code-write-gate.js';
28
+ // 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
29
+ // require 是 undefined ⇒ 抛错被 catch 静默吞掉 ⇒ "自愈"根本没跑, DID 一直是空的 ✗。
30
+ import { loadOrCreateAgentIdentity } from './agent-identity.js';
31
+ import { agentPersonaName } from './channel-identity.js';
15
32
  import { getContextManager } from '../bootstrap/context-manager.js';
16
33
  import { createRequire } from 'module';
17
34
  import { currentPackageRoot } from '../utils/version-info.js';
@@ -68,7 +85,7 @@ import { ReactHarness } from '../security/react-harness.js';
68
85
  import { HooksEngine } from '../hooks/hooks-engine.js';
69
86
  import { DenyPipeline } from './deny-pipeline.js';
70
87
  import { parseToolCall as parseToolCallImpl, parseAllToolCalls, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
71
- import { buildObservation, buildReflection, formatObservationWithReflection } from './error-classifier.js';
88
+ import { buildObservation, buildReflection, formatObservationWithReflection, classifyError } from './error-classifier.js';
72
89
  import { sessionStore as defaultSessionStore } from './session-store.js';
73
90
  import { ToolRegistry } from './tool-registry.js';
74
91
  import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
@@ -130,6 +147,62 @@ export function writeBackCurrentTurnInto(history, wire) {
130
147
  return 0; // 写回失败绝不影响对话本身
131
148
  }
132
149
  }
150
+ import { LoopStallState, observeToolCall, batchHint } from './tool-loop-guard.js';
151
+ /**
152
+ * 写操作"读回自证" (2026-10-01 优化 #4): 写类工具成功后**自动**核一次, 把事实拼进结果。
153
+ * 为什么: 过程纪律写了"读回一次", 但**没人执行** ✗ ⇒ 现在由机制执行: "工具说成功 ≠ 任务成功"。
154
+ * 只做**便宜**的核对(存在性/大小); 失败静默(核对不该把工具搞失败)。
155
+ */
156
+ export function verifyWriteOutcome(toolName, args, cwd) {
157
+ try {
158
+ const fsMod = require('node:fs');
159
+ const pathMod = require('node:path');
160
+ const WRITE = new Set(['write_file', 'edit_file', 'mkdir', 'move_file', 'copy_file']);
161
+ if (!WRITE.has(String(toolName)))
162
+ return null;
163
+ const rel = String(args?.path ?? args?.to ?? args?.dir ?? '').trim();
164
+ if (!rel)
165
+ return null;
166
+ const abs = pathMod.isAbsolute(rel) ? rel : pathMod.resolve(cwd, rel);
167
+ const st = fsMod.statSync(abs);
168
+ if (st.isDirectory())
169
+ return `[已核对] 目录存在: ${rel}`;
170
+ return `[已核对] 文件已落盘: ${rel} (${st.size} 字节, ${new Date(st.mtimeMs).toISOString()})`;
171
+ }
172
+ catch (e) {
173
+ return `[未核对] 读回失败: ${String(e?.message || e).slice(0, 80)} —— 别急着说"已完成", 先确认路径/权限`;
174
+ }
175
+ }
176
+ /**
177
+ * 过程纪律 (2026-10-01, 用户: 「智能体回复方式没有主动性 … 在过程里面更加主动考虑」)。
178
+ *
179
+ * 诊断: 系统提示里只有"理解→分析→调用→观察"这种**反应式**循环描述 ⇒ 模型容易"问一句答一句、
180
+ * 试一次就收尾、把方案当交付"。主动不是靠一句"请主动"能给的, 要落成**可判定的规矩**。
181
+ * 落地方式 (7 条, 都是"做/不做"能验收的): 先动手别先问 · 做到底别停在半成品 · 一个工具不够就换法继续 ·
182
+ * 工具说成功 ≠ 任务成功(读回验证) · 被阻塞如实说绝不编 · 每轮要么推进要么交付 · 顺手想影响面并给下一步建议。
183
+ */
184
+ export const PROACTIVE_WORK_DISCIPLINE = `
185
+ 过程纪律 (比"答得漂亮"更重要, 逐条可判定):
186
+ 0. **先动手, 别先问**: 请求有显而易见的默认解释时, 按它做, 不要为了确认而停下 —— 只有"不同解释会让我做不同的事"时才问。
187
+ 0.5 **别把"主动"用在闲聊上**: 寒暄 / 感受 / 纯闲聊这类**没有任务**的消息 ⇒ 直接回答, **不要**为此调工具
188
+ (用户实测: 一句「你好」去跑了 git_status 花 4.1s ✗ —— 那是浪费)。"主动"= 接到任务后**想在你前面**,
189
+ 不是"每句话都先把环境查一遍" ✗。
190
+ 1. **做到底**: 交付的是**能跑的东西 + 真实的工具输出**, 不是"我打算怎么做"。只写方案、只搭一半、只报告打算做什么, 都算没做完。
191
+ 2. **一个工具不够就换法继续**: 同一件事试到确实不通为止 —— 换参数/换命令/换路径/换工具接着试, 不要试一次就收尾。
192
+ 3. **工具说成功 ≠ 任务成功**: 写入/生成/发布这类动作, **读回一次**再声称完成(内容/存在/大小/哈希都可)。
193
+ 4. **被阻塞就如实说**: 讲清卡在哪一步、为什么、还缺什么; **绝不**用编造的结果顶替(编一个"看起来对"的输出比说"没做成"更糟)。
194
+ 5. **每一轮要么用工具推进, 要么给出结论**: 不要把"下一步我打算…"当成回答。
195
+ 6. **收尾时说三句**: 改了什么 · 验过什么(拿什么读到的) · 还剩什么。不重述过程。
196
+ 7.4 **技能是"要用时先读再照做"**: 库里有一批已装载技能(名字+说明), 用 list_skills <关键词> 找,
197
+ 用 read_skill <名字> 读全文 —— **相关时先读它再动手**, 别凭印象做(技能里往往写着踩过的坑与判据 ✓)。
198
+ 7.5 **一轮里可以同时调多个独立工具** (并行执行已支持): 互不依赖的读取/查询**一次发出去**, 别一个一个来回 —
199
+ 零碎调用既慢又费预算(批量工具还会退还额度 ✓)。
200
+ 7.6 **收尾标记的语义**(别猜, 就按这条): 写完那个结束标记 = "**我已收尾, 且自己验过**(改了什么/拿什么验证的), 请复核"。
201
+ 只在这两种情况下写: ① 任务真的做完了(不是"我打算做") · ② 纯对话/问答也**不用**写(直接答即可)。
202
+ 没做完就**别写** —— 写了会被当成"已完成"进入复核, 反而浪费一轮 ✓。
203
+ 7. **过程中主动考虑**: 动手前先想这个改动的**影响面**(同类调用点 · 相邻功能 · 已有数据/契约), 发现关联问题就说出来,
204
+ 并给出你建议的下一步 —— 主动是指"想在你前面", 不是"多问几句"。
205
+ `;
133
206
  export class PiAgentSession {
134
207
  cwd;
135
208
  peerId;
@@ -150,7 +223,51 @@ export class PiAgentSession {
150
223
  /** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
151
224
  cachedToolDefinitions = '';
152
225
  /** M2.4: 缓存 persona section */
226
+ /**
227
+ * 当前活跃计划 (待办) 的**重注入段** (2026-10-01 落实④)。
228
+ * 每轮都带 —— 因为上下文压缩会把早先注入的计划文本丢掉, 一次性注入等于"压完就忘"。
229
+ * 有界 (条数/字符都封顶); 读不到就返回空串, 绝不影响主流程。
230
+ */
231
+ async renderActivePlansSection() {
232
+ // 2026-10-01: 后台委派的结果**自动回灌** —— 完成后的第一轮在这里给一行提示(只提一次)
233
+ let bg = '';
234
+ try {
235
+ bg = renderDelegateNotices();
236
+ }
237
+ catch { /* 提示失败不打断主流程 */ }
238
+ try {
239
+ const block = renderNoticeBlock(); // 通用回流(复盘产出/后台任务) —— 空闲排空 ⇒ 新一轮 ✓
240
+ if (block)
241
+ bg = bg ? `${bg}\n${block}` : block;
242
+ }
243
+ catch { /* 回流失败不打断 */ }
244
+ let plans = '';
245
+ try {
246
+ const { listActivePlans, formatPlansForPrompt } = await import('./plan-store.js');
247
+ const list = await listActivePlans();
248
+ plans = formatPlansForPrompt(list, { maxChars: 1200, maxItems: 3 });
249
+ }
250
+ catch {
251
+ return bg;
252
+ }
253
+ return bg ? `${bg}\n${plans}` : plans;
254
+ }
153
255
  cachedPersonaSection = '';
256
+ /** 本轮迭代预算 (每轮重置; 批处理工具会退还 —— 见 iteration-budget.ts) */
257
+ /** 上一个工具调用的参数指纹 (遥测判"是否重复调用") */
258
+ lastToolSig = null;
259
+ /** 最近一条用户消息 (按需子集档用来判 intent) */
260
+ lastUserIntent = '';
261
+ /** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
262
+ tsTouchedThisTurn = [];
263
+ /** 本回合是否已跑过类型检查 (一轮只跑一次) */
264
+ typecheckRanThisTurn = false;
265
+ iterBudget = null;
266
+ iterBudgetWarned = false;
267
+ /** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
268
+ lastReviewedTaskSig = undefined;
269
+ /** 上一次回合后自审的时间 (节流; 0 = 从未) */
270
+ lastExperienceReviewAt = 0;
154
271
  /** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
155
272
  _sessionStore;
156
273
  /** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
@@ -364,11 +481,41 @@ export class PiAgentSession {
364
481
  this.minimaxAvailable = this.checkMinimax();
365
482
  // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
366
483
  this.currentAgentId = config.agentId || '';
484
+ // 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
485
+ // 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
486
+ // 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
487
+ // 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
488
+ // 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
489
+ try {
490
+ if (this.currentAgentId) {
491
+ const cur = this.identity || {};
492
+ const curDid = String(cur.did || '');
493
+ const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
494
+ const missingName = !String(cur.name || '').trim();
495
+ // 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
496
+ // 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
497
+ // 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
498
+ const mine = loadOrCreateAgentIdentity(this.currentAgentId);
499
+ const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
500
+ if (badDid || missingName || mismatch) {
501
+ if (badDid || mismatch) {
502
+ console.warn(`[identity] 身份纠正: ${this.currentAgentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
503
+ }
504
+ // 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
505
+ // 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
506
+ Object.assign(this.identity, {
507
+ ...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
508
+ ...(missingName ? { name: agentPersonaName(this.currentAgentId) || this.currentAgentId } : {}),
509
+ });
510
+ }
511
+ }
512
+ }
513
+ catch { /* 自愈失败不致命 */ }
367
514
  // 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
368
515
  this._sessionStore = config.sessionStore ?? defaultSessionStore;
369
516
  this.constraintLayer = new ConstraintLayer();
370
517
  this.workflowEngine = new WorkflowEngine(this.constraintLayer);
371
- this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
518
+ this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.currentAgentId);
372
519
  this.agentsManager = new DiscoveredAgentsManager();
373
520
  this.usePivotLoop = config.usePivotLoop ?? false;
374
521
  this.pivotLoopConfig = config.pivotLoopConfig;
@@ -466,7 +613,13 @@ export class PiAgentSession {
466
613
  }
467
614
  }
468
615
  catch (err) {
469
- console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
616
+ // 2026-09-30: "会话文件不存在" 不是失败 —— 新 channel / 还没落过盘的会话就是这种情况
617
+ // (真机噪声: `hydrateMessageHistory 失败 (non-fatal): session not found: ch_bolloon:default`)。
618
+ // 只有**非** not-found 的错误才值得 warn。
619
+ const __msg = String(err?.message || err);
620
+ if (!/session not found|ENOENT/i.test(__msg)) {
621
+ console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${__msg.slice(0, 100)}`);
622
+ }
470
623
  }
471
624
  }
472
625
  /**
@@ -600,6 +753,59 @@ export class PiAgentSession {
600
753
  this._inboxMessages = [];
601
754
  const toolCtx = {
602
755
  tools: this.tools,
756
+ // 2026-10-01 (用户: 「process 也要可以管理群聊和去中心化交流进程」):
757
+ // 把**常驻的交流类服务**也挂进 process —— 能启停的给启停(社交心跳 ✓), 只能看的如实标"仅状态" ✓(不假装能停 ✗)
758
+ managedServices: (() => {
759
+ const ms = managedServices;
760
+ ms.register({
761
+ name: 'social-heartbeat',
762
+ description: '社交心跳: 主动发现节点/发起对话/组织群聊的常驻循环(去中心化交流用)',
763
+ status: () => (this.socialHeartbeat ? '运行中' : '已停止'),
764
+ start: () => this.startSocialHeartbeat(),
765
+ stop: () => this.stopSocialHeartbeat(),
766
+ });
767
+ ms.register({
768
+ name: 'p2p-network',
769
+ description: 'P2P 网络: 与其他 bolloon 节点的连接(群聊/消息都走它)',
770
+ status: () => { try {
771
+ const peers = this.getPeers?.() ?? [];
772
+ return `已连接 ${peers.length} 个节点`;
773
+ }
774
+ catch {
775
+ return '状态不可用';
776
+ } },
777
+ });
778
+ ms.register({
779
+ name: 'orbitdb-groups',
780
+ description: 'OrbitDB 群聊存储: 本地群列表与消息持久化',
781
+ status: () => { try {
782
+ const g = this.listGroupsShallow?.();
783
+ return g ? `本地群 ${g}` : '已就绪(用 /group 看列表)';
784
+ }
785
+ catch {
786
+ return '状态不可用';
787
+ } },
788
+ });
789
+ ms.register({
790
+ name: 'document-receiver',
791
+ description: '文档接收器: 监听并接收其他节点发来的文档分片',
792
+ status: () => '监听中(随进程启动)',
793
+ });
794
+ return ms;
795
+ })(),
796
+ // 2026-10-01: 技能发现(list_skills / read_skill)的实现 —— 技能原先只注册不露面 ✗, 现在模型能自己找
797
+ listSkills: () => this.skillRegistry.list().map((sk) => ({ name: sk.name, description: String(sk.description || '') })),
798
+ getSkillBody: async (name) => {
799
+ const sk = this.skillRegistry.get(name);
800
+ if (!sk)
801
+ return null;
802
+ try {
803
+ return await sk.execute({});
804
+ }
805
+ catch {
806
+ return null;
807
+ }
808
+ },
603
809
  cwd: this.cwd,
604
810
  identity: this.identity,
605
811
  persona: this.persona,
@@ -662,16 +868,18 @@ export class PiAgentSession {
662
868
  // 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
663
869
  if (this.cachedToolDefinitions)
664
870
  return this.cachedToolDefinitions;
665
- const defs = ['可用工具 (name(params) - 简介):'];
666
871
  // 2026-07-29: 使用 allowedTools() 过滤掉拒绝列表中的工具
667
- for (const tool of this.allowedTools()) {
668
- // 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
669
- // 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
670
- // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
671
- const paramNames = Object.keys(tool.parameters).join(',');
672
- defs.push(`- ${tool.name}(${paramNames})`);
673
- }
674
- this.cachedToolDefinitions = defs.join('\n');
872
+ const allowed = Array.from(this.allowedTools());
873
+ const header = '可用工具 (按类分组; name(params) - 简介):';
874
+ // 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
875
+ // 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
876
+ // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
877
+ // 2026-10-01 (优化 #2 v2): 按类**分组**列 + 说明"未列出的也能按名字调" ⇒ 治选择过载, 且不丢能力。
878
+ // BOLLOON_TOOL_SUBSET=on 时改走"按 intent 只展开相关桶"(仍带 list_tools + 能力提示 ✓)。
879
+ const subsetOn = String(process.env.BOLLOON_TOOL_SUBSET || '').toLowerCase() === 'on';
880
+ const intentText = subsetOn ? this.lastUserIntent : '';
881
+ const rendered = renderToolListWithParams(allowed, intentText, { subset: subsetOn, perBucket: 14 });
882
+ this.cachedToolDefinitions = `${header}\n${rendered.text}`;
675
883
  return this.cachedToolDefinitions;
676
884
  }
677
885
  async initSession() {
@@ -683,12 +891,28 @@ export class PiAgentSession {
683
891
  }
684
892
  }
685
893
  createDefaultIdentity() {
686
- return {
687
- did: `did:pi:${this.peerId.substring(0, 16)}`,
688
- name: `Agent-${this.peerId.substring(0, 8)}`,
689
- publicKey: this.peerId,
690
- createdAt: Date.now()
691
- };
894
+ // 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
895
+ // 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
896
+ // 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
897
+ const scope = this.currentAgentId || this.peerId || 'default';
898
+ try {
899
+ const real = loadOrCreateAgentIdentity(scope);
900
+ return {
901
+ did: real.did,
902
+ name: `Agent-${this.peerId.substring(0, 8)}`,
903
+ publicKey: real.publicKey || this.peerId,
904
+ createdAt: Date.now(),
905
+ };
906
+ }
907
+ catch {
908
+ // 真身份生成失败时**不编假 DID**: 如实留空, 由 /did 与 get_identity 报"未绑定"
909
+ return {
910
+ did: '',
911
+ name: `Agent-${this.peerId.substring(0, 8)}`,
912
+ publicKey: this.peerId,
913
+ createdAt: Date.now(),
914
+ };
915
+ }
692
916
  }
693
917
  checkMinimax() {
694
918
  try {
@@ -904,6 +1128,106 @@ export class PiAgentSession {
904
1128
  try {
905
1129
  // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
906
1130
  const loopResult = await this.runReActLoop(this.currentOnStream ?? undefined, options?.signal);
1131
+ // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
1132
+ if (decideTypecheck(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
1133
+ this.typecheckRanThisTurn = true;
1134
+ try {
1135
+ const tscTool = this.tools.get('tsc_check');
1136
+ if (tscTool?.execute) {
1137
+ const r = await tscTool.execute({});
1138
+ const line = formatTypecheckResult(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
1139
+ this.currentOnStream?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1140
+ }
1141
+ }
1142
+ catch (e) {
1143
+ this.currentOnStream?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1144
+ }
1145
+ this.tsTouchedThisTurn = [];
1146
+ }
1147
+ // 2026-10-01: **回合后自审 (纯旁路)** —— 沉淀可复用经验, 对应"结束后沉淀"那一段。
1148
+ // 纪律: fire-and-forget (不阻塞主回合) · 只写 ~/.bolloon/experience/ (**不碰主对话/prompt 缓存**)
1149
+ // · 节流 (默认 10 分钟/agent) · 失败只记一行日志, 绝不影响本回合结果。
1150
+ try {
1151
+ const reviewNow = Date.now();
1152
+ const turnSummary = `用户: ${String(this.currentUserInput || '').slice(0, 2000)}\n助手: ${String(loopResult?.reply || '').slice(0, 4000)}`;
1153
+ // 任务签名: 用户这次要的东西 + 用了哪些工具 ⇒ 换任务立刻复盘(同一任务才节流) ✓
1154
+ const taskSig = createHash('sha256')
1155
+ .update(`${String(this.currentUserInput || '').slice(0, 300)}|${this.totalToolCallsThisTurn || ''}`)
1156
+ .digest('hex').slice(0, 12);
1157
+ if (shouldReviewTask(reviewNow, this.lastExperienceReviewAt, this.lastReviewedTaskSig, taskSig)) {
1158
+ this.lastExperienceReviewAt = reviewNow; // 先记账, 免得多路并发各起一次
1159
+ this.lastReviewedTaskSig = taskSig;
1160
+ // 2026-10-01 (用户: 「我没有看到执行完命令后 bolloon 知道选中更新 skills,是不是没有触发」):
1161
+ // 实测**确实没触发** ✓ —— 两个原因: ① 被调方又自己判了一次节流 ⇒ 换任务也被 throttled 掉 ✗;
1162
+ // ② 反馈走 `console.warn`(TUI 里被吞 ✗) + `void … .catch(() => {})`(失败无声 ✗)
1163
+ // ⇒ 你什么都看不到 ✓。现在: 传 force 尊重调用方的决定 ✓ · 复盘过程/结果/失败**都进对话流** ✓ ·
1164
+ // 同时落一份 `~/.bolloon/logs/experience-review.log` 供事后核 ✓。
1165
+ const reviewLog = (m) => {
1166
+ const line = `[${new Date().toISOString()}] ${m}`;
1167
+ try {
1168
+ const p = path.join(os.homedir(), '.bolloon', 'logs', 'experience-review.log');
1169
+ void fs.mkdir(path.dirname(p), { recursive: true })
1170
+ .then(() => fs.appendFile(p, line + '\n'))
1171
+ .catch(() => { });
1172
+ }
1173
+ catch { /* 落日志失败不影响主流程 */ }
1174
+ try {
1175
+ this.currentOnStream?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1176
+ }
1177
+ catch { /* 上屏失败不打断 */ }
1178
+ };
1179
+ reviewLog('开始(换了任务 ⇒ 立刻复盘)');
1180
+ void runExperienceReview({
1181
+ turnSummary,
1182
+ force: true,
1183
+ chat: async (prompt) => {
1184
+ const r = await getMinimax().chat(prompt);
1185
+ return typeof r === 'string' ? r : String(r?.content ?? r?.text ?? '');
1186
+ },
1187
+ log: reviewLog,
1188
+ // 2026-10-01: 写了经验之后再找"能沉淀进哪个已有技能"的候选 —— **只记候选, 不自动改技能** ✓
1189
+ // (实测: 拿真实教训撞真实 1300 个技能, 最高分只有 2 且 top 命中是瞎的 ✗ ⇒ 不替人决定 ✓)
1190
+ onLesson: (lesson) => {
1191
+ try {
1192
+ // ⓐ 管理: 找"能沉淀进哪个已有技能"的候选(只记, 不自动改 ✗)
1193
+ const dirs = defaultSkillPaths(os.homedir(), process.cwd());
1194
+ const decision = decideLessonSink(lesson, skillsFromDirs(dirs));
1195
+ logLessonSuggestions(lesson, decision);
1196
+ // ⓐ' 用户要求「**都进去**」⇒ 每条教训都落进技能库: 强命中写那个技能 ✓, 否则写沉淀技能 `lessons-learned` ✓
1197
+ // 2026-10-01 **写来源隔离**: 复盘/自审产生的写, 打上 review 标记 ✓ ⇒
1198
+ // 它只许治理**自己造出来**的技能 ✓(用户在 skills 里点名要的归用户 ✓)
1199
+ runWithWriteOrigin('review', () => routeLessonToSkill(lesson, skillsFromDirs(dirs)));
1200
+ // 2026-10-01 (用户: 「复盘任务能否触发 loop?」): 教训**回流成下一轮的任务源** ✓ ——
1201
+ // 只"记下来"等于闭环断在这 ✗; 投进共享队列 ⇒ 空闲时排空 ⇒ 以**新一轮**浮现 ✓(绝不中途插队 ✓)。
1202
+ try {
1203
+ pushNotice('review', `${lesson.title} —— ${String(lesson.body || '').replace(/\s+/g, ' ').slice(0, 120)}`);
1204
+ }
1205
+ catch { /* 回流失败不打断 */ }
1206
+ // ⓑ 接入**判断力系统**: 同一条教训也进 HumanJudgment(带 source/confidence/revisable ⇒ 可被后续演化取代 ✓)
1207
+ // 这样经验库与判断力库**同一份来源**, 判断力注入(gate)时就能用上 ✓
1208
+ import('../pi-ecosystem-judgment/human-value-store.js').then((m) => m.storeHumanJudgment({
1209
+ decision: lesson.title,
1210
+ decision_type: 'modify',
1211
+ reasons: [lesson.body],
1212
+ values_derived: [],
1213
+ context: { domain: lesson.klass || 'general', complexity: 'simple', stakes: 'medium', time_pressure: 'low' },
1214
+ outcome: { approved: true },
1215
+ metadata: { source: 'trajectory', confidence: 0.7, revisable: true },
1216
+ })).catch(() => { });
1217
+ }
1218
+ catch { /* 这一段整体是锦上添花, 绝不外泄错误 */ }
1219
+ },
1220
+ }).then((r) => {
1221
+ // 结果**如实上屏** ✓: 审没审 / 写没写 / 为什么没写 —— 不再无声 ✓
1222
+ reviewLog(r?.reviewed
1223
+ ? (r?.applied ? `已沉淀经验 ✓${r?.file ? ` → ${path.basename(String(r.file))}` : ''}` : `审了但没写: ${r?.reason || '无'}`)
1224
+ : `没审: ${r?.reason || '无'}`);
1225
+ }).catch((e) => {
1226
+ reviewLog(`失败(已如实记录, 不外泄错误): ${String(e?.message || e).slice(0, 160)}`);
1227
+ });
1228
+ }
1229
+ }
1230
+ catch { /* 挂点自身失败也不影响主流程 */ }
907
1231
  this.finishTrajectory(trajRec, loopResult.reply, loopResult.aiFailed ? 'error' : 'ok');
908
1232
  return loopResult.reply;
909
1233
  }
@@ -1262,6 +1586,10 @@ ${this.getToolDefinitions()}
1262
1586
 
1263
1587
  ${PiAgentSession.TOOL_SELECTION_GUIDE}
1264
1588
 
1589
+ ${PROACTIVE_WORK_DISCIPLINE}
1590
+
1591
+ ${await this.renderActivePlansSection()}
1592
+
1265
1593
  工作模式:
1266
1594
  1. 理解用户自然语言请求
1267
1595
  2. 分析需要哪些工具来完成
@@ -1546,6 +1874,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1546
1874
  const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
1547
1875
  let totalToolCallsThisLoop = 0;
1548
1876
  const lastNTools = []; // 最近 MAX_IDEMPOTENT_TOOL 次工具名, 检测重复
1877
+ // 2026-10-01: 工具停滞观测状态 (分类 + 温和引导 + 重复结果引用) —— 见 agents/tool-loop-guard.ts
1878
+ // 与上面的"同工具 5 次就提示"不同: 它按 (工具名 + 参数 + 结果) 签名判定, 并抓 A→B→A→B 的循环
1879
+ const stallState = new LoopStallState();
1549
1880
  // 2026-08-10: unreported 循环逃生门 — LLM 反复不把工具结果写进回复时, 3 次后强制 final (不死板)
1550
1881
  const MAX_UNREPORTED_RETRIES = 3;
1551
1882
  let unreportedRetries = 0;
@@ -1710,7 +2041,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1710
2041
  }
1711
2042
  // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
1712
2043
  // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
1713
- const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
2044
+ // 2026-10-01 (落实②: 可退还的迭代预算): 惩罚零碎调用, **奖励批处理** ——
2045
+ // 程序化工具(execute_code, 一次能顶多次)调用后归还一次迭代 ⇒ 有效寿命被延长。
2046
+ // 退出条件仍由 decideMaxIterations 决定(读**净**用量), 不做硬刹车。
2047
+ if (iteration === 0 || !this.iterBudget)
2048
+ this.iterBudget = new IterationBudget(this.MAX_REACT_ITERATIONS);
2049
+ const iterBudget = this.iterBudget;
2050
+ iterBudget.consume();
2051
+ if (iterBudget.warn(0.8) && !this.iterBudgetWarned) {
2052
+ this.iterBudgetWarned = true;
2053
+ onStream?.({ type: 'status', internal: true, content: `⏳ 迭代预算已用 ${iterBudget.describe()} (批量工具会退还)` });
2054
+ }
2055
+ const maxIterDecision = decideMaxIterations(iterBudget.used, iterBudget.maxTotal);
1714
2056
  if (maxIterDecision.shouldExit) {
1715
2057
  console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
1716
2058
  onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
@@ -1839,6 +2181,10 @@ ${toolDefs}
1839
2181
 
1840
2182
  ${PiAgentSession.TOOL_SELECTION_GUIDE}
1841
2183
 
2184
+ ${PROACTIVE_WORK_DISCIPLINE}
2185
+
2186
+ ${await this.renderActivePlansSection()}
2187
+
1842
2188
  工作模式:
1843
2189
  1. 理解用户自然语言请求
1844
2190
  2. 分析需要哪些工具来完成
@@ -2149,7 +2495,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2149
2495
  }
2150
2496
  let result = replaySkip
2151
2497
  ? { success: true, output: `[恢复保护] ${toolCall.name} 在中断前已成功执行过, 本次不重复执行 (避免重复副作用)。当时结果: ${replaySkip}`, _replaySkipped: true }
2152
- : await tool.execute(toolCall.args);
2498
+ : await (toolCall.__t0 = Date.now(), tool.execute(expandHomeArgs(toolCall.args)));
2153
2499
  const toolDurationMs = Date.now() - toolStart;
2154
2500
  if (replaySkip) {
2155
2501
  console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
@@ -2208,7 +2554,39 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2208
2554
  console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${after.outputBlocked.reason}`);
2209
2555
  result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${after.outputBlocked.reason}]`, _harnessDenied: true };
2210
2556
  }
2557
+ // 2026-10-01: 落库前做一次停滞观测 —— 命中的是"引导/引用", 不是"拒绝执行":
2558
+ // 完全相同的返回从第 2 次起折叠成引用 (省上下文); 连续 3 次同参数同结果或检测到循环 ⇒
2559
+ // 在结果尾部追加一条系统提示 (保留模型的选择权, 不硬停 —— 硬停那条已被用户否决)。
2560
+ try {
2561
+ const obs = observeToolCall(stallState, {
2562
+ toolName: toolCall.name,
2563
+ sameSignatureBefore: this.lastToolSig === argsFingerprint(toolCall.args),
2564
+ args: toolCall.args,
2565
+ resultText: String(result.output || (result.success ? '' : String(result.error || ''))),
2566
+ ok: !!result.success,
2567
+ seenResultBefore: stallState.hasSeenResult(String(result.output || '')),
2568
+ });
2569
+ if (obs.stub) {
2570
+ result = { ...result, output: obs.stub, _stubbed: true };
2571
+ }
2572
+ else if (obs.notice) {
2573
+ result = { ...result, output: `${String(result.output || '')}\n\n${obs.notice}` };
2574
+ }
2575
+ if (obs.action === 'warn') {
2576
+ console.warn(`[PiAgent] 工具停滞引导 (${obs.code}, 第 ${obs.count} 次): ${toolCall.name}`);
2577
+ onStream?.({ type: 'status', internal: true, content: `🩺 检测到重复调用 ${toolCall.name} (${obs.code}), 已提示模型换法`, tool: 'loop' });
2578
+ }
2579
+ }
2580
+ catch { /* 观测失败绝不影响主路径 */ }
2211
2581
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2582
+ // 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
2583
+ // 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
2584
+ // 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
2585
+ if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
2586
+ const __batchHint = batchHint(toolCall.name, 1);
2587
+ if (__batchHint)
2588
+ this.messageHistory.push({ role: 'system', content: __batchHint.trim() });
2589
+ }
2212
2590
  this.logToHarness(toolCall.name, toolCall.args, result);
2213
2591
  // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
2214
2592
  // 去重: 同一工具同 args 连续成功只记一次 (防 LLM 重复 react 刷屏)
@@ -2237,10 +2615,48 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2237
2615
  onStream({ type: 'step_error', content: `${toolCall.name} 执行失败`, tool: toolCall.name, error: result.error });
2238
2616
  }
2239
2617
  }
2618
+ // 2026-10-01 (优化 #1/#3/#4): 工具结果的**进上下文闸** + **遥测** + **写操作读回自证**
2619
+ {
2620
+ const __ms = Date.now() - Number(toolCall.__t0 || Date.now());
2621
+ try {
2622
+ // #1 结果闸: 超上限 ⇒ 头尾 + 完整结果落文件(给路径) ⇒ 上下文不再被一条大输出长期占住
2623
+ const capped = capToolResult(String(result.output ?? ''), { tool: toolCall.name });
2624
+ if (capped.capped) {
2625
+ result.output = capped.text;
2626
+ console.warn(`[PiAgent] ${toolCall.name} 结果过长(${capped.originalChars})已截断` + (capped.spilledTo ? `, 完整内容: ${capped.spilledTo}` : ''));
2627
+ }
2628
+ // #4 写操作"读回自证": 写类工具成功后自动核一次(存在? 大小?) ⇒ "工具说成功≠任务成功"从规矩变成机制
2629
+ // 2026-10-01: 改了 TS 源码就记账 ⇒ 回合收尾自动跑类型检查 (把"靠自觉"变成机制)
2630
+ {
2631
+ const tsFile = codeWriteTarget(toolCall.name, toolCall.args);
2632
+ if (tsFile && !this.tsTouchedThisTurn.includes(tsFile))
2633
+ this.tsTouchedThisTurn.push(tsFile);
2634
+ }
2635
+ const verified = verifyWriteOutcome(toolCall.name, toolCall.args, this.cwd);
2636
+ if (verified)
2637
+ result.output = `${String(result.output ?? '')}\n${verified}`;
2638
+ // #3 遥测: 记一行(工具/指纹/耗时/成败/结果大小/是否与上一次同签名) ⇒ 重复率可算
2639
+ recordToolCall({
2640
+ tool: toolCall.name,
2641
+ sig: argsFingerprint(toolCall.args),
2642
+ ms: __ms,
2643
+ ok: true,
2644
+ resultChars: String(result.output ?? '').length,
2645
+ prevSig: this.lastToolSig ?? null,
2646
+ });
2647
+ this.lastToolSig = argsFingerprint(toolCall.args);
2648
+ }
2649
+ catch { /* 任何优化项失败都不影响工具结果本身 */ }
2650
+ }
2240
2651
  if (result.success) {
2241
2652
  consecutiveErrors = 0;
2242
2653
  // 2026-07-29: Hermes 风格硬限制计数
2243
2654
  totalToolCallsThisLoop++;
2655
+ // 2026-10-01: 批处理工具(一次顶多次) ⇒ **退还**这次迭代 (奖励批处理, 压零碎调用)
2656
+ if (this.iterBudget && isRefundableTool(toolCall.name)) {
2657
+ this.iterBudget.refund();
2658
+ console.warn(`[PiAgent] ${toolCall.name} 是批量工具 ⇒ 退还 1 次迭代 (现 ${this.iterBudget.describe()})`);
2659
+ }
2244
2660
  lastNTools.push(toolCall.name);
2245
2661
  if (lastNTools.length > MAX_IDEMPOTENT_TOOL)
2246
2662
  lastNTools.shift();
@@ -2261,6 +2677,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2261
2677
  }
2262
2678
  else {
2263
2679
  consecutiveErrors++;
2680
+ // 2026-10-01 (优化 #3): 失败也记一行(带错误类别) ⇒ "错工具率"从这类错误里看得出来
2681
+ try {
2682
+ const __cls = classifyError(String(result.error || '')).label || '未分类';
2683
+ recordToolCall({ tool: toolCall.name, sig: argsFingerprint(toolCall.args), ms: 0,
2684
+ ok: false, resultChars: 0, prevSig: this.lastToolSig ?? null, errorClass: __cls });
2685
+ this.lastToolSig = argsFingerprint(toolCall.args);
2686
+ }
2687
+ catch { /* 遥测失败不影响 */ }
2264
2688
  totalErrors++;
2265
2689
  if (toolCall.name === lastFailedTool) {
2266
2690
  lastFailedToolCount++;
@@ -2797,7 +3221,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2797
3221
  // currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
2798
3222
  // 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
2799
3223
  const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
2800
- // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
3224
+ // 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
3225
+ // **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
3226
+ try {
3227
+ const rc = String(response?.reasoningContent || '').trim();
3228
+ if (rc)
3229
+ this.currentOnStream?.({ type: 'reasoning', content: rc });
3230
+ }
3231
+ catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
2801
3232
  return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
2802
3233
  }
2803
3234
  catch (err) {
@@ -3343,7 +3774,13 @@ ${this.extractOperationsFromRef(operationsRef)}
3343
3774
  return { ...this.identity };
3344
3775
  }
3345
3776
  updateIdentity(updates) {
3346
- this.identity = { ...this.identity, ...updates };
3777
+ // 2026-10-01 **修切换频道后身份不换的根因**: 上一版是 `this.identity = { ...this.identity, ...updates }`
3778
+ // —— **换成新对象** ✗。而工具上下文是在 registerTools() 时按**对象引用**捕获的 (`identity: this.identity`),
3779
+ // 并且切频道时 session 是**复用**的(pi-sdk-session-factory 的模块级单例, 只调 updateIdentity) ⇒
3780
+ // 工具里的 ctx.identity 一直指向**旧对象** ⇒ `get_identity` 永远返回上一个频道的身份 ✗✗
3781
+ // (实测: xiaomi/智能体频道都答「233 的 DID + 小龙」, 因为 233 是进程启动时的频道)。
3782
+ // ⇒ 改成**原地改**: 所有持有者(工具上下文/会话/状态栏)都看到同一个对象的新值 ✓。
3783
+ Object.assign(this.identity, updates);
3347
3784
  }
3348
3785
  setCurrentChannelId(channelId) {
3349
3786
  this.currentChannelId = channelId;
@@ -3359,6 +3796,17 @@ ${this.extractOperationsFromRef(operationsRef)}
3359
3796
  }
3360
3797
  async setPersona(persona) {
3361
3798
  await this.sessionManager.savePersona(persona);
3799
+ // 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
3800
+ // (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
3801
+ if (this.currentAgentId) {
3802
+ try {
3803
+ const { applyPersonaToDocs } = await import('../bootstrap/persona-init.js');
3804
+ const wrote = await applyPersonaToDocs(this.currentAgentId, persona);
3805
+ if (wrote.length)
3806
+ console.warn(`[persona] 已把 ${this.currentAgentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3807
+ }
3808
+ catch { /* 非致命 */ }
3809
+ }
3362
3810
  this.persona = persona;
3363
3811
  if (persona.name) {
3364
3812
  this.identity.name = persona.name;