@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
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
+ import { deliverThroughActor } from './kernel/channel-actor.js';
1
2
  import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, createVerificationManager, } from '@diap/sdk';
2
3
  import { irohTransport } from './network/iroh-transport.js';
3
4
  import { getLastAiTiming, getLastAiUsage } from './llm/pi-ai.js';
4
5
  import { setHistoryScope } from './cli/input-history.js';
5
- import { statusSegments, fitSegments, rightAlignPad } from './cli/status-segments.js';
6
+ import { statusSegments, fitSegments, rightAlignPad, dispWidthSafe, statusLineBudget } from './cli/status-segments.js';
7
+ import { renderReasoning } from './cli/reasoning-view.js';
8
+ import { traceLabel } from './cli/trace-line.js';
6
9
  import { loadWalletTool } from './agents/wallet-tools.js';
7
10
  import { HybridMessenger } from './network/hybrid-messenger.js';
8
11
  import * as ed25519 from '@noble/ed25519';
@@ -29,10 +32,83 @@ import { installStartupLogGate, isStartupVerbose, startupLogPath, VERBOSE_ENV, c
29
32
  // 2026-09-27: 回复流卫生 — 内部运行日志 (循环推进/运行登记/收尾计数…) 不进对话回复流, 改落日志文件
30
33
  import { isInternalRunLog, appendInternalRunLog } from './cli/reply-hygiene.js';
31
34
  // 2026-09-27: 启动**前言 / 就绪度报告**闸门 (默认不上屏 → 面板一行 + 显式查询命令; 失败折成面板提示)
32
- import { noticeLine as noticePreamble, flushStartupNotices, setStartupReadiness, alertsFromSetup, readinessLine, preambleHintLine, startupPreambleVisible, pushStartupAlert, takeStartupPanelNotes, clearBufferedNotices, } from './cli/startup-notice.js';
35
+ import { onlyConnectivityRetest, SETUP_STATUS_CMD, noticeLine as noticePreamble, flushStartupNotices, setStartupReadiness, alertsFromSetup, readinessLine, preambleHintLine, startupPreambleVisible, pushStartupAlert, takeStartupPanelNotes, clearBufferedNotices, } from './cli/startup-notice.js';
33
36
  import { clipboardWrite } from './cli/clipboard.js';
34
37
  // 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
35
38
  import { createRequire } from 'module';
39
+ export function parseGroupSub(input) {
40
+ const rest = String(input || '').replace(/^\/group\b/i, '').trim();
41
+ if (!rest)
42
+ return { sub: 'list', arg: '' };
43
+ const sp = rest.indexOf(' ');
44
+ const head = (sp === -1 ? rest : rest.slice(0, sp)).toLowerCase();
45
+ const arg = sp === -1 ? '' : rest.slice(sp + 1).trim();
46
+ const aliases = { ls: 'list', l: 'list', n: 'new', create: 'new', add: 'join', msg: 'send', history: 'log', use_: 'use' };
47
+ const sub = aliases[head] || head;
48
+ // 2026-10-01 (用户: 「group 命令无法选中进群, 要可以选中」): 裸序号 ⇒ 直接选中该群
49
+ // 与 `/channel <序号>` 同一个习惯 ✓ (列表里就带序号)
50
+ if (/^\d+$/.test(head))
51
+ return { sub: 'use', arg: head };
52
+ const known = ['list', 'new', 'join', 'leave', 'use', 'send', 'log', 'members', 'help'];
53
+ return { sub: known.includes(sub) ? sub : 'help', arg: known.includes(sub) ? arg : rest };
54
+ }
55
+ export const CLI_KNOWN_COMMAND_HEADS = new Set([
56
+ '/group',
57
+ '/agent',
58
+ '/answer',
59
+ '/approve',
60
+ '/contacts',
61
+ '/copy',
62
+ '/criteria',
63
+ '/cron',
64
+ '/dequeue',
65
+ '/did',
66
+ '/dq',
67
+ '/dream',
68
+ '/email',
69
+ '/fork',
70
+ '/goal',
71
+ '/goals',
72
+ '/help',
73
+ '/insight',
74
+ '/ipfs',
75
+ '/ipns',
76
+ '/judgement',
77
+ '/judgments',
78
+ '/login',
79
+ '/logout',
80
+ '/loop',
81
+ '/mcp',
82
+ '/memory',
83
+ '/model',
84
+ '/net',
85
+ '/now',
86
+ '/p2p',
87
+ '/pause',
88
+ '/payments',
89
+ '/plan',
90
+ '/q',
91
+ '/questions',
92
+ '/queue',
93
+ '/reject',
94
+ '/resume',
95
+ '/runs',
96
+ '/session',
97
+ '/sessions',
98
+ '/setup',
99
+ '/skill',
100
+ '/skills',
101
+ '/suggestions',
102
+ '/supervise',
103
+ '/todo',
104
+ '/tools',
105
+ '/trace',
106
+ '/tx',
107
+ '/wake',
108
+ '/wallet',
109
+ '/wiki',
110
+ '/x402',
111
+ ]);
36
112
  const _require = createRequire(import.meta.url);
37
113
  const _BOLLOON_VERSION = (() => {
38
114
  try {
@@ -68,6 +144,75 @@ const MOVE_UP = '\x1b[A';
68
144
  const CLEAR_LINE = '\x1b[2K';
69
145
  const HIDE_CURSOR = '\x1b[?25l';
70
146
  const SHOW_CURSOR = '\x1b[?25h';
147
+ /**
148
+ * 统一出口 (2026-10-01, 用户: 「还是UI有污染」)。
149
+ * 为什么: Ink 被显式设成 `patchConsole: false` ⇒ **不接管 console** ✗ ⇒ 运行期任何 `console.log`
150
+ * 都写在 Ink 托管区**之外** ⇒ 每次直写都把正在渲染的 footer(提示行 + 分界线)往下推一份 ✗
151
+ * ⇒ 屏幕上重复出现「· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出」✓(真机实测就是这个)。
152
+ * 判据复用已有的 `startupPanelReady`(启动面板画好了 = Ink 已在跑 ✓):
153
+ * Ink 在跑 ⇒ **一律走它自己的消息流**; 没跑(boot 期)⇒ 才直写 ✓。
154
+ */
155
+ function writeOut(line) {
156
+ // 2026-10-01 (用户: 「时间好像是面板后面启动的」+「继续,还没去掉」):
157
+ // **关键**: 启动那几步**不是 await 的** ✗(fire-and-forget)⇒ 它们常在 `startupPanelReady = true`
158
+ // **之后**才打印 ✓ ⇒ 用"面板好了没"当判据必然漏 ✗(我上一版就栽在这 ✓)。
159
+ // 改成**独立的启动期标志** `bootPhase` ✓: 启动期内的**进度类**输出一律只落盘 ✓, 与面板状态无关 ✓。
160
+ if (bootPhase) {
161
+ bootLogOnly(line);
162
+ return;
163
+ }
164
+ if (startupPanelReady) {
165
+ try {
166
+ appendLine(line);
167
+ return;
168
+ }
169
+ catch { /* 落到落盘 */ }
170
+ }
171
+ // 2026-10-01 (用户: 「启动那几行……继续,还没去掉」): **启动期的"进度类"输出直接不进屏** ✓
172
+ // 上一版我把它们**搬进对话流**了 ✗ —— 但用户要的是**去掉** ✓(面板已经把版本/模型/技能这些要点画了 ✓,
173
+ // 而 `复用 DID` / `iroh:` / `主题:` / `[N/5]` 这类属于**过程细节** ✓)。
174
+ // 现在: 启动期 ⇒ **只落日志**(~/.bolloon/logs/startup.log ✓ 事后照样能查 ✓), 不上屏 ✓。
175
+ bootLogOnly(line);
176
+ }
177
+ /**
178
+ * 启动期**警告/错误**仍然要上屏 ✓ (不许因为"想干净"把问题藏起来 ✗):
179
+ * Ink 没起 ⇒ 先缓冲, `flushBootBuffer()` 在 Ink 起来后灌进对话流 ✓; Ink 已在跑 ⇒ 直接进 ✓。
180
+ */
181
+ function writeOutWarn(line) {
182
+ if (startupPanelReady) {
183
+ try {
184
+ appendLine(line);
185
+ return;
186
+ }
187
+ catch { /* 落到缓冲 */ }
188
+ }
189
+ if (bootBuffer.length < 500) {
190
+ bootBuffer.push(line);
191
+ return;
192
+ }
193
+ console.log(line);
194
+ }
195
+ /** 启动期进度类输出: 只落盘(不上屏 ✓) */
196
+ function bootLogOnly(line) {
197
+ try {
198
+ const clean = String(line).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
199
+ fsSync.appendFileSync(startupLogPath(), `[boot] ${clean}\n`);
200
+ }
201
+ catch { /* 落盘失败也不上屏(它本来就是过程细节 ✓) */ }
202
+ }
203
+ /** 启动期"警告/错误"缓冲: Ink 起来之前攒着, `flushBootBuffer()` 一次性灌进对话流 ✓ */
204
+ const bootBuffer = [];
205
+ export function flushBootBuffer() {
206
+ while (bootBuffer.length) {
207
+ const line = bootBuffer.shift();
208
+ try {
209
+ appendLine(line);
210
+ }
211
+ catch {
212
+ console.log(line);
213
+ }
214
+ }
215
+ }
71
216
  const s = {
72
217
  banner: () => {
73
218
  printBanner(_BOLLOON_VERSION);
@@ -78,52 +223,48 @@ const s = {
78
223
  status === 'warn' ? `${YELLOW}⚠` :
79
224
  status === 'error' ? `${MAGENTA}✗` :
80
225
  `${CYAN}●`;
81
- console.log(` ${check} ${WHITE}[${num}/${total}]${GRAY} ${text}${RESET}`);
226
+ writeOut(` ${check} ${WHITE}[${num}/${total}]${GRAY} ${text}${RESET}`);
82
227
  },
83
- success: (text) => console.log(` ${GREEN}✓${RESET} ${text}`),
84
- warn: (text) => console.log(` ${YELLOW}⚠${RESET} ${text}`),
85
- error: (text) => console.log(` ${MAGENTA}✗${RESET} ${text}`),
86
- info: (text) => console.log(` ${CYAN}●${RESET} ${text}`),
228
+ success: (text) => writeOut(` ${GREEN}✓${RESET} ${text}`),
229
+ warn: (text) => writeOutWarn(` ${YELLOW}⚠${RESET} ${text}`),
230
+ error: (text) => writeOutWarn(` ${MAGENTA}✗${RESET} ${text}`),
231
+ info: (text) => writeOut(` ${CYAN}●${RESET} ${text}`),
87
232
  section: (title) => {
88
- console.log(`\n${BLUE}━━━ ${WHITE}${BOLD}${title}${RESET} ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
233
+ writeOut(`\n${BLUE}━━━ ${WHITE}${BOLD}${title}${RESET} ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
89
234
  },
90
- divider: () => console.log(`\n${GRAY}${'─'.repeat(50)}${RESET}\n`),
91
- prompt: (text) => console.log(`\n${CYAN}❯ ${WHITE}${text}${RESET}`),
235
+ divider: () => writeOut(`\n${GRAY}${'─'.repeat(50)}${RESET}\n`),
236
+ prompt: (text) => writeOut(`\n${CYAN}❯ ${WHITE}${text}${RESET}`),
92
237
  response: (label, content) => {
93
- console.log(`\n${GREEN}${label}${RESET}\n${content}\n`);
238
+ writeOut(`\n${GREEN}${label}${RESET}\n${content}\n`);
94
239
  },
95
240
  agentCard: (agent) => {
96
241
  const statusColor = agent.status === 'active' ? GREEN :
97
242
  agent.status === 'idle' ? YELLOW :
98
243
  agent.status === 'busy' ? MAGENTA : GRAY;
99
- console.log(` ${WHITE}${BOLD}${agent.name}${RESET}`);
100
- console.log(` ${GRAY}ID:${RESET} ${agent.id}`);
101
- console.log(` ${GRAY}状态:${RESET} ${statusColor}${agent.status}${RESET}`);
102
- console.log(` ${GRAY}能力:${RESET} ${agent.capabilities.join(', ')}`);
244
+ writeOut(` ${WHITE}${BOLD}${agent.name}${RESET}`);
245
+ writeOut(` ${GRAY}ID:${RESET} ${agent.id}`);
246
+ writeOut(` ${GRAY}状态:${RESET} ${statusColor}${agent.status}${RESET}`);
247
+ writeOut(` ${GRAY}能力:${RESET} ${agent.capabilities.join(', ')}`);
103
248
  if (agent.did)
104
- console.log(` ${GRAY}DID:${RESET} ${agent.did}`);
105
- console.log();
249
+ writeOut(` ${GRAY}DID:${RESET} ${agent.did}`);
250
+ writeOut('');
106
251
  },
107
252
  Thinking: () => {
108
- const frames = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
109
- let i = 0;
110
- let dots = 0;
111
- const frame = frames[0];
112
- appendLine(` ${frame} 思考...`);
113
- return setInterval(() => {
114
- i = (i + 1) % frames.length;
115
- dots = (dots + 1) % 4;
116
- const dotStr = '.'.repeat(dots || 1);
117
- appendLine(`\r ${frames[i]} 思考${dotStr} `);
118
- }, 600);
253
+ // 2026-10-01 (用户: 「颜文字被加载进去了」/「可能是 ink 渲染的问题」): **真凶在这里** ✗ ——
254
+ // 原来每一帧都走 `appendLine` ⇒ 而对话流是 Ink 的 `<Static>`(**写一次永不重绘** ✓)
255
+ // ⇒ 于是每 600ms 就有一条「思考中...」被**永久烙进滚动区** ✗, 带 `\r` 也擦不掉 ✗,
256
+ // 滚出来的残影还能被框选/拖进输入框 ⇒ 混进上下文 ✓。
257
+ // 现在改成走 **Ink 的瞬时行**(footer 自己帧进动画 ✓, 只重绘一行 ✓, 不落进 Static ✓)。
258
+ inkSetThinking(true);
259
+ return setInterval(() => { }, 600);
119
260
  },
120
261
  clearThinking: (interval) => {
121
262
  clearInterval(interval);
122
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
263
+ inkSetThinking(false);
123
264
  },
124
265
  dialog: async (title, promptText) => {
125
266
  return new Promise((resolve) => {
126
- console.log(renderDialog({ title, prompt: promptText }));
267
+ writeOut(renderDialog({ title, prompt: promptText }));
127
268
  const rl = readline.createInterface({
128
269
  input: process.stdin,
129
270
  output: process.stdout
@@ -149,6 +290,14 @@ ed25519.hashes.sha512 = sha512;
149
290
  let startupPanelFirst = false;
150
291
  /** 面板第一帧已经画出来了 (startCLI 取走面板提示之后): 晚到的降级提示直接追加进面板 */
151
292
  let startupPanelReady = false;
293
+ /**
294
+ * **启动期**标志 (2026-10-01): 与 `startupPanelReady` **不是一回事** ✗ ——
295
+ * 启动那几步是 fire-and-forget, 打印时间常在面板之后 ✓ ⇒ 只有用独立标志才拦得住 ✓。
296
+ * 结束条件: **用户第一次发消息** ✓(他真正接管了)或启动后 **20s 兜底** ✓。
297
+ */
298
+ let bootPhase = true;
299
+ function endBootPhase() { bootPhase = false; }
300
+ setTimeout(() => { bootPhase = false; }, 20_000).unref?.();
152
301
  /**
153
302
  * 2026-09-30: 启动面板 / 就绪行**各只许追加一次**.
154
303
  * 防任何重入路径 (会话切换 / 重复 boot / 恢复) 把面板再叠一条 —— leo 报的「顶框重复 14 次」那一类
@@ -235,8 +384,8 @@ async function bootstrapIdentity() {
235
384
  const username = getUserName();
236
385
  const suffix = did.split(':').pop()?.substring(0, 4);
237
386
  const name = `blln-${username}-${suffix}`;
238
- console.log(` ${reused ? GRAY + '复用 ' : ''}${GRAY}DID:${RESET} ${did}`);
239
- console.log(` ${GRAY}名称:${RESET} ${name}`);
387
+ writeOut(` ${reused ? GRAY + '复用 ' : ''}${GRAY}DID:${RESET} ${did}`);
388
+ writeOut(` ${GRAY}名称:${RESET} ${name}`);
240
389
  s.step(1, 5, reused ? '复用 DIAP 身份' : '生成 DIAP 身份', 'ok');
241
390
  return { keypair: kp, did, name };
242
391
  }
@@ -297,7 +446,7 @@ async function bootstrapP2P(verifier) {
297
446
  await comm.start();
298
447
  const topic = createTopic('bolloon-agent-harness');
299
448
  await comm.joinTopic(topic);
300
- console.log(` ${GRAY}主题:${RESET} ${topic.slice(0, 8).toString('hex')}...`);
449
+ writeOut(` ${GRAY}主题:${RESET} ${topic.slice(0, 8).toString('hex')}...`);
301
450
  s.step(3, 5, '启动 P2P 网络', 'ok');
302
451
  return comm;
303
452
  }
@@ -315,20 +464,20 @@ async function bootstrapIroh(keypair, name) {
315
464
  s.step(4, 5, '启动 iroh P2P', 'loading');
316
465
  try {
317
466
  const node = await irohTransport.start();
318
- console.log(` ${GRAY}iroh:${RESET} ${node.nodeId.substring(0, 16)}...`);
467
+ writeOut(` ${GRAY}iroh:${RESET} ${node.nodeId.substring(0, 16)}...`);
319
468
  hybridMessenger = new HybridMessenger({
320
469
  preferIrohForLarge: true,
321
470
  largeThresholdBytes: 64 * 1024,
322
471
  enableRelay: true,
323
472
  });
324
473
  hybridMessenger.onMessage('task', async (msg) => {
325
- console.log(`[iroh] Task from ${msg.from.substring(0, 12)}...: ${new TextDecoder().decode(msg.payload).substring(0, 50)}...`);
474
+ writeOut(`[iroh] Task from ${msg.from.substring(0, 12)}...: ${new TextDecoder().decode(msg.payload).substring(0, 50)}...`);
326
475
  });
327
476
  hybridMessenger.onMessage('blob', async (msg) => {
328
- console.log(`[iroh] Blob from ${msg.from.substring(0, 12)}...: ${msg.payload.length} bytes`);
477
+ writeOut(`[iroh] Blob from ${msg.from.substring(0, 12)}...: ${msg.payload.length} bytes`);
329
478
  });
330
479
  hybridMessenger.onMessage('response', async (msg) => {
331
- console.log(`[iroh] Response from ${msg.from.substring(0, 12)}...`);
480
+ writeOut(`[iroh] Response from ${msg.from.substring(0, 12)}...`);
332
481
  });
333
482
  if (agentIdentity) {
334
483
  agentIdentity.irohNodeId = node.nodeId;
@@ -337,13 +486,15 @@ async function bootstrapIroh(keypair, name) {
337
486
  }
338
487
  catch (e) {
339
488
  s.step(4, 5, '启动 iroh P2P', 'warn');
340
- console.log(` ${YELLOW}iroh 启动失败: ${e.message}${RESET}`);
341
- console.log(` ${GRAY}继续使用 Hyperswarm P2P${RESET}`);
489
+ writeOut(` ${YELLOW}iroh 启动失败: ${e.message}${RESET}`);
490
+ writeOut(` ${GRAY}继续使用 Hyperswarm P2P${RESET}`);
342
491
  }
343
492
  }
344
493
  // ---------------------------------------------------------------------------
345
494
  // Agent 懒加载
346
495
  // ---------------------------------------------------------------------------
496
+ // 2026-10-01: /group 的"当前群"(进程内; 重启后需重新 /group use)
497
+ let currentGroupId;
347
498
  let agent = null;
348
499
  /**
349
500
  * 退出存档 (2026-09-30, leo: 「按 ctrl+c / esc 退出的时候, 自动存档 session 进程, 最后显示再见👋」)
@@ -488,15 +639,34 @@ async function getAgent() {
488
639
  catch { /* 读不到就退默认 */ }
489
640
  }
490
641
  let identityDoc;
491
- if (chIdentity?.did && chIdentity.publicKey) {
492
- // channel 已有持久 DID → 用 channel 身份
642
+ // 2026-10-01 (用户实测: 两个不同 channel 都答同一个名字, get_identity 的 DID 为空):
643
+ // 旧逻辑要求 channel 记录**同时**有 did + publicKey, 否则落到 `agentIdentity`(进程级**共享**) ⇒
644
+ // 多个 channel 共用一个身份 ✗; 且 channel 里可能存着假 DID (did:local:…) 或干脆没有 did。
645
+ // 现在: 归属以 channel 的 agentId 为准 —— 现取现造**该 agent 的**真 did:key, 名字用**它自己的** persona。
646
+ const { channelIdentityUsable, nameForChannel } = await import('./agents/channel-identity.js');
647
+ let perAgentIdentity;
648
+ if (chIdentity?.agentId) {
649
+ try {
650
+ const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
651
+ const mine = loadOrCreateAgentIdentity(chIdentity.agentId);
652
+ if (mine?.did)
653
+ perAgentIdentity = { did: mine.did, publicKey: mine.publicKey, name: nameForChannel(chIdentity, chIdentity.agentId) };
654
+ }
655
+ catch { /* 退回落 */ }
656
+ }
657
+ if (channelIdentityUsable(chIdentity)) {
658
+ // channel 自带**可用**的持久 DID (真 did:key) → 用它, 名字仍按该 agent 取 (不再用共享名)
493
659
  identityDoc = {
494
660
  did: chIdentity.did,
495
- name: chIdentity.persona?.name || chIdentity.name || 'agent',
661
+ name: nameForChannel(chIdentity, chIdentity.agentId),
496
662
  publicKey: chIdentity.publicKey,
497
663
  createdAt: Date.now(),
498
664
  };
499
665
  }
666
+ else if (perAgentIdentity) {
667
+ // channel 缺身份 / 只有假 DID ⇒ 用**该 agent 自己的**真身份 (按 agentId 落盘密钥, 稳定)
668
+ identityDoc = { ...perAgentIdentity, createdAt: Date.now() };
669
+ }
500
670
  else if (agentIdentity) {
501
671
  identityDoc = {
502
672
  did: agentIdentity.did,
@@ -512,15 +682,60 @@ async function getAgent() {
512
682
  else {
513
683
  identityDoc = undefined;
514
684
  }
685
+ // 2026-10-01: **身份取值自证** —— 只在"可疑"时打一行事实 (不是刷屏):
686
+ // 缺 did / 假 did / channel 有 agentId 却没被用上 ⇒ 打出来, 免得靠猜。
687
+ // 用户实测过 DID 为空、名字串台、以及一个数据里根本不存在的 did ⇒ 这行能直接指认哪条分支被走了。
688
+ try {
689
+ const d = String(identityDoc?.did ?? '');
690
+ const suspicious = !d || /^did:(local|pi):/i.test(d) || (!chIdentity?.agentId && !!targetChannelId);
691
+ if (suspicious) {
692
+ console.warn(`[identity] 身份可疑: channel=${targetChannelId ?? '(无)'} · channel.agentId=${chIdentity?.agentId ?? '(无)'} · ` +
693
+ `分支=${channelIdentityUsable(chIdentity) ? 'channel 自带' : perAgentIdentity ? 'per-agent' : agentIdentity ? '**共享 agentIdentity**' : '默认'} · ` +
694
+ `did=${d ? d.slice(0, 30) : '(空)'} · name=${String(identityDoc?.name ?? '(空)')}`);
695
+ }
696
+ }
697
+ catch { /* 自证失败不影响主流程 */ }
698
+ // 2026-10-01: 把当前 agent 告诉进程 —— 项目上下文(context-collector)据此取**该 agent 的** persona,
699
+ // 否则它会读全局 persona.json, 于是 get_identity 已是「小龙」而上下文里还写着「小宝」✗。
700
+ if (chIdentity?.agentId)
701
+ process.env.BOLLOON_ACTIVE_AGENT_ID = chIdentity.agentId;
702
+ // 2026-10-01: **身份取值一行事实** —— 每次**建/切 agent** 打一行(不是每回合, 不刷屏)。
703
+ // 用户报「xiaomi 频道却拿到 233 的 DID + 小龙」而可疑判据没触发 ⇒ 猜不动了, 让代码直说。
704
+ try {
705
+ console.warn(`[identity] channel=${targetChannelId ?? '(无)'} · 查到=${chIdentity ? 'yes' : '**no**'} · ` +
706
+ `channel.agentId=${chIdentity?.agentId ?? '(无)'} · channel.did=${String(chIdentity?.did ?? '(无)').slice(0, 26)} · ` +
707
+ `分支=${channelIdentityUsable(chIdentity) ? 'channel自带' : perAgentIdentity ? 'per-agent' : agentIdentity ? '**共享' : '默认'} · ` +
708
+ `结果 did=${String(identityDoc?.did ?? '(空)').slice(0, 26)} name=${String(identityDoc?.name ?? '(空)')}`);
709
+ }
710
+ catch { /* 自证失败不影响主流程 */ }
515
711
  const loadSessionKey = targetChannelId
516
712
  ? `${targetChannelId}:${chIdentity?.currentSessionId || 'default'}`
517
713
  : undefined;
714
+ // 2026-10-01: 确保该 agent 名下有身份文档 (此前仓里没有任何"建文档"的路径 ⇒
715
+ // 用户四个 agent 的 persona/<agentId>/ 全空 ⇒ loadPersonaDocs 读回空 ⇒ 所有智能体一个样)。
716
+ // 挂在这里 = 建 agent / 切 channel 两条路都会走到; 幂等, 已存在的不覆盖。
717
+ if (chIdentity?.agentId) {
718
+ try {
719
+ const { ensurePersonaDocs } = await import('./bootstrap/persona-init.js');
720
+ // 2026-10-01: 名字必须和该 agent 的 persona.json 一致 —— 之前传的是**渠道名**,
721
+ // 于是 soul.md 写「我是 233」而 persona.json 写「小龙」⇒ 用户看到"身份没匹配上" ✗。
722
+ const r = await ensurePersonaDocs(chIdentity.agentId, { name: nameForChannel(chIdentity, chIdentity.agentId) });
723
+ if (r.created.length)
724
+ console.warn(`[persona] 已为 ${r.agentId} 生成 ${r.created.length} 份起步身份文档: ${r.created.join(' · ')} → ${r.dir}`);
725
+ }
726
+ catch { /* 非致命 */ }
727
+ }
518
728
  agent = await createAgentSession({
519
729
  cwd: process.cwd(),
520
730
  peerId: targetChannelId ?? 'harness',
521
- identityDoc,
522
731
  // 2026-08-09: 透传 channel.agentId → persona docs 按 agent 加载 (身份真正变化)
523
732
  agentId: chIdentity?.agentId || (targetChannelId ? undefined : agentIdentity?.name),
733
+ // 2026-10-01 ★ **必须传 identityDoc** —— 之前漏了 ✗, 后果有两层:
734
+ // ① 上面按频道算好的身份(名字 + 该 agent 自己的 did)被**丢掉**, session 用自己造/复用的;
735
+ // ② session 工厂的更新路径 (pi-sdk-session-factory.updateIdentity) 以 `config.identityDoc?.did`
736
+ // 为条件 ⇒ 缺它 **永不触发** ⇒ 切频道时复用同一个 session 实例, 身份却**不换** ✗✗
737
+ // (实测: 233→智能体 后答 233 的身份; 智能体→233 后答 智能体的身份 = 身份"差一格" ✓ 根因在此)
738
+ identityDoc,
524
739
  loadSessionKey,
525
740
  });
526
741
  agentBoundChannelId = targetChannelId;
@@ -548,7 +763,7 @@ async function dispatchTask(raw) {
548
763
  const task = safeParse(body);
549
764
  if (!task)
550
765
  return `ERR|${JSON.stringify({ code: 'bad_format' })}`;
551
- console.log(`\n📥 [${task.type}] from=${task.from?.substring(0, 18)}... id=${task.id}`);
766
+ writeOut(`\n📥 [${task.type}] from=${task.from?.substring(0, 18)}... id=${task.id}`);
552
767
  try {
553
768
  switch (task.type) {
554
769
  case 'summarize':
@@ -567,8 +782,9 @@ async function handleSummarize(task) {
567
782
  if (!task.documentPath)
568
783
  return `ERR|${JSON.stringify({ code: 'no_path' })}`;
569
784
  const a = await getAgent();
570
- const { summary, qualityScore } = await a.summarizeDocument(task.documentPath);
571
- console.log(` ✅ 质量=${(qualityScore * 10).toFixed(1)}/10`);
785
+ const docPath = task.documentPath; // 闭包里 TS 不保留收窄 ⇒ 先收成局部 const
786
+ const { summary, qualityScore } = await deliverThroughActor(a, () => a.summarizeDocument(docPath));
787
+ writeOut(` ✅ 质量=${(qualityScore * 10).toFixed(1)}/10`);
572
788
  return `OK|${JSON.stringify({ id: task.id, type: 'summarize', qualityScore, summary })}`;
573
789
  }
574
790
  async function handleImprove(task) {
@@ -576,13 +792,16 @@ async function handleImprove(task) {
576
792
  return `ERR|${JSON.stringify({ code: 'no_path_or_req' })}`;
577
793
  }
578
794
  const a = await getAgent();
579
- const res = await a.improveDocument({
580
- originalPath: task.documentPath,
581
- requirements: task.requirements,
795
+ // 闭包里 TS 不保留收窄 ⇒ 先把两个字段收成局部 const
796
+ const docPath = task.documentPath;
797
+ const reqs = task.requirements;
798
+ const res = await deliverThroughActor(a, () => a.improveDocument({
799
+ originalPath: docPath,
800
+ requirements: reqs,
582
801
  context: `来自节点: ${task.from}`,
583
- });
802
+ }));
584
803
  const ok = res.improved ?? false;
585
- console.log(` ✅ 改进${ok ? '成功' : '失败'} 质量=${(res.qualityScore * 10).toFixed(1)}/10 自动发送=${res.shouldAutoSend}`);
804
+ writeOut(` ✅ 改进${ok ? '成功' : '失败'} 质量=${(res.qualityScore * 10).toFixed(1)}/10 自动发送=${res.shouldAutoSend}`);
586
805
  return `OK|${JSON.stringify({
587
806
  id: task.id, type: 'improve', improved: ok,
588
807
  qualityScore: res.qualityScore, shouldAutoSend: res.shouldAutoSend,
@@ -731,7 +950,12 @@ function getStatus() {
731
950
  return `${C_DIM}${icon}${RESET} ${color}${val}${RESET}`;
732
951
  });
733
952
  // 按剩余宽度取舍 (真机 112 列时右段被截 ⇒ 看着像没出现)
734
- const __segsFit = fitSegments(__segs.map(x => x.replace(/\x1b\[[0-9;]*m/g, '')), Math.max(20, termWidth() - 2 - __leftPlainLen - 6));
953
+ // 2026-10-01: 底栏一律用**保守测宽** (Ambiguous 字符按 2 列) —— 超 1 列就会让终端折行、
954
+ // 进而打破 Ink 光标数学, 表现是整块底栏被重复打印 (见 status-segments.dispWidthSafe 注释)
955
+ // 2026-10-01: 预算按**保守宽度**给 (left + ≥1 格 + title ≤ width-1) ⇒ 任何字体下都不折行
956
+ const __notePlain = (cliSessionPreview && cliSessionTitleKey === cliSessionKey) ? `▸ ${cliSessionPreview}` : '';
957
+ const __budget = statusLineBudget(termWidth(), __leftPlainLen, String(__notePlain || ''));
958
+ const __segsFit = fitSegments(__segs.map(x => x.replace(/\x1b\[[0-9;]*m/g, '')), __budget, dispWidthSafe);
735
959
  const __live = __segsFit.length
736
960
  ? ` ${C_DIM}│${RESET} ` + __segsFit.map(x => {
737
961
  const m = /^(\S+)\s(.*)$/.exec(x);
@@ -742,13 +966,13 @@ function getStatus() {
742
966
  : '';
743
967
  // 2026-09-30 (leo: 「这个 title 需要顶格右侧」): 会话 Title **右对齐**贴右边缘 ——
744
968
  // 先算左半边+活数据段的显示宽, 再补空格把 `▸ 标题` 推到 width-1 处; 放不下就不显示 (不挤坏这一行)。
745
- const __notePlain = (cliSessionPreview && cliSessionTitleKey === cliSessionKey) ? `▸ ${cliSessionPreview}` : '';
746
969
  let __noteStr = '';
747
970
  if (__notePlain) {
748
971
  // 宽度必须用 dispWidth (East Asian Width 表): 行内的 `│ ░ ◷ ▸ ◎ ✓ ↑ ⚙` 都是**单宽**,
749
972
  // 用 "charCode > 255 ⇒ 2" 的启发式会每行多算十几列 ⇒ 右对齐留一大截 (leo: 「没有完全右对齐」)。
750
- const used = dispWidth(`${__left}${__live}`);
751
- const pad = rightAlignPad(used, dispWidth(__notePlain), termWidth());
973
+ const used = dispWidthSafe(`${__left}${__live}`);
974
+ // 保守测宽 (同上): 右对齐后整行最多到 width-1, 在任何终端字体下都不会折行
975
+ const pad = rightAlignPad(used, dispWidthSafe(__notePlain), termWidth());
752
976
  if (pad !== null)
753
977
  __noteStr = ' '.repeat(pad) + `${C_DIM}${__notePlain}${RESET}`;
754
978
  }
@@ -1150,6 +1374,8 @@ async function startCLI(commReady) {
1150
1374
  startupPanelReady = true;
1151
1375
  // 2026-09-30: 面板在起帧前已进 store ⇒ 首帧里与输入框一起出现
1152
1376
  startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
1377
+ // 2026-10-01: Ink 起来了 ⇒ 把启动期缓冲的日志一次性灌进对话流(不丢, 也不跟面板交错 ✓)
1378
+ flushBootBuffer();
1153
1379
  // 2026-08-10: 自动整理心跳 (CLI 侧, 与社交心跳并列) — 启动后立即"固定看一下 skills view"
1154
1380
  // (扫描遗留 skills), 之后按周期 (默认 30min, env BOLLOON_ORGANIZE_HEARTBEAT_MS) 完整进化经验.
1155
1381
  // 显示走 transient 颜文字行: 触发时显示, 结束后清空 (显示为空).
@@ -1200,7 +1426,7 @@ async function startCLI(commReady) {
1200
1426
  new Promise((res) => setTimeout(() => res(null), 8000)),
1201
1427
  ]);
1202
1428
  if (a && typeof a.promptStream === 'function') {
1203
- llm = (p) => a.promptStream(p, () => { }, undefined, cliActiveChannelId || undefined);
1429
+ llm = (p) => deliverThroughActor(a, () => a.promptStream(p, () => { }, undefined, cliActiveChannelId || undefined));
1204
1430
  }
1205
1431
  }
1206
1432
  catch { /* 无 agent → 仅扫描 */ }
@@ -1303,6 +1529,7 @@ async function startCLI(commReady) {
1303
1529
  * scheduler 的每个 tick 会先查 resolveDnd(), 处于勿扰时把 due job 记 deferred 并留到下一轮.
1304
1530
  */
1305
1531
  async function processInput(input, comm) {
1532
+ endBootPhase(); // 用户已接管 ⇒ 启动期结束(之后的输出照常上屏 ✓)
1306
1533
  const gate = globalThis.__bolloonMainTask;
1307
1534
  if (gate?.enterMainTask) {
1308
1535
  try {
@@ -1440,7 +1667,31 @@ async function processInputInner(input, comm) {
1440
1667
  const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
1441
1668
  appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
1442
1669
  appendLine(`${C_DIM} channel: ${r.channel.id} [${r.match}]${RESET}`);
1443
- appendLine(`${C_DIM} persona: ${r.channel.persona?.description || r.channel.persona?.personality || '无'}${RESET}`);
1670
+ // 2026-10-01 (用户报「切换身份还没有成功」): 把**CLI 认为的身份**打进对话流 —— 之前用 console.warn,
1671
+ // 在这套 TUI 里被吞掉, 用户从来看不到 ✗。切频道才一行, 不刷屏。
1672
+ try {
1673
+ const live = agent?.getIdentity?.() ?? null;
1674
+ appendLine(`${C_DIM} [identity] 频道身份=${r.identity.name} (did=${String(r.identity?.did ?? '(无)').slice(0, 26)})` +
1675
+ ` · agent 当前身份=${live?.name ?? '(读不到)'} (did=${String(live?.did ?? '(无)').slice(0, 26)})` +
1676
+ ` · agentId=${r.channel.agentId ?? '(无)'}${RESET}`);
1677
+ }
1678
+ catch { /* 自证失败不影响切换 */ }
1679
+ // 2026-10-01: 这行原先读 channels.json 里**内联的 metadata** (你四条 channel 都空 ⇒ 恒打"无"),
1680
+ // 而身份真源是 ~/.bolloon/persona/<agentId>/ 的 6 份文档 ⇒ 改读真源, 如实报"加载到几份"。
1681
+ try {
1682
+ const { loadPersonaDocs, PERSONA_DOC_FILES_HINT } = await import('./bootstrap/persona-loader.js').then(async (m) => ({ loadPersonaDocs: m.loadPersonaDocs, PERSONA_DOC_FILES_HINT: ['soul', 'identity', 'project', 'user', 'agent', 'wiki'] }));
1683
+ const agentId = r.channel.agentId || '';
1684
+ const docs = agentId ? await loadPersonaDocs(agentId) : null;
1685
+ const loaded = docs ? PERSONA_DOC_FILES_HINT.filter((k) => String(docs[k] || '').trim().length > 0) : [];
1686
+ appendLine(agentId
1687
+ ? (loaded.length
1688
+ ? `${C_DIM} persona: ${loaded.length}/6 份身份文档 (${loaded.join(' · ')}) · agentId=${agentId}${RESET}`
1689
+ : `${C_DIM} persona: 未初始化 (agentId=${agentId}, 目录 ${'~/.bolloon/persona/' + agentId}/ 为空 — 下次加载会自动生成起步模板)${RESET}`)
1690
+ : `${C_DIM} persona: 该 channel 没有 agentId ⇒ 无法按 agent 加载身份文档${RESET}`);
1691
+ }
1692
+ catch {
1693
+ appendLine(`${C_DIM} persona: (读身份文档失败, 不影响本次切换)${RESET}`);
1694
+ }
1444
1695
  }
1445
1696
  catch (e) {
1446
1697
  appendLine(`${C_ERROR}/channel 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
@@ -1522,7 +1773,7 @@ async function processInputInner(input, comm) {
1522
1773
  });
1523
1774
  }
1524
1775
  await fs.writeFile(agentsPath, JSON.stringify(arr, null, 2), 'utf-8');
1525
- console.log(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
1776
+ writeOut(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
1526
1777
  }
1527
1778
  catch { /* agents.json 写失败不阻塞创建 */ }
1528
1779
  // 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
@@ -1619,6 +1870,14 @@ async function processInputInner(input, comm) {
1619
1870
  // 不能全局改成"首词" —— 同文件里 `/email clear` 这类多词命令就是靠整行匹配的。
1620
1871
  // 所以加一个**首词**给"带参数的命令"用 (cmd 保持原样给多词命令用)。
1621
1872
  const cmdHead = cmd.split(/\s+/)[0];
1873
+ // 2026-10-01: **未知命令兜底** —— 以 / 开头但不在白名单里的输入, 不当消息发下去 ✗
1874
+ // (用户实测: /group 不是命令 ⇒ 却发给模型, 白烧一轮工具调用)
1875
+ if (cmd.startsWith('/') && !CLI_KNOWN_COMMAND_HEADS.has(cmdHead)) {
1876
+ const near = [...CLI_KNOWN_COMMAND_HEADS].filter((h) => h.includes(cmdHead.slice(1)) || cmdHead.includes(h.slice(1))).slice(0, 6);
1877
+ appendLine(`${C_WARN}未知命令 ${cmdHead}${RESET}${C_DIM} —— 输入${RESET} / ${C_DIM}看命令列表, 或 /help 看用法${RESET}`
1878
+ + (near.length ? `${C_DIM} · 你是不是想用: ${near.join(' / ')}${RESET}` : ''));
1879
+ return;
1880
+ }
1622
1881
  // /net — Agent 网络快捷命令 (join/status/ctx, 2026-09-08)
1623
1882
  if (cmd === '/net' || cmd.startsWith('/net ')) {
1624
1883
  const arg = trimmed.slice(4).trim();
@@ -1747,7 +2006,7 @@ async function processInputInner(input, comm) {
1747
2006
  return;
1748
2007
  }
1749
2008
  const agent = await getAgent();
1750
- const active = String(agent?.currentChannelId || '');
2009
+ const active = String(agent?.actor?.state?.channelId || ''); // K5 步骤⑧: 本体在 actor (删访问器后这里曾静默变空 ⇒ 安全检查失效)
1751
2010
  if (rec.channelId && active && rec.channelId !== active) {
1752
2011
  appendLine(`${C_ERROR}该运行属于 channel ${rec.channelId} (当前 ${active}) — 先 /channel 切过去再 /resume${RESET}`);
1753
2012
  return;
@@ -2056,7 +2315,7 @@ async function processInputInner(input, comm) {
2056
2315
  }
2057
2316
  agent.setGoalId?.(req.goal.goalId);
2058
2317
  agent.setContinuationGuards?.(req.guards || []);
2059
- await agent.prompt(req.instruction);
2318
+ await deliverThroughActor(agent, () => agent.prompt(req.instruction));
2060
2319
  return { runId: agent.getLastRunId?.() || agent.getRunId?.(), status: 'done' };
2061
2320
  }
2062
2321
  : undefined;
@@ -2615,7 +2874,7 @@ async function processInputInner(input, comm) {
2615
2874
  const a = await getAgent();
2616
2875
  const h = a.messageHistory ?? [];
2617
2876
  appendLine(`${C_ACCENT}当前会话:${RESET} ${C_DIM}${cliSessionKey || '—'}${RESET}`);
2618
- appendLine(` ${C_DIM}channel:${RESET} ${a.currentChannelId || cliActiveChannelId || '—'} ${C_DIM}agent:${RESET} ${a.currentAgentId || cliAgentId || '—'}`);
2877
+ appendLine(` ${C_DIM}channel:${RESET} ${a.actor?.state?.channelId || cliActiveChannelId || '—'} ${C_DIM}agent:${RESET} ${a.currentAgentId || cliAgentId || '—'}`);
2619
2878
  appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 ${h.length > 15 ? `${C_DIM}(${h.length - 15} 条已压缩)${RESET}` : ''}`);
2620
2879
  }
2621
2880
  catch (e) {
@@ -3144,7 +3403,7 @@ async function processInputInner(input, comm) {
3144
3403
  const a = await getAgent();
3145
3404
  appendLine(`${C_ACCENT}智能体:${RESET}`);
3146
3405
  appendLine(` ${C_DIM}名称:${RESET} ${cliAgentName}`);
3147
- appendLine(` ${C_DIM}agentId:${RESET} ${a.currentAgentId || '—'}`);
3406
+ appendLine(` ${C_DIM}agentId:${RESET} ${a.actor?.state?.agentId || '—'}`); // K5 步骤⑧: 本体在 actor
3148
3407
  appendLine(` ${C_DIM}channel:${RESET} ${cliActiveChannelId || '—'}`);
3149
3408
  }
3150
3409
  catch { /* 静默 */ }
@@ -3576,6 +3835,153 @@ async function processInputInner(input, comm) {
3576
3835
  }
3577
3836
  return;
3578
3837
  }
3838
+ // 「当前群」的取用: 没选过时, 只有恰好一个群就自动用它; 否则提示先 /group use
3839
+ const ensureCurrentGroup = async (gg) => {
3840
+ if (currentGroupId)
3841
+ return currentGroupId;
3842
+ const gs = await gg.listGroups();
3843
+ if (gs.length === 1) {
3844
+ currentGroupId = gs[0].id;
3845
+ return currentGroupId;
3846
+ }
3847
+ if (!gs.length) {
3848
+ appendLine(`${C_DIM}本地还没有群 —— 用 /group new <名字> 建一个${RESET}`);
3849
+ return undefined;
3850
+ }
3851
+ appendLine(`${C_DIM}本地有 ${gs.length} 个群, 先 /group use <名字|id> 选一个${RESET}`);
3852
+ return undefined;
3853
+ };
3854
+ // /group — OrbitDB 群聊 (2026-10-01; 接 gateway-group 那套: 建群/加入/收发/成员/日志)
3855
+ if (cmd === '/group' || cmd.startsWith('/group ')) {
3856
+ const { sub, arg } = parseGroupSub(trimmed);
3857
+ const gg = await import('./agents/gateway-group.js');
3858
+ const dim = (t) => appendLine(`${C_DIM}${t}${RESET}`);
3859
+ try {
3860
+ if (sub === 'help') {
3861
+ appendLine(`${C_ACCENT}/group${RESET} —— OrbitDB 群聊`);
3862
+ dim(' /group 列出本地群');
3863
+ dim(' /group new <名字> 建群 (默认创建者独占写; 会打印可分享地址)');
3864
+ dim(' /group join <链接|地址> 加入');
3865
+ dim(' /group leave <名字|id> 退出');
3866
+ dim(' /group use <名字|id> 选定当前群 (之后 send/log/members 都指它)');
3867
+ dim(' /group send <文本> 往当前群发一条');
3868
+ dim(' /group log [N] 看当前群最近 N 条 (默认 10)');
3869
+ dim(' /group members 列当前群成员');
3870
+ return;
3871
+ }
3872
+ if (sub === 'list') {
3873
+ const gs = await gg.listGroups();
3874
+ if (!gs.length) {
3875
+ dim('本地还没有群 —— 用 /group new <名字> 建一个');
3876
+ return;
3877
+ }
3878
+ const rows = [];
3879
+ gs.forEach((g, i) => {
3880
+ const cur = currentGroupId === g.id ? '●' : '○';
3881
+ const n = String(i + 1).padStart(2, ' ');
3882
+ rows.push(`${n}. ${cur} ${g.name} ${String(g.id).slice(0, 18)}…`);
3883
+ });
3884
+ rows.push('');
3885
+ rows.push('选中进群: /group <序号> (或 /group use <名字|id>)');
3886
+ appendLine(renderMessageBox({ title: 'OrbitDB 群', body: rows.join('\n'), color: C_ACCENT, maxLines: 24 }));
3887
+ return;
3888
+ }
3889
+ if (sub === 'new') {
3890
+ const r = await gg.createGroup(arg || '');
3891
+ if (!r.ok) {
3892
+ appendLine(`${C_WARN}建群失败: ${r.error}${RESET}`);
3893
+ return;
3894
+ }
3895
+ currentGroupId = r.group?.id || currentGroupId;
3896
+ appendLine(renderMessageBox({ title: '✅ 群已建立', body: `名字: ${r.group?.name || arg}\nid: ${r.group?.id || '-'}\n地址: ${r.group?.link || r.group?.address || '(未返回地址)'}\n\n把地址发给同伴, 用 /group join <地址> 加入`, color: C_ACCENT, maxLines: 12 }));
3897
+ return;
3898
+ }
3899
+ if (sub === 'join') {
3900
+ if (!arg) {
3901
+ dim('用法: /group join <链接|地址>');
3902
+ return;
3903
+ }
3904
+ const r = await gg.joinGroup(arg);
3905
+ if (!r.ok) {
3906
+ appendLine(`${C_WARN}加入失败: ${r.error}${RESET}`);
3907
+ return;
3908
+ }
3909
+ currentGroupId = r.group?.id || currentGroupId;
3910
+ appendLine(`${C_ACCENT}✓ 已加入 ${r.group?.name || arg}${RESET}`);
3911
+ return;
3912
+ }
3913
+ if (sub === 'leave') {
3914
+ if (!arg) {
3915
+ dim('用法: /group leave <名字|id>');
3916
+ return;
3917
+ }
3918
+ const r = await gg.leaveGroup(arg);
3919
+ appendLine(r.ok ? `${C_ACCENT}✓ 已退出 ${r.removed || arg}${RESET}` : `${C_WARN}退出失败: ${r.error}${RESET}`);
3920
+ if (r.ok && currentGroupId === arg)
3921
+ currentGroupId = undefined;
3922
+ return;
3923
+ }
3924
+ if (sub === 'use') {
3925
+ if (!arg) {
3926
+ dim('用法: /group use <名字|id>');
3927
+ return;
3928
+ }
3929
+ const gs = await gg.listGroups();
3930
+ // 序号 (与 /group 列表里的编号一致) → 名字 → id → 名字子串
3931
+ let hit = /^\d+$/.test(arg) ? gs[parseInt(arg, 10) - 1] : undefined;
3932
+ if (!hit)
3933
+ hit = gs.find((g) => g.id === arg || g.name === arg) || gs.find((g) => String(g.name).includes(arg));
3934
+ if (!hit) {
3935
+ appendLine(`${C_WARN}没找到群: ${arg}${RESET}${C_DIM} —— 先 /group 看编号${RESET}`);
3936
+ return;
3937
+ }
3938
+ currentGroupId = hit.id;
3939
+ appendLine(`${C_ACCENT}✓ 当前群: ${hit.name} (${String(hit.id).slice(0, 18)}…)${RESET}${C_DIM} 现在可以 /group send <文本> 或 /group log${RESET}`);
3940
+ return;
3941
+ }
3942
+ if (sub === 'send') {
3943
+ if (!arg) {
3944
+ dim('用法: /group send <文本>');
3945
+ return;
3946
+ }
3947
+ const gid = await ensureCurrentGroup(gg);
3948
+ if (!gid)
3949
+ return;
3950
+ const r = await gg.groupSend(gid, arg, String(agentIdentity?.did || 'cli')).catch((e) => ({ ok: false, error: String(e?.message || e) }));
3951
+ appendLine(r.ok === false ? `${C_WARN}发送失败: ${r.error}${RESET}` : `${C_ACCENT}✓ 已发到群${RESET}`);
3952
+ return;
3953
+ }
3954
+ if (sub === 'log') {
3955
+ const gid = await ensureCurrentGroup(gg);
3956
+ if (!gid)
3957
+ return;
3958
+ const n = Math.max(1, Math.min(100, parseInt(arg || '10', 10) || 10));
3959
+ const msgs = await gg.groupMessages(gid, n);
3960
+ if (!msgs.length) {
3961
+ dim('这个群还没有消息');
3962
+ return;
3963
+ }
3964
+ appendLine(renderMessageBox({ title: `群消息 (最近 ${msgs.length})`, body: msgs.map((m) => `${m.from ? String(m.from).slice(0, 10) : '?'}: ${String(m.text || '').slice(0, 200)}`).join('\n'), color: C_ACCENT, maxLines: 24 }));
3965
+ return;
3966
+ }
3967
+ if (sub === 'members') {
3968
+ const gid = await ensureCurrentGroup(gg);
3969
+ if (!gid)
3970
+ return;
3971
+ const ms = await gg.groupMembers(gid);
3972
+ dim(ms.length ? ms.join('\n') : '(暂无成员记录)');
3973
+ return;
3974
+ }
3975
+ dim('用法: /group help');
3976
+ }
3977
+ catch (e) {
3978
+ // GroupStoreUnreachableError 等: 说清"本地群存储没起来", 不要笼统报错
3979
+ const msg = String(e?.message || e);
3980
+ const unreachable = e?.name === 'GroupStoreUnreachableError' || /unreachable|not open|未就绪/i.test(msg);
3981
+ appendLine(`${C_WARN}${unreachable ? '本地群存储未就绪 —— 需要本机服务/OrbitDB 起来后才行' : '群操作失败'}: ${msg}${RESET}`);
3982
+ }
3983
+ return;
3984
+ }
3579
3985
  if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
3580
3986
  appendLine(`${C_DIM}命令:${RESET}`);
3581
3987
  appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
@@ -3710,12 +4116,24 @@ async function processInputInner(input, comm) {
3710
4116
  cliTurnStartedAt = Date.now();
3711
4117
  cliTurnReplyBytes = 0;
3712
4118
  cliTurnToolCount = 0;
3713
- const response = await a.prompt(trimmed, {
4119
+ const response = await deliverThroughActor(a, () => a.prompt(trimmed, {
3714
4120
  onStream: (e) => {
3715
4121
  // 2026-08-07: 中间思考/状态显示 — 之前只显示工具步骤, LLM 的思考过程
3716
4122
  // (thinking / status / phase / Reflection) 全被丢弃 → 用户只能看到输入和最终输出
3717
4123
  // 用户偏好 (2026-08-07): 思考过程 = 圆角框渲染 (和回复同路径 renderMessageBox),
3718
4124
  // 颜文字动画 (inkSetThinking) 只表示"正在运行", 不承载思考内容
4125
+ // 2026-10-01: 思考记录 (模型的思维链) —— 暗色块 + 明确标注"未验证", 与正式回答分开 ✓
4126
+ if (e.type === 'reasoning' && e.content) {
4127
+ // 2026-10-01 (用户: 「我要的是那种**短的**思考, 长思维链可以不显示」): 默认只显示**一句**"在想什么";
4128
+ // 想看全链 ⇒ BOLLOON_SHOW_THINKING=full; 不想看 ⇒ =0。
4129
+ const r = renderReasoning(String(e.content));
4130
+ if (r.text) {
4131
+ if (r.mode === 'short')
4132
+ appendLine(`${C_DIM}💭 ${r.text}${RESET}`);
4133
+ else
4134
+ appendLine(renderMessageBox({ title: '💭 思考 (未验证)', body: r.text, color: C_DIM, maxLines: 16 }));
4135
+ }
4136
+ }
3719
4137
  if (e.type === 'thinking' && e.content) {
3720
4138
  // thinking 事件只有 "🤔 开始思考..." 占位 → 不 appendLine, 运行过程由动画表示;
3721
4139
  // 真正思考内容在 status 的 Reflection/💡 事件 → 下方框渲染
@@ -3742,7 +4160,10 @@ async function processInputInner(input, comm) {
3742
4160
  // Reflection / 反思 / 💡 → 圆角思考框 (和回复一样走 renderMessageBox, 白字+亮边框)
3743
4161
  else if (content.includes('Reflection') || content.includes('反思') || content.includes('💡')) {
3744
4162
  const body = content.replace(/^💡\s*/, '').slice(0, 1500);
3745
- if (body.trim())
4163
+ // 2026-10-01: 只有标记词 (Reflection: / 反思 / 💡) 而无正文 ⇒ **不渲染**, 免得出空框 ✗
4164
+ const plain = body.replace(/\x1b\[[0-9;]*m/g, '').trim(); // 2026-10-01: 先剥 ANSI —— 只有彩色控制符的正文看着就是**空框** ✗
4165
+ const onlyMarker = /^(?:reflection|反思)\s*[::\-—]?\s*$/i.test(plain);
4166
+ if (plain && !onlyMarker)
3746
4167
  appendLine(renderMessageBox({ title: '💡 反思', body, color: C_WARN }));
3747
4168
  }
3748
4169
  else if (!content.includes('🔄 循环') && !content.includes('📋 参数')
@@ -3763,7 +4184,9 @@ async function processInputInner(input, comm) {
3763
4184
  // 改用 transient 行显示"正在执行"(消息流只在 done 时出现一次完成行).
3764
4185
  if (toolName !== 'system' && toolName !== 'loop' && toolName !== '?') {
3765
4186
  const activeNames = tuiToolCalls.map(c => c.tool).filter(t => t !== 'system' && t !== 'loop' && t !== '?');
3766
- const label = activeNames.length > 1 ? `执行 ${activeNames.length} 个工具: ${activeNames.join(', ')}` : `🔧 ${toolName}`;
4187
+ const label = activeNames.length > 1
4188
+ ? `执行 ${activeNames.length} 个工具: ${activeNames.join(', ')}`
4189
+ : `🔧 ${traceLabel(toolName)}`; // trace 执行描述: 写清这一步在做什么
3767
4190
  inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
3768
4191
  }
3769
4192
  }
@@ -3793,7 +4216,7 @@ async function processInputInner(input, comm) {
3793
4216
  const remaining = tuiToolCalls.filter(c => c.tool !== 'system' && c.tool !== 'loop' && c.tool !== '?');
3794
4217
  cliTurnToolCount = remaining.length; // ⚙ 状态栏用 (模块级)
3795
4218
  if (remaining.length > 0) {
3796
- const label = remaining.length > 1 ? `执行 ${remaining.length} 个工具: ${remaining.map(c => c.tool).join(', ')}` : `🔧 ${remaining[0].tool}`;
4219
+ const label = remaining.length > 1 ? `执行 ${remaining.length} 个工具: ${remaining.map(c => c.tool).join(', ')}` : `🔧 ${traceLabel(remaining[0].tool)}`; // trace 执行描述
3797
4220
  inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
3798
4221
  }
3799
4222
  else {
@@ -3801,7 +4224,7 @@ async function processInputInner(input, comm) {
3801
4224
  }
3802
4225
  }
3803
4226
  }
3804
- });
4227
+ }));
3805
4228
  // 智能体回复框
3806
4229
  appendLine(renderAgentMessage(response));
3807
4230
  try {
@@ -4071,7 +4494,7 @@ goalFlag) {
4071
4494
  error = response;
4072
4495
  break;
4073
4496
  }
4074
- const result = await a.summarizeDocument(filePath, ctx.join(' '));
4497
+ const result = await deliverThroughActor(a, () => a.summarizeDocument(filePath, ctx.join(' ')));
4075
4498
  response = `📝 摘要:\n${result.summary}\n\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10`;
4076
4499
  metadata.qualityScore = result.qualityScore;
4077
4500
  break;
@@ -4083,10 +4506,10 @@ goalFlag) {
4083
4506
  error = response;
4084
4507
  break;
4085
4508
  }
4086
- const result = await a.improveDocument({
4509
+ const result = await deliverThroughActor(a, () => a.improveDocument({
4087
4510
  originalPath: filePath,
4088
4511
  requirements: req.join(' ')
4089
- });
4512
+ }));
4090
4513
  response = result.newContent || '';
4091
4514
  if (!result.improved) {
4092
4515
  response = '错误: 改进失败';
@@ -4267,7 +4690,7 @@ goalFlag) {
4267
4690
  error = response;
4268
4691
  break;
4269
4692
  }
4270
- response = await a.prompt(text);
4693
+ response = await deliverThroughActor(a, () => a.prompt(text));
4271
4694
  break;
4272
4695
  }
4273
4696
  case 'tools': {
@@ -4895,18 +5318,18 @@ goalFlag) {
4895
5318
  error,
4896
5319
  metadata
4897
5320
  };
4898
- console.log(JSON.stringify(result, null, 2));
5321
+ writeOut(JSON.stringify(result, null, 2));
4899
5322
  }
4900
5323
  else {
4901
5324
  if (error) {
4902
5325
  s.divider();
4903
- console.log(`${MAGENTA}${error}${RESET}\n`);
5326
+ writeOut(`${MAGENTA}${error}${RESET}\n`);
4904
5327
  }
4905
5328
  else {
4906
5329
  s.divider();
4907
- console.log(`${response}\n`);
5330
+ writeOut(`${response}\n`);
4908
5331
  }
4909
- console.log(`${GRAY}耗时: ${metadata.duration}ms${RESET}`);
5332
+ writeOut(`${GRAY}耗时: ${metadata.duration}ms${RESET}`);
4910
5333
  }
4911
5334
  }
4912
5335
  async function runNonInteractive(args, comm) {
@@ -4922,11 +5345,11 @@ async function runNonInteractive(args, comm) {
4922
5345
  }
4923
5346
  else if (prompt) {
4924
5347
  const a = await getAgent();
4925
- console.log(await a.prompt(prompt));
5348
+ writeOut(await deliverThroughActor(a, () => a.prompt(prompt)));
4926
5349
  }
4927
5350
  console.log = originalLog;
4928
5351
  await fs.writeFile(output, outputBuffer.trim(), 'utf-8');
4929
- console.log(`✅ 结果已保存到: ${output}`);
5352
+ writeOut(`✅ 结果已保存到: ${output}`);
4930
5353
  return;
4931
5354
  }
4932
5355
  if (tool === 'prompt' && prompt) {
@@ -4935,23 +5358,23 @@ async function runNonInteractive(args, comm) {
4935
5358
  const startTime = Date.now();
4936
5359
  const a = await getAgent();
4937
5360
  try {
4938
- const response = await a.prompt(prompt);
5361
+ const response = await deliverThroughActor(a, () => a.prompt(prompt));
4939
5362
  const elapsed = Date.now() - startTime;
4940
5363
  if (json) {
4941
- console.log(JSON.stringify({ success: true, response, elapsedMs: elapsed }, null, 2));
5364
+ writeOut(JSON.stringify({ success: true, response, elapsedMs: elapsed }, null, 2));
4942
5365
  }
4943
5366
  else {
4944
- console.log(response);
4945
- console.log(`\n耗时: ${elapsed}ms`);
5367
+ writeOut(response);
5368
+ writeOut(`\n耗时: ${elapsed}ms`);
4946
5369
  }
4947
5370
  }
4948
5371
  catch (e) {
4949
5372
  const error = e?.message || String(e);
4950
5373
  if (json) {
4951
- console.log(JSON.stringify({ success: false, error }, null, 2));
5374
+ writeOut(JSON.stringify({ success: false, error }, null, 2));
4952
5375
  }
4953
5376
  else {
4954
- console.log(`\n错误: ${error}`);
5377
+ writeOut(`\n错误: ${error}`);
4955
5378
  }
4956
5379
  process.exit(1);
4957
5380
  }
@@ -4965,7 +5388,7 @@ async function runNonInteractive(args, comm) {
4965
5388
  const a = await getAgent();
4966
5389
  let response;
4967
5390
  try {
4968
- response = await a.prompt(prompt);
5391
+ response = await deliverThroughActor(a, () => a.prompt(prompt));
4969
5392
  }
4970
5393
  catch (e) {
4971
5394
  response = `错误: ${e.message}`;
@@ -4979,10 +5402,10 @@ async function runNonInteractive(args, comm) {
4979
5402
  error: response.startsWith('错误:') ? response : undefined,
4980
5403
  metadata: { duration, peers }
4981
5404
  };
4982
- console.log(JSON.stringify(result, null, 2));
5405
+ writeOut(JSON.stringify(result, null, 2));
4983
5406
  }
4984
5407
  else {
4985
- console.log(response);
5408
+ writeOut(response);
4986
5409
  }
4987
5410
  }
4988
5411
  }
@@ -5300,7 +5723,7 @@ function parseArgs() {
5300
5723
  return result;
5301
5724
  }
5302
5725
  function printHelp() {
5303
- console.log(`
5726
+ writeOut(`
5304
5727
  🤖 Bolloon Agent - AI 可调用文档处理智能体
5305
5728
 
5306
5729
  用法:
@@ -5528,21 +5951,21 @@ async function main() {
5528
5951
  const res = await runStandaloneSupervisorHost({
5529
5952
  once: !!args.superviseOnce,
5530
5953
  dryRun: !!args.superviseDryRun,
5531
- log: (m) => console.log(m),
5954
+ log: (m) => writeOut(m),
5532
5955
  });
5533
5956
  if (args.superviseOnce) {
5534
5957
  const rep = res.lastReport;
5535
5958
  if (rep) {
5536
- console.log(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
5959
+ writeOut(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
5537
5960
  for (const s of rep.skipped.slice(0, 8))
5538
- console.log(` 跳过 ${s.goalId}: ${s.reason}`);
5961
+ writeOut(` 跳过 ${s.goalId}: ${s.reason}`);
5539
5962
  for (const e of rep.executed)
5540
- console.log(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
5963
+ writeOut(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
5541
5964
  }
5542
- console.log(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
5965
+ writeOut(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
5543
5966
  }
5544
5967
  else {
5545
- console.log(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
5968
+ writeOut(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
5546
5969
  }
5547
5970
  return;
5548
5971
  }
@@ -5593,6 +6016,14 @@ async function main() {
5593
6016
  if (process.env.BOLLOON_SKIP_SETUP === '1') {
5594
6017
  noticePreamble('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
5595
6018
  }
6019
+ else if (onlyConnectivityRetest(gateEv)) {
6020
+ // 2026-10-01 (用户报「启动时候的日志也没去掉」):
6021
+ // 只差"连通性结果过期(需重测)"这一类 ⇒ **不在启动时跑 setup 向导** (跑一次要刷约 30 行前言)。
6022
+ // 改为: 面板内一行告警 + 让用户按需跑 `bolloon setup --test`; 门禁照旧拦住 agent 执行 (不绕过)。
6023
+ const a = `⚠ 连通性结果过期 → 跑 \`bolloon setup --test\` 复测 (启动时不自动跑), 全量: ${SETUP_STATUS_CMD}`;
6024
+ pushStartupAlert(a);
6025
+ noticePreamble(a);
6026
+ }
5596
6027
  else {
5597
6028
  const { runSetupWizard } = await import('./cli/setup-wizard.js');
5598
6029
  // quietStartup: 向导的续跑前言默认不上屏 (它自己会在"要人回答/有失败"时先 flush)
@@ -5793,7 +6224,7 @@ async function main() {
5793
6224
  // 默认用户模式: 不自迭代, 自改卡片不自动出现, 仍可 POST /api/self-improve/trigger 手动触发
5794
6225
  const selfImprove = process.env.BOLLOON_DEV_MODE === '1' || process.env.BOLLOON_DEV_MODE === 'true';
5795
6226
  if (selfImprove) {
5796
- console.log('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
6227
+ writeOut('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
5797
6228
  }
5798
6229
  const { createWebServer, openBrowser } = await import('./web/server.js');
5799
6230
  // 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
@@ -5808,7 +6239,7 @@ async function main() {
5808
6239
  console.info = originalInfo;
5809
6240
  process.stdout.write = originalStdoutWrite;
5810
6241
  s.info('执行命令...');
5811
- console.log();
6242
+ writeOut('');
5812
6243
  await runNonInteractive(args, comm);
5813
6244
  comm?.stop();
5814
6245
  // chat-watch / chat-p2p-listen 是长循环, 不会自然 return, 走 SIGINT 自然退出