@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
@@ -83,23 +83,53 @@ async function collectGit(cwd, limit) {
83
83
  uncommittedChanges: statusOut ? statusOut.split('\n').filter(Boolean).length : 0,
84
84
  };
85
85
  }
86
- async function collectPersona() {
87
- const home = process.env.HOME || os.homedir() || '/tmp';
86
+ /**
87
+ * 按 agent 取 persona (2026-10-01)。
88
+ *
89
+ * 用户实测: 233 会话的 get_identity 已经是「小龙」✓, 但**项目上下文**里仍显示 `## Persona: 小宝` ✗
90
+ * —— 那是**全局** `~/.bolloon/persona.json`(8/10 的老文件), 这里原先**无条件**读它 ✗。
91
+ * 规矩 (与 session-manager 的分流一致):
92
+ * ① 该 agent 有自己的 persona.json ⇒ 用它;
93
+ * ② 该 agent 有身份文档目录(persona/<agentId>/*.md) ⇒ **不注入 persona** (身份由文档承担, 别把全局名漏进来 ✗);
94
+ * ③ 既没 scope 又什么都没有 ⇒ 才回落全局 (兼容老流程)。
95
+ */
96
+ export async function resolvePersonaForScope(home, scopeId) {
97
+ const scope = String(scopeId || '').trim();
98
+ if (scope) {
99
+ const safe = scope.replace(/[^a-zA-Z0-9._-]/g, '_');
100
+ const dir = path.join(home, '.bolloon', 'persona', safe);
101
+ const own = await safeReadFile(path.join(dir, 'persona.json'), 5000);
102
+ if (own) {
103
+ try {
104
+ const p = JSON.parse(own);
105
+ return { name: String(p.name || 'unknown'), description: String(p.description || ''), personality: String(p.personality || '') };
106
+ }
107
+ catch { /* 坏了就往下走 */ }
108
+ }
109
+ // 有身份文档 ⇒ 不注入 persona (否则全局名会盖过该 agent 的身份 ✗)
110
+ try {
111
+ const entries = await fs.readdir(dir);
112
+ if (entries.some((f) => f.endsWith('.md')))
113
+ return null;
114
+ }
115
+ catch { /* 目录不存在 ⇒ 往下走 */ }
116
+ }
88
117
  const raw = await safeReadFile(path.join(home, '.bolloon', 'persona.json'), 5000);
89
118
  if (!raw)
90
119
  return null;
91
120
  try {
92
121
  const p = JSON.parse(raw);
93
- return {
94
- name: String(p.name || 'unknown'),
95
- description: String(p.description || ''),
96
- personality: String(p.personality || ''),
97
- };
122
+ return { name: String(p.name || 'unknown'), description: String(p.description || ''), personality: String(p.personality || '') };
98
123
  }
99
124
  catch {
100
125
  return null;
101
126
  }
102
127
  }
128
+ async function collectPersona() {
129
+ const home = process.env.HOME || os.homedir() || '/tmp';
130
+ // 当前 agent 由会话侧注入 (CLI 建/切 agent 时设置) ⇒ 项目上下文里的 persona 与之对齐
131
+ return resolvePersonaForScope(home, process.env.BOLLOON_ACTIVE_AGENT_ID);
132
+ }
103
133
  async function collectJudgmentsSummary(topN) {
104
134
  // 用动态 import 避免循环依赖
105
135
  const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
@@ -11,14 +11,23 @@ import { getCachedBolloonContext, clearBolloonContextCache } from './context-col
11
11
  import { formatContextForSystemPrompt } from './project-context.js';
12
12
  import { loadPersonaDocs, formatPersonaForSystemPrompt, loadPersonaJudgmentDeclaration, formatJudgmentDeclaration } from './persona-loader.js';
13
13
  let lastSessionStartAt = 0;
14
- const MIN_INTERVAL_MS = 5000; // 同一进程 5s 内最多触发一次, 防止循环
14
+ let lastIdentityKey = ''; // 上一次计算时的身份键 (agentId + channelId)
15
+ const MIN_INTERVAL_MS = 5000; // 同一身份 5s 内最多触发一次, 防止循环
15
16
  export async function onSessionStart(opts = {}) {
16
17
  const start = Date.now();
17
- if (start - lastSessionStartAt < MIN_INTERVAL_MS) {
18
- // 限流: 返回空 (调用方已经有缓存, 不需要重算)
18
+ // 节流必须**按身份**生效 (2026-10-01 修复):
19
+ // 旧实现只按时间节流 —— 5s 内第二次调用直接 `return { systemAddition: '' }`,
20
+ // 而 /channel 切换后重建 agent 只要几毫秒 ⇒ 新 agent 几乎必然落在窗口里,
21
+ // 拿到的**身份文档是空的** (persona 一个字都没有), 用户看到的就是
22
+ // 「切了智能体, 身份文档没换」。切了 agent/channel = 身份变了 ⇒ 必须重算。
23
+ // 同一身份连来两次仍走节流 (原意: 防循环); 上下文本身还有 cwd 级缓存兜底。
24
+ const identityKey = `${opts.agentId || ''}\u0000${opts.channelId || ''}`;
25
+ if (identityKey === lastIdentityKey && start - lastSessionStartAt < MIN_INTERVAL_MS) {
26
+ // 限流: 身份没变, 调用方此刻手里的就是这份身份的加法 (不需要重算)
19
27
  return { systemAddition: '', collectMs: 0, truncated: false };
20
28
  }
21
29
  lastSessionStartAt = start;
30
+ lastIdentityKey = identityKey;
22
31
  try {
23
32
  const ctx = await getCachedBolloonContext({ cwd: opts.cwd ?? process.cwd() }, opts.force ?? false);
24
33
  let systemAddition = formatContextForSystemPrompt(ctx, { maxChars: opts.maxChars });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * persona-init.ts — **确保每个 agent 名下有身份文档** (2026-10-01)
3
+ *
4
+ * 用户报: 「切换之后应该知道加载智能体初始化文档, 为什么目前是无, 所有回复都是一个智能体人格?」
5
+ * 真因: `~/.bolloon/persona/<agentId>/` 下**一个文件都没有** (用户四个 agent 全缺),
6
+ * `loadPersonaDocs(agentId)` 读回来 6 个字段全空 ⇒ 系统提示里只剩 `# Persona (agentId=…)` 这个头
7
+ * + 所有 agent 共用的 INJECT 工作纪律 ⇒ 换谁都一样。
8
+ * 而且仓里此前**没有任何"建身份文档"的路径** (只有读的 persona-loader + 一个模板 json + 测试助手)。
9
+ *
10
+ * 这里补上那条路: 按 `persona-loader` 认的 6 个文件名生成**起步文档**, 幂等 (已存在不覆盖 ——
11
+ * 用户写过的内容绝不能被模板盖掉)。
12
+ *
13
+ * 拿的是"能立刻用"的起步稿, 不是空壳: 每份都带该 agent 的名字与 agentId, 并**明确标注是模板**,
14
+ * 提示用户去编辑; 不同 agent 生成的正文必然不同 (有名字/agentId), 所以"所有人格一个样"从结构上就不成立。
15
+ */
16
+ import * as fs from 'node:fs/promises';
17
+ import { readFileSync } from 'node:fs';
18
+ import * as os from 'node:os';
19
+ import * as path from 'node:path';
20
+ /** 与 `persona-loader.ts` 的 FILE_KEYS 对齐 (读侧是唯一权威, 这里只复述文件名) */
21
+ export const PERSONA_DOC_FILES = ['soul', 'identity', 'project', 'user', 'agent', 'wiki'];
22
+ /** 与 persona-loader 的 sanitizeAgentId 同口径 */
23
+ export function sanitizePersonaAgentId(agentId) {
24
+ return String(agentId || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
25
+ }
26
+ export function personaDirOf(agentId, home) {
27
+ const root = home || os.homedir();
28
+ return path.join(root, '.bolloon', 'persona', sanitizePersonaAgentId(agentId));
29
+ }
30
+ /** 该 agent 自己的 persona.json 里的名字 (同步读; 没有就 undefined) */
31
+ export function readPersonaNameSync(agentId, home) {
32
+ try {
33
+ const p = path.join(personaDirOf(agentId, home), 'persona.json');
34
+ const j = JSON.parse(readFileSync(p, 'utf-8'));
35
+ return String(j?.name || '').trim() || undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ const TITLES = {
42
+ soul: 'soul — 我是谁 / 我认什么',
43
+ identity: 'identity — 身份与边界',
44
+ project: 'project — 我在做的项目',
45
+ user: 'user — 我在服务的人',
46
+ agent: 'agent — 我的工作方式',
47
+ wiki: 'wiki — 我的知识索引',
48
+ };
49
+ function starter(file, agentId, name) {
50
+ const head = `---\nagentId: ${sanitizePersonaAgentId(agentId)}\ntitle: ${TITLES[file]}\nstage: starter\n---\n\n`;
51
+ const body = {
52
+ soul: `# ${name} 的 soul\n\n`
53
+ + `> 这是**起步模板** (由 bolloon 在建/切 agent 时自动生成, 只生成一次)。请把它改成你自己的。\n\n`
54
+ // 2026-10-01: 标题行必须与**该 agent 自己的 persona.json** 一致 ——
55
+ // 之前用渠道名生成, 于是 soul.md 写「我是 233」而 persona.json 写「小龙」⇒ 用户看到"身份没匹配上" ✗
56
+ + `我是 **${name}**。\n\n`
57
+ + `- 我认什么: (待写 — 我的判断准则、我拒绝做什么)\n`
58
+ + `- 我说话的方式: (待写 — 语气/详略/爱用的结构)\n`
59
+ + `- 我不知道时会怎么做: (待写 — 先问 / 先查 / 先给假设并标注)\n`,
60
+ identity: `# ${name} 的身份与边界\n\n`
61
+ + `- agentId: \`${sanitizePersonaAgentId(agentId)}\`\n`
62
+ + `- 名字: ${name}\n`
63
+ + `- 归属: (待写 — 属于谁 / 代表谁)\n`
64
+ + `- 边界: (待写 — 哪些事我不能替人决定)\n`,
65
+ project: `# ${name} 在做的事\n\n`
66
+ + `- 当前目标: (待写)\n`
67
+ + `- 进行中的线: (待写)\n`
68
+ + `- 不做的事: (待写)\n`,
69
+ user: `# ${name} 服务的人\n\n`
70
+ + `- 偏好: (待写 — 语言/格式/节奏)\n`
71
+ + `- 忌讳: (待写)\n`,
72
+ agent: `# ${name} 的工作方式\n\n`
73
+ + `1. 先确认现状 (读真文件/真输出), 不靠记忆猜\n`
74
+ + `2. 改动前后都给出可核验的证据 (命令、数字、路径)\n`
75
+ + `3. 拿不准就说不确定, 并给出下一步怎么确认\n`,
76
+ wiki: `# ${name} 的知识索引\n\n`
77
+ + `- 稳定结论放这里; 过程留在会话/日志里\n`
78
+ + `- (待写) 我负责的领域与其唯一出处\n`,
79
+ };
80
+ return head + body[file];
81
+ }
82
+ /**
83
+ * 确保该 agent 名下有 6 份身份文档。**已存在的一律不碰** (幂等; 用户写过的内容不可被模板覆盖)。
84
+ * 失败不抛 —— 调用方 (getAgent 等) 不该因为写文档失败而崩; 返回已建/已保留的清单供如实汇报。
85
+ */
86
+ export async function ensurePersonaDocs(agentId, opts = {}) {
87
+ const id = String(agentId || '').trim();
88
+ const dir = personaDirOf(id, opts.home);
89
+ const out = { agentId: sanitizePersonaAgentId(id), dir, created: [], kept: [] };
90
+ if (!id)
91
+ return out;
92
+ const name = (opts.name || '').trim() || id;
93
+ try {
94
+ await fs.mkdir(dir, { recursive: true });
95
+ for (const f of PERSONA_DOC_FILES) {
96
+ const file = path.join(dir, `${f}.md`);
97
+ let exists = false;
98
+ try {
99
+ await fs.access(file);
100
+ exists = true;
101
+ }
102
+ catch {
103
+ exists = false;
104
+ }
105
+ if (exists) {
106
+ out.kept.push(f);
107
+ continue;
108
+ }
109
+ await fs.writeFile(file, starter(f, id, name), 'utf-8');
110
+ out.created.push(f);
111
+ }
112
+ }
113
+ catch {
114
+ // 静默: 写不进去不影响本次对话 (但调用方可通过 created/kept 看出没建起来)
115
+ }
116
+ return out;
117
+ }
118
+ /** agent 自己写的人格在文档里的标记区 (手写的部分在标记外, 永不覆盖) */
119
+ export const PERSONA_AUTO_BEGIN = '<!-- persona:auto:begin -->';
120
+ export const PERSONA_AUTO_END = '<!-- persona:auto:end -->';
121
+ /**
122
+ * **写透**: 把 agent 自己设的 persona 落进它的身份文档 (2026-10-01)。
123
+ *
124
+ * 为什么必须做: 有身份文档的 agent 不再套用 persona.json ⇒ 身份由 6 份文档承担。
125
+ * 而 `set_persona` 原先只写 persona.json ⇒ **agent 自己改的人格根本不进系统提示**,
126
+ * 用户的感受就是"每次让智能体改, 都是同一个"(各 agent 的文档还都是同一份模板)。
127
+ *
128
+ * 规矩: 只重写 `<!-- persona:auto -->` 标记**之内**的内容 —— 标记外是你手写的, 一个字都不动。
129
+ * 落在 `soul.md` (人格/基调) 与 `identity.md` (身份/边界) 两份里。
130
+ */
131
+ export async function applyPersonaToDocs(agentId, persona, opts = {}) {
132
+ const id = String(agentId || '').trim();
133
+ if (!id)
134
+ return [];
135
+ const dir = personaDirOf(id, opts.home);
136
+ // 目录可能还不存在 (该 agent 还没生成过身份文档) ⇒ 先建, 否则两份都写失败、静默返回空
137
+ try {
138
+ await fs.mkdir(dir, { recursive: true });
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ const block = [
144
+ PERSONA_AUTO_BEGIN,
145
+ `- 名字: ${persona.name || id}`,
146
+ persona.description ? `- 定位: ${persona.description}` : '',
147
+ persona.personality ? `- 性恪/基调: ${persona.personality}` : '',
148
+ persona.greeting ? `- 打招呼: ${persona.greeting}` : '',
149
+ persona.capabilities?.length ? `- 我能做: ${persona.capabilities.join(' · ')}` : '',
150
+ persona.interests?.length ? `- 我关心: ${persona.interests.join(' · ')}` : '',
151
+ PERSONA_AUTO_END,
152
+ ].filter(Boolean).join('\n');
153
+ const written = [];
154
+ for (const name of ['soul', 'identity']) {
155
+ const file = path.join(dir, `${name}.md`);
156
+ let cur = '';
157
+ try {
158
+ cur = await fs.readFile(file, 'utf-8');
159
+ }
160
+ catch {
161
+ cur = '';
162
+ }
163
+ const wrapped = /<!-- persona:auto:begin -->[\s\S]*?<!-- persona:auto:end -->/;
164
+ const next = wrapped.test(cur)
165
+ ? cur.replace(wrapped, block)
166
+ : `${cur.trimEnd()}\n\n${block}\n`;
167
+ try {
168
+ await fs.writeFile(file, next, 'utf-8');
169
+ written.push(name);
170
+ }
171
+ catch { /* 单份写失败不影响另一份 */ }
172
+ }
173
+ return written;
174
+ }
@@ -11,6 +11,7 @@
11
11
  * - formatPersonaForSystemPrompt 固定追加 INJECT 工作纪律段 (Context OS 读取协议),
12
12
  * 任何 channel 即使无 persona 文件也有纪律约束.
13
13
  */
14
+ import { sanitizeForLlm } from '../agents/egress-sanitize.js';
14
15
  import * as fs from 'fs/promises';
15
16
  import * as os from 'os';
16
17
  import * as path from 'path';
@@ -136,6 +137,9 @@ const OUTPUT_ORDER = ['identity', 'soul', 'project', 'user', 'agent', 'wiki'];
136
137
  * 纪律段不参与动态预算 — 即使没有 persona 文件也有纪律约束.
137
138
  */
138
139
  export function formatPersonaForSystemPrompt(docs, maxChars) {
140
+ // 2026-10-01 (落实③): 身份文档最终会**拼进给模型的提示** ⇒ 过**唯一出口**: 先脱敏(凭证一律打码)
141
+ // 再按上限中间省略。文档是用户/智能体自己写的, 里面完全可能出现私钥/助记词/URL 带密码。
142
+ const __sanitize = sanitizeForLlm;
139
143
  const cap = maxChars ?? DEFAULT_MAX_CHARS;
140
144
  // 段: 标识 + 字段值 (空字段跳过)
141
145
  const sections = [];
@@ -173,5 +177,5 @@ export function formatPersonaForSystemPrompt(docs, maxChars) {
173
177
  const truncateMarker = '\n... (截断)';
174
178
  result = result.substring(0, Math.max(0, cap - truncateMarker.length)) + truncateMarker;
175
179
  }
176
- return result;
180
+ return __sanitize(String(result), 8000);
177
181
  }
@@ -8,11 +8,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
8
8
  * ↑/↓ 导航, Tab/Enter 选中, Esc 关闭, 弹出窗打开时 TextInput 让出焦点 (focus=false)
9
9
  */
10
10
  import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
11
+ import { ruleFor } from './status-segments.js';
11
12
  import { loadInputHistory, appendInputHistory, MEMORY_CAP } from './input-history.js';
12
13
  import * as fs from 'fs';
13
14
  import { Static, render, Box, Text, useInput, useApp, useStdout } from 'ink';
15
+ import { stripUiNoise, collapsePaste, stripBracketedPaste, looksLikePasteChunk, logPasteChunk, shouldSuppressMention, singleLine, logPopupEvent, PASTE_MENTION_SHIELD_MS } from './input-paste.js';
14
16
  import TextInput from 'ink-text-input';
15
17
  import { dispWidth, LOADING_FRAMES as KAOMOJI } from './loading-tui.js';
18
+ import { reasoningMode } from './reasoning-view.js';
16
19
  import { THEME, fg } from './theme.js';
17
20
  import { COMPOSER_PLACEHOLDER, POPUP_TITLE_TAB, POPUP_TITLE_AGENT, POPUP_TITLE_FILE, POPUP_TITLE_COMMAND } from './content.js';
18
21
  import { DOUBLE_ESC_MS, STATUS_TICK_MS, THINK_FRAME_MS } from './timing.js';
@@ -105,6 +108,14 @@ const MentionPopup = ({ title, items, sel, width, loading, maxRows }) => {
105
108
  };
106
109
  const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
107
110
  const [input, setInput] = useState('');
111
+ /** 本次粘贴攒到的**原文**(只进文件, 不进输入框 ⇒ 输入框那份是引用) */
112
+ const pasteRawRef = useRef('');
113
+ /** 同一次粘贴复用同一个文件序号(避免每块都新开一个文件) */
114
+ const pasteSeqRef = useRef(null);
115
+ /** 粘贴后短暂封住补全弹窗(免得粘贴内容里的 '@'/'/'/'#' 弹窗抢焦点) */
116
+ const pasteShieldUntilRef = useRef(0);
117
+ /** 本次粘贴的**文件路径** —— 输入框那份不带路径(@/'/'/'#' 会触发弹窗 ✗), 提交时用它补上 ✓ */
118
+ const pastePathRef = useRef(null);
108
119
  // 2026-08-07: inputRef 同步镜像 input — useInput 回调拿最新值 (闭包里的 input 是陈旧的)
109
120
  const inputRef = useRef('');
110
121
  useEffect(() => {
@@ -130,9 +141,20 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
130
141
  const lastEscRef = useRef(0);
131
142
  const C_WARN_ANSI = fg(THEME.warn); // #f59e0b
132
143
  // ── @ / # 弹出窗状态 ──────────────────────────────────────────────────────
133
- const mention = useMemo(() => getMention(input), [input]);
144
+ // 2026-10-01: 粘贴引用本身 / 超长输入 / "刚粘贴过" 一律不当 mention 来源 (纯判断 ⇒ 弹窗**根本不会开** ⇒ 也就没有抢焦点/抖动 ✓)
145
+ const [pastedTick, setPastedTick] = useState(0);
146
+ const mention = useMemo(() => (shouldSuppressMention(input, pasteShieldUntilRef.current) ? null : getMention(input)), [input, pastedTick]);
134
147
  const mentionKey = mention ? `${mention.kind}:${mention.start}` : null;
135
148
  const [items, setItems] = useState([]);
149
+ // 2026-10-01: 弹窗观测 —— 实测证明 **mention 从未命中**(全 mention.none ✓); 且用户确认那个弹窗是
150
+ // **macOS 终端自己的"多行粘贴确认"** ✓(不是 bolloon 画的 ✗) ⇒ 所以只保留 mention 这一条观测即可 ✓
151
+ // (曾试过把 items/tabState/picker 也记上, 已撤掉: 目标已被证伪, 留着只是噪音 ✗ —— 减法 ✓)。
152
+ useEffect(() => {
153
+ if (mention)
154
+ void logPopupEvent({ ev: 'mention.hit', kind: mention.kind, trigger: mention.trigger, len: input.length, head: input });
155
+ else if (input.length > 0)
156
+ void logPopupEvent({ ev: 'mention.none', len: input.length, head: input });
157
+ }, [mentionKey, input.length]);
136
158
  const [sel, setSel] = useState(0);
137
159
  const [dismissed, setDismissed] = useState(null);
138
160
  const [loadingFiles, setLoadingFiles] = useState(false);
@@ -438,8 +460,17 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
438
460
  historyIdxRef.current = -1;
439
461
  draftRef.current = '';
440
462
  setInput('');
463
+ // 2026-10-01: 长输入/粘贴**折叠** —— 整段落文件, 对话流只留一行引用(指向文件 ⇒ 需要细节读得回来)
464
+ // 阈值: ≥8 行 或 ≥1200 字符; 短输入原样发送; 写盘失败退化成原样(绝不打断发送)。
465
+ let outgoing = stripUiNoise(trimmed); // 防 UI 残影(颜文字/思考中/ANSI)进上下文
466
+ try {
467
+ const collapsed = collapsePaste(trimmed);
468
+ if (collapsed.collapsed)
469
+ outgoing = collapsed.sendText;
470
+ }
471
+ catch { /* 折叠失败 = 原样发送 */ }
441
472
  // 用户消息由 processInput 统一通过 appendLine(renderUserMessage) 显示
442
- onPrompt(trimmed);
473
+ onPrompt(outgoing);
443
474
  }, [onPrompt]);
444
475
  /**
445
476
  * 输入历史: 上一/下一条。
@@ -481,6 +512,36 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
481
512
  requestExit();
482
513
  return;
483
514
  }
515
+ // ── ① 粘贴块**最先**处理 (2026-10-01) ──
516
+ // 必须在弹窗/picker/历史之前: 否则粘贴内容里的 TAB 会先被补全弹窗吃掉 ⇒ 弹窗打开 + 抢焦点 ✗(实测)。
517
+ // 实测形态: 一整块 762 字符纯文本(无括号标记/无换行) ⇒ 同步落文件 + 输入框整段设成"零触发字符"的引用 ✓。
518
+ // 2026-10-01 (第五版, 最终简化): **粘贴块 ⇒ 同步折叠成一个引用**。
519
+ // 为什么砍掉前面那套(定时器 + burst 状态 + 兜底冲洗): 真机上 80ms 定时器会**抖**(输入框变窄一下 ✗)
520
+ // 而且没冲上就**什么都看不到** ✗。现在: 不用定时器 ✗ 不用状态开关 ✗ ——
521
+ // 攒到的原文先落文件, 输入框**整段设成那一个引用**(不是追加) ⇒ 永远只有一行 ✓ 永远看得见 ✓
522
+ // 引用零触发字符 ⇒ 补全弹窗根本不会开 ✓ 也就没有"变窄"的位移 ✓。
523
+ {
524
+ const piece = stripBracketedPaste(_input);
525
+ if (piece && looksLikePasteChunk(_input)) {
526
+ pasteRawRef.current += piece;
527
+ let shown = singleLine(piece);
528
+ try {
529
+ const c = collapsePaste(pasteRawRef.current, { counter: pasteSeqRef.current ?? undefined });
530
+ if (c.collapsed) {
531
+ if (!pasteSeqRef.current)
532
+ pasteSeqRef.current = Number((/\d+/.exec(c.inputText) || ['0'])[0]) || null;
533
+ pastePathRef.current = c.path ?? null;
534
+ shown = c.inputText;
535
+ }
536
+ }
537
+ catch { /* 折叠失败 ⇒ 原样显示(至少看得见) */ }
538
+ pasteShieldUntilRef.current = Date.now() + PASTE_MENTION_SHIELD_MS;
539
+ setPastedTick(t => t + 1);
540
+ setInput(() => shown);
541
+ void logPasteChunk({ len: _input.length, marker: _input !== piece, nl: /[\n\r]/.test(_input), esc: _input.includes('\u001b'), tab: _input.includes('\t') });
542
+ return;
543
+ }
544
+ }
484
545
  // ── 输入历史优先 (比补全弹窗更优先) ──
485
546
  // 规则: 空输入, 或已经在历史态 (historyIdx !== -1) ⇒ ↑/↓ 归历史, 弹窗/选择器都让路。
486
547
  // 只有"用户自己敲了内容且不在历史态"时, 补全弹窗才拥有 ↑/↓。
@@ -542,9 +603,16 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
542
603
  }
543
604
  if ((key.return || /[\n\r]/.test(_input)) && filtered.length === 0) {
544
605
  // 弹窗无匹配项: Enter = 提交当前输入 (否则 /channel 无参 + Enter 永远提交不了 — 2026-08-06)
545
- const v = input.trim();
546
- if (v)
547
- onSubmit(v);
606
+ // 输入框里是**不含路径**的引用(@/'/'/'#' 会触发弹窗 ✗) ⇒ 提交时把路径与"要看细节读它"补上 ✓
607
+ const refText = input.trim();
608
+ const full = (pastePathRef.current && /^\[粘贴[^\]]*\]$/.test(refText))
609
+ ? `${refText}\n(整段已存到 ${pastePathRef.current} —— 需要看细节就 read_file 读它, 不用我重述)`
610
+ : refText;
611
+ pasteRawRef.current = '';
612
+ pasteSeqRef.current = null;
613
+ pastePathRef.current = null;
614
+ if (full)
615
+ onSubmit(full);
548
616
  else
549
617
  setDismissed(mentionKey);
550
618
  return;
@@ -588,6 +656,10 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
588
656
  return;
589
657
  }
590
658
  if (_input && !key.ctrl && !key.meta && !key.return) {
659
+ if (pasteShieldUntilRef.current) {
660
+ pasteShieldUntilRef.current = 0;
661
+ setPastedTick(t => t + 1);
662
+ }
591
663
  setInput(cur => cur + _input);
592
664
  return;
593
665
  }
@@ -767,7 +839,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
767
839
  }
768
840
  catch { /* 探针写不进去绝不影响界面 */ }
769
841
  });
770
- return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) })] })] }));
842
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsx(Text, { color: "yellow", children: reasoningMode() === 'trace' ? '思考中...' : `${KAOMOJI[thinkingIdx.current]} 思考中...` })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: singleLine(input), onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) })] })] }));
771
843
  };
772
844
  export { InkApp };
773
845
  // ─── 启动 ────────────────────────────────────────────────────────────────────
@@ -778,6 +850,8 @@ export function startInk(onPrompt, initialStatus, getStatusUpdate) {
778
850
  const tw = process.stdout.columns || 80;
779
851
  const th = process.stdout.rows || 24;
780
852
  _lastInkArgs = { onPrompt, initialStatus, getStatusUpdate };
853
+ // 2026-10-01: 接管 console —— 别的模块(p2p/iroh 等)的直写也一律进 Ink 消息流, 不再推 footer ✓
854
+ installConsoleIntercept();
781
855
  _inkInstance = render(_jsx(InkApp, { onPrompt: onPrompt, initialStatus: initialStatus, getStatusUpdate: getStatusUpdate, terminalW: tw, terminalH: th }), {
782
856
  stdout: process.stdout,
783
857
  stdin: process.stdin,
@@ -825,6 +899,7 @@ export function stopInk() {
825
899
  _inkInstance = null;
826
900
  }
827
901
  _lastInkArgs = null;
902
+ uninstallConsoleIntercept(); // 2026-10-01: 退出后把 console 还给系统(之后照常能打印 ✓)
828
903
  }
829
904
  export function inkAppendLine(line) {
830
905
  appendMsg(line);
@@ -849,3 +924,75 @@ export function inkSetThinking(v) {
849
924
  export function inkSetTransient(v) {
850
925
  setUiTransient(v);
851
926
  }
927
+ /**
928
+ * **进程级 console 拦截** (2026-10-01, 用户: 「有新的需要拦截」)。
929
+ * 为什么: 上一轮只把 `src/index.ts` 的直写收口了 ✗ —— 但启动那几行(`iroh: …` · `主题: …` ·
930
+ * `✓ [4/5] 启动 iroh P2P`)来自**别的模块** ✓(`src/network/p2p.ts` 24 处 · `iroh-integration.ts` 7 处 ✓),
931
+ * 它们绕过统一出口 ⇒ 照样把 footer 推下去(重复的提示行 ✓)。
932
+ * 做法: 在 **Ink 启动那一刻**接管 `console.log/info/warn/error` ✓ —— Ink 在跑 ⇒ 一律走它的消息流 ✓;
933
+ * **绝不动 `process.stdout.write`** ✗(Ink 自己要用 ✓)。卸载时恢复原函数 ✓(退出后照样能打印 ✓)。
934
+ * 递归保护 + 长度上限 ✓(任何异常都退回原函数 ✓, 不许因为拦截而丢日志 ✓)。
935
+ */
936
+ let _consoleOriginals = null;
937
+ let _interceptBusy = false;
938
+ /** 把一串 console 参数压成一行(剥 ANSI · 对象用紧凑 JSON · 有上限 ✓) */
939
+ export function formatConsoleArgs(args) {
940
+ const one = (v) => {
941
+ if (typeof v === 'string')
942
+ return v;
943
+ if (v instanceof Error)
944
+ return v.message;
945
+ try {
946
+ return JSON.stringify(v);
947
+ }
948
+ catch {
949
+ return String(v);
950
+ }
951
+ };
952
+ return args.map(one).join(' ')
953
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
954
+ .replace(/\s+/g, ' ')
955
+ .trim()
956
+ .slice(0, 1000);
957
+ }
958
+ export function installConsoleIntercept() {
959
+ if (_consoleOriginals)
960
+ return; // 已装过 ⇒ 幂等 ✓
961
+ _consoleOriginals = { log: console.log, info: console.info, warn: console.warn, error: console.error };
962
+ const route = (level) => (...args) => {
963
+ if (_interceptBusy) {
964
+ _consoleOriginals.log(...args);
965
+ return;
966
+ }
967
+ const line = formatConsoleArgs(args);
968
+ if (!line)
969
+ return;
970
+ _interceptBusy = true;
971
+ try {
972
+ // 归类前缀让来源一眼可辨 ✓(不猜内容, 只标级别 ✓)
973
+ appendMsg(level === 'log' || level === 'info' ? line : `${level === 'warn' ? '⚠ ' : '✗ '}${line}`);
974
+ }
975
+ catch {
976
+ try {
977
+ _consoleOriginals.log(...args);
978
+ }
979
+ catch { /* 真打不出去就算了 */ }
980
+ }
981
+ finally {
982
+ _interceptBusy = false;
983
+ }
984
+ };
985
+ console.log = route('log');
986
+ console.info = route('info');
987
+ console.warn = route('warn');
988
+ console.error = route('error');
989
+ }
990
+ export function uninstallConsoleIntercept() {
991
+ if (!_consoleOriginals)
992
+ return;
993
+ console.log = _consoleOriginals.log;
994
+ console.info = _consoleOriginals.info;
995
+ console.warn = _consoleOriginals.warn;
996
+ console.error = _consoleOriginals.error;
997
+ _consoleOriginals = null;
998
+ }