@bolloon/bolloon-agent 0.1.34 → 0.1.36

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 (226) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  4. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  5. package/Bolloon.md +144 -0
  6. package/Dive-into/CITATION.cff +17 -0
  7. package/Dive-into/LICENSE +25 -0
  8. package/Dive-into/README.md +598 -0
  9. package/Dive-into/README_zh.md +599 -0
  10. package/Dive-into/assets/context.png +0 -0
  11. package/Dive-into/assets/extensibility.png +0 -0
  12. package/Dive-into/assets/iteration.png +0 -0
  13. package/Dive-into/assets/layered_architecture.png +0 -0
  14. package/Dive-into/assets/main_structure.png +0 -0
  15. package/Dive-into/assets/permission.png +0 -0
  16. package/Dive-into/assets/session_compact.png +0 -0
  17. package/Dive-into/assets/subagent.png +0 -0
  18. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  19. package/README.md +1 -1
  20. package/dist/agents/p2p-chat-tools.js +6 -6
  21. package/dist/agents/permission-mode.js +115 -0
  22. package/dist/agents/pi-sdk.js +687 -25
  23. package/dist/agents/pre-tool-validator.js +194 -0
  24. package/dist/agents/workflow-pivot-loop.js +113 -12
  25. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  26. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  27. package/dist/bollharness/src/scripts/context_router.js +1 -1
  28. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  29. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  30. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  31. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  32. package/dist/bollharness-integration/context-router.js +2 -2
  33. package/dist/bollharness-integration/guard-checker.js +1 -1
  34. package/dist/bootstrap/bootstrap.js +117 -0
  35. package/dist/bootstrap/context-collector.js +312 -0
  36. package/dist/bootstrap/context-hierarchy.js +218 -0
  37. package/dist/bootstrap/lifecycle-hooks.js +171 -0
  38. package/dist/bootstrap/project-context.js +158 -0
  39. package/dist/context-compaction/auto-compact.js +144 -0
  40. package/dist/context-compaction/budget-gate.js +28 -0
  41. package/dist/context-compaction/budget-reduce.js +35 -0
  42. package/dist/context-compaction/context-collapse.js +66 -0
  43. package/dist/context-compaction/index.js +21 -0
  44. package/dist/context-compaction/microcompact.js +51 -0
  45. package/dist/context-compaction/pipeline.js +123 -0
  46. package/dist/context-compaction/snip.js +45 -0
  47. package/dist/context-compaction/token-estimator.js +35 -0
  48. package/dist/context-compaction/types.js +19 -0
  49. package/dist/heartbeat/HealthMonitor.js +3 -2
  50. package/dist/index.js +49 -2
  51. package/dist/llm/llm-judgment-client.js +32 -30
  52. package/dist/llm/pi-ai.js +125 -28
  53. package/dist/llm/system-prompt/health.js +129 -0
  54. package/dist/llm/system-prompt/registry.js +246 -0
  55. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  56. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  57. package/dist/llm/tool-manifest/bash.js +23 -0
  58. package/dist/llm/tool-manifest/create_file.js +24 -0
  59. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  60. package/dist/llm/tool-manifest/image_search.js +24 -0
  61. package/dist/llm/tool-manifest/index.js +69 -0
  62. package/dist/llm/tool-manifest/mcp.js +43 -0
  63. package/dist/llm/tool-manifest/message_compose.js +31 -0
  64. package/dist/llm/tool-manifest/places.js +83 -0
  65. package/dist/llm/tool-manifest/present_files.js +21 -0
  66. package/dist/llm/tool-manifest/recipe.js +40 -0
  67. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  68. package/dist/llm/tool-manifest/str_replace.js +27 -0
  69. package/dist/llm/tool-manifest/types.js +7 -0
  70. package/dist/llm/tool-manifest/view.js +24 -0
  71. package/dist/llm/tool-manifest/weather.js +27 -0
  72. package/dist/llm/tool-manifest/web.js +51 -0
  73. package/dist/network/p2p-direct.js +23 -0
  74. package/dist/network/source-intent-broadcaster.js +203 -0
  75. package/dist/network/source-intent.js +100 -0
  76. package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
  77. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  78. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  79. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  80. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  81. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  82. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  83. package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
  84. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  85. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  86. package/dist/security/builtin-guards.js +124 -0
  87. package/dist/security/context-router-tool.js +106 -0
  88. package/dist/security/input-scanner.js +223 -0
  89. package/dist/security/react-harness.js +143 -0
  90. package/dist/security/tool-gate.js +235 -0
  91. package/dist/utils/auto-evolve-policy.js +117 -0
  92. package/dist/utils/clamp.js +7 -0
  93. package/dist/utils/double.js +6 -0
  94. package/dist/web/client.js +3810 -3830
  95. package/dist/web/components/p2p/P2PModal.js +188 -0
  96. package/dist/web/components/p2p/index.js +264 -226
  97. package/dist/web/components/p2p/p2p-modal.js +657 -0
  98. package/dist/web/components/p2p/p2p-tools.js +248 -0
  99. package/dist/web/index.html +60 -49
  100. package/dist/web/server.js +827 -124
  101. package/dist/web/style.css +531 -249
  102. package/dist/web/ui/message-renderer.js +463 -0
  103. package/dist/web/ui/step-timeline.js +375 -0
  104. package/lefthook.yml +33 -0
  105. package/package.json +3 -2
  106. package/scripts/auto-evolve-loop.ts +481 -0
  107. package/scripts/auto-evolve-oneshot.sh +155 -0
  108. package/scripts/auto-evolve-snapshot.sh +136 -0
  109. package/scripts/build-web.ts +35 -1
  110. package/scripts/detect-schema-changes.sh +48 -0
  111. package/scripts/diff-reviewer.ts +159 -0
  112. package/scripts/validate-system-prompt.ts +142 -0
  113. package/scripts/weekly-report.ts +364 -0
  114. package/src/agents/p2p-chat-tools.ts +6 -6
  115. package/src/agents/permission-mode.ts +127 -0
  116. package/src/agents/pi-sdk.ts +741 -30
  117. package/src/agents/pre-tool-validator.ts +213 -0
  118. package/src/agents/workflow-pivot-loop.ts +110 -19
  119. package/src/bollharness/CLAUDE.md +1 -1
  120. package/src/bollharness/README.md +2 -2
  121. package/src/bollharness/README.zh-CN.md +2 -2
  122. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  123. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  124. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  125. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  126. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  127. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  128. package/src/bollharness/src/scripts/context_router.ts +1 -1
  129. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  130. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  131. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  132. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  133. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  134. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  135. package/src/bollharness-integration/context-router.ts +2 -2
  136. package/src/bollharness-integration/guard-checker.ts +1 -1
  137. package/src/bootstrap/bootstrap.ts +135 -0
  138. package/src/bootstrap/context-collector.ts +371 -0
  139. package/src/bootstrap/context-hierarchy.ts +283 -0
  140. package/src/bootstrap/lifecycle-hooks.ts +289 -0
  141. package/src/bootstrap/project-context.ts +171 -0
  142. package/src/context-compaction/auto-compact.ts +153 -0
  143. package/src/context-compaction/budget-gate.ts +32 -0
  144. package/src/context-compaction/budget-reduce.ts +37 -0
  145. package/src/context-compaction/context-collapse.ts +72 -0
  146. package/src/context-compaction/index.ts +24 -0
  147. package/src/context-compaction/microcompact.ts +54 -0
  148. package/src/context-compaction/pipeline.ts +133 -0
  149. package/src/context-compaction/snip.ts +51 -0
  150. package/src/context-compaction/token-estimator.ts +36 -0
  151. package/src/context-compaction/types.ts +99 -0
  152. package/src/heartbeat/HealthMonitor.ts +3 -2
  153. package/src/index.ts +47 -2
  154. package/src/llm/llm-judgment-client.ts +36 -35
  155. package/src/llm/pi-ai.ts +135 -29
  156. package/src/llm/system-prompt/health.ts +159 -0
  157. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  158. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  159. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  160. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  161. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  162. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  163. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  164. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  165. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  166. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  167. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  168. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  169. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  170. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  171. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  172. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  173. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  174. package/src/llm/system-prompt/layers/role/security.md +15 -0
  175. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  176. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  177. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  178. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  179. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  180. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  181. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  182. package/src/llm/system-prompt/registry.ts +325 -0
  183. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  184. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  185. package/src/llm/tool-manifest/bash.ts +25 -0
  186. package/src/llm/tool-manifest/create_file.ts +26 -0
  187. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  188. package/src/llm/tool-manifest/image_search.ts +26 -0
  189. package/src/llm/tool-manifest/index.ts +88 -0
  190. package/src/llm/tool-manifest/mcp.ts +46 -0
  191. package/src/llm/tool-manifest/message_compose.ts +33 -0
  192. package/src/llm/tool-manifest/places.ts +86 -0
  193. package/src/llm/tool-manifest/present_files.ts +23 -0
  194. package/src/llm/tool-manifest/recipe.ts +42 -0
  195. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  196. package/src/llm/tool-manifest/str_replace.ts +29 -0
  197. package/src/llm/tool-manifest/types.ts +52 -0
  198. package/src/llm/tool-manifest/view.ts +26 -0
  199. package/src/llm/tool-manifest/weather.ts +29 -0
  200. package/src/llm/tool-manifest/web.ts +54 -0
  201. package/src/network/p2p-direct.ts +22 -0
  202. package/src/network/source-intent-broadcaster.ts +242 -0
  203. package/src/network/source-intent.ts +167 -0
  204. package/src/security/builtin-guards.ts +162 -0
  205. package/src/security/context-router-tool.ts +122 -0
  206. package/src/security/input-scanner.ts +287 -0
  207. package/src/security/react-harness.ts +177 -0
  208. package/src/security/tool-gate.ts +294 -0
  209. package/src/utils/auto-evolve-policy.ts +138 -0
  210. package/src/utils/clamp.ts +5 -0
  211. package/src/web/client.js +105 -2187
  212. package/src/web/client.ts +4326 -0
  213. package/src/web/index.html +60 -49
  214. package/src/web/server.ts +894 -148
  215. package/src/web/style.css +531 -249
  216. package/src/web/ui/message-renderer.ts +540 -0
  217. package/src/web/ui/step-timeline.ts +394 -0
  218. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  219. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  220. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  221. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  222. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  223. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  224. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
  225. package/test-results/.last-run.json +6 -0
  226. package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import * as fs from 'fs/promises';
6
6
  import * as fsSync from 'fs';
7
7
  import * as path from 'path';
8
+ import * as os from 'os';
8
9
  import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, } from '@diap/sdk';
9
10
  import { documentReader } from '../documents/reader.js';
10
11
  import { initMinimax, getMinimax } from '../constraints/index.js';
@@ -30,15 +31,15 @@ function resolveWebRoot() {
30
31
  }
31
32
  const d = _baseDirname;
32
33
  const candidates = [
33
- path.join(d), // dist/web
34
- path.join(d, '..', '..', 'dist', 'web'), // src/web dist/web
34
+ path.join(d, '..', '..', 'dist', 'web'), // 2026-06-15 优先: src/web → dist/web (build 产物)
35
+ path.join(d), // dist/web (npm 跑时)
35
36
  path.join(d, '..', 'web'), // dist/ → web/ 兄弟
36
37
  ];
37
38
  for (const c of candidates) {
38
39
  if (fsSync.existsSync(path.join(c, 'index.html')))
39
40
  return c;
40
41
  }
41
- return candidates[1];
42
+ return candidates[0];
42
43
  }
43
44
  const webRoot = resolveWebRoot();
44
45
  console.log(`[web] webRoot = ${webRoot}`);
@@ -139,7 +140,6 @@ async function saveTheme(theme, agentId) {
139
140
  }
140
141
  // ==================== Task Queue & Workflow System ====================
141
142
  const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
142
- const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
143
143
  async function loadTaskQueue() {
144
144
  try {
145
145
  const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
@@ -152,33 +152,6 @@ async function loadTaskQueue() {
152
152
  async function saveTaskQueue(tasks) {
153
153
  await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
154
154
  }
155
- async function loadWorkflowState(channelId) {
156
- try {
157
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
158
- const states = JSON.parse(data);
159
- return states.find(s => s.channelId === channelId) || null;
160
- }
161
- catch {
162
- return null;
163
- }
164
- }
165
- async function saveWorkflowState(state) {
166
- try {
167
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
168
- const states = JSON.parse(data);
169
- const index = states.findIndex(s => s.channelId === state.channelId);
170
- if (index >= 0) {
171
- states[index] = state;
172
- }
173
- else {
174
- states.push(state);
175
- }
176
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
177
- }
178
- catch {
179
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
180
- }
181
- }
182
155
  let isExecutingTask = false;
183
156
  let executionTaskId = null;
184
157
  async function executeTask(task, channelId) {
@@ -220,32 +193,6 @@ async function executeTask(task, channelId) {
220
193
  result = `📝 文档总结:\n\n${summary.summary}`;
221
194
  }
222
195
  break;
223
- case 'workflow':
224
- // 执行多步骤工作流
225
- if (task.steps && task.steps.length > 0) {
226
- let loopCount = 0;
227
- for (let i = 0; i < task.steps.length; i++) {
228
- // 广播循环开始
229
- loopCount++;
230
- broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
231
- task.steps[i].status = 'running';
232
- broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
233
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
234
- // 执行步骤 - 模拟流式输出
235
- for (let j = 0; j < 3; j++) {
236
- await new Promise(resolve => setTimeout(resolve, 300));
237
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
238
- }
239
- task.steps[i].status = 'completed';
240
- task.progress = Math.round(((i + 1) / task.steps.length) * 100);
241
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
242
- broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
243
- broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
244
- }
245
- result = '✅ 工作流执行完成';
246
- broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
247
- }
248
- break;
249
196
  default:
250
197
  result = '未知任务类型';
251
198
  }
@@ -631,7 +578,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
631
578
  const { getMinimax } = await import('../constraints/index.js');
632
579
  const llm = getMinimax();
633
580
  // v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
634
- const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "主人".\n\n`;
581
+ const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "用户".\n\n`;
635
582
  // v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
636
583
  let dirHint = '';
637
584
  const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
@@ -653,11 +600,20 @@ async function handleV3P2PMessage(parsed, conn, comm) {
653
600
  }
654
601
  dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
655
602
  }
656
- const fullPrompt = `${visitorHint}${dirHint}${judgmentHint}${text}`;
603
+ // 2026-06-15: P2P 远端访客路径也用显式 marker 包裹 text
604
+ // 2026-06-15 二次修: 把 text 放在最前 (与主路径 server.ts:1868 对齐),
605
+ // 避免 LLM 被 judgmentHint 末尾的 "..." 误判为整个 input 被截断
606
+ const fullPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${visitorHint}${dirHint}${judgmentHint}\n`;
657
607
  let fullResponse = '';
658
608
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
659
609
  let lastFlushAt = 0;
610
+ let usedJudgmentIds = [];
660
611
  const streamCallback = (event) => {
612
+ // P0.5: 注入门回传
613
+ if (event?.type === 'used_judgments' && Array.isArray(event.usedIds)) {
614
+ usedJudgmentIds = event.usedIds;
615
+ return;
616
+ }
661
617
  if (event.type === 'token') {
662
618
  fullResponse += event.content;
663
619
  if (fullResponse.length - lastFlushAt >= 20) {
@@ -671,7 +627,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
671
627
  }
672
628
  };
673
629
  const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
674
- fullResponse = await agent.promptStream(fullPrompt, streamCallback);
630
+ fullResponse = await agent.promptStream(fullPrompt, streamCallback, undefined, channelId);
675
631
  // v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
676
632
  try {
677
633
  const existing = await loadSession(channelId, 'default');
@@ -682,6 +638,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
682
638
  id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
683
639
  type: 'ai',
684
640
  content: fullResponse,
641
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
685
642
  timestamp: new Date().toISOString()
686
643
  });
687
644
  session.lastUpdated = new Date().toISOString();
@@ -967,6 +924,16 @@ export async function createWebServer(port = 3000, options = {}) {
967
924
  process.on('unhandledRejection', (reason, promise) => {
968
925
  console.error('[警告] 未处理的 Promise 拒绝:', reason);
969
926
  });
927
+ // Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
928
+ // 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
929
+ try {
930
+ const { bootstrapBolloon } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
931
+ const bs = await bootstrapBolloon({ cwd: process.cwd() });
932
+ console.log(`[createWebServer] bootstrap 完成 (${bs.durationMs}ms)`);
933
+ }
934
+ catch (err) {
935
+ console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
936
+ }
970
937
  // 重置旧的 agent session,确保使用新的 LLM 配置
971
938
  const { resetAgentSession } = await import('../agents/pi-sdk.js');
972
939
  resetAgentSession();
@@ -1410,10 +1377,40 @@ export async function createWebServer(port = 3000, options = {}) {
1410
1377
  const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
1411
1378
  // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
1412
1379
  const channelForJudgment = channel;
1380
+ // per-channel queue 检查: 已在跑就入队, 等当前跑完自动接上
1381
+ const runState = getOrCreateRunState(channelId);
1382
+ if (runState.running) {
1383
+ runState.queue.push({ channelId, text, boundWalletAddress, autoToolsEnabled });
1384
+ broadcastQueueUpdate(channelId);
1385
+ console.log(`[queue] /message 入队 channel=${channelId}, queue len=${runState.queue.length}`);
1386
+ return;
1387
+ }
1388
+ runState.running = true;
1389
+ runState.abortController = new AbortController();
1390
+ broadcastQueueUpdate(channelId);
1413
1391
  try {
1414
1392
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
1415
1393
  let fullResponse = '';
1394
+ // P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
1395
+ let usedJudgmentIds = [];
1416
1396
  const streamCallback = (event) => {
1397
+ // P0.5: 捕获注入门回传
1398
+ if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
1399
+ usedJudgmentIds = event.usedIds;
1400
+ // 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
1401
+ broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
1402
+ return;
1403
+ }
1404
+ // 阶段事件 (注入门 / D 触发)
1405
+ if (event.type === 'phase') {
1406
+ broadcast({
1407
+ type: 'phase',
1408
+ phase: event.phase,
1409
+ detail: event.detail,
1410
+ usedCount: event.usedCount,
1411
+ }, channelId);
1412
+ return;
1413
+ }
1417
1414
  // 同时发送给流式显示和工作流显示
1418
1415
  if (event.type === 'token' || event.type === 'thinking') {
1419
1416
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
@@ -1427,6 +1424,35 @@ export async function createWebServer(port = 3000, options = {}) {
1427
1424
  broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
1428
1425
  console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
1429
1426
  }
1427
+ else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
1428
+ // 2026-06-15: 步骤状态机事件 — 原样转发 (前端 step-timeline 组件订阅)
1429
+ broadcast({
1430
+ type: event.type,
1431
+ tool: event.tool,
1432
+ content: event.content,
1433
+ success: event.success,
1434
+ output: event.output,
1435
+ error: event.error,
1436
+ args: event.args,
1437
+ }, channelId);
1438
+ // 2026-06-16: 累积 step 到 runState, 供 /api/loop/inspect 读取
1439
+ try {
1440
+ if (event.type === 'step_done' || event.type === 'step_error') {
1441
+ const rs = channelRunState.get(channelId);
1442
+ if (rs) {
1443
+ if (!rs.lastSteps)
1444
+ rs.lastSteps = [];
1445
+ rs.lastSteps.push({
1446
+ name: String(event.tool || event.content || 'step').slice(0, 60),
1447
+ status: event.type === 'step_error' ? 'failed' : (event.success === false ? 'failed' : 'ok'),
1448
+ durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
1449
+ output: event.output ? String(event.output).slice(0, 800) : (event.error ? String(event.error).slice(0, 800) : undefined),
1450
+ });
1451
+ }
1452
+ }
1453
+ }
1454
+ catch { /* non-fatal */ }
1455
+ }
1430
1456
  else if (event.type === 'error') {
1431
1457
  broadcast({ type: 'error', content: event.content }, channelId);
1432
1458
  }
@@ -1437,7 +1463,7 @@ export async function createWebServer(port = 3000, options = {}) {
1437
1463
  if (realChannelDid)
1438
1464
  contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
1439
1465
  // v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
1440
- contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "主人" 即可.\n`;
1466
+ contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
1441
1467
  if (boundWalletAddress) {
1442
1468
  contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
1443
1469
  }
@@ -1583,7 +1609,28 @@ export async function createWebServer(port = 3000, options = {}) {
1583
1609
  }
1584
1610
  if (contextHint)
1585
1611
  contextHint += '\n';
1586
- fullResponse = await agent.promptStream(contextHint + text, streamCallback);
1612
+ try {
1613
+ // 2026-06-15: 把 user text 单独 marker 包起来, LLM 不会被 8K+ 的 system context 吞掉
1614
+ // (之前 contextHint + text 拼成一整段当 user role, 24 字符的 user input 埋在 8K+ 里看不出)
1615
+ // 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
1616
+ // 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
1617
+ // 的 "..." 误判为整个 input 截断)
1618
+ const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
1619
+ fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
1620
+ }
1621
+ catch (err) {
1622
+ // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
1623
+ if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
1624
+ console.log(`[chat] aborted channel=${channelId}`);
1625
+ }
1626
+ else {
1627
+ throw err;
1628
+ }
1629
+ }
1630
+ // abort 模式: 给 partial 拼后缀
1631
+ if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
1632
+ fullResponse = fullResponse + '\n\n_[生成已中断]_';
1633
+ }
1587
1634
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
1588
1635
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
1589
1636
  broadcast({ type: 'ai', content: fullResponse }, channelId);
@@ -1592,7 +1639,15 @@ export async function createWebServer(port = 3000, options = {}) {
1592
1639
  session.sessionId = currentSessionId;
1593
1640
  // v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
1594
1641
  session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString(), source: 'local' });
1595
- session.messages.push({ id: crypto.randomUUID(), type: 'ai', content: fullResponse, timestamp: new Date().toISOString(), source: 'local' });
1642
+ session.messages.push({
1643
+ id: crypto.randomUUID(),
1644
+ type: 'ai',
1645
+ content: fullResponse,
1646
+ timestamp: new Date().toISOString(),
1647
+ source: 'local',
1648
+ // P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
1649
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
1650
+ });
1596
1651
  session.lastUpdated = new Date().toISOString();
1597
1652
  await saveSession(session);
1598
1653
  const channels = await loadChannels();
@@ -1605,6 +1660,46 @@ export async function createWebServer(port = 3000, options = {}) {
1605
1660
  await saveChannels(channels);
1606
1661
  }
1607
1662
  broadcast({ type: 'done' }, channelId);
1663
+ // D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
1664
+ setImmediate(() => {
1665
+ try {
1666
+ const lastTurns = session.messages.slice(-6).map((m) => ({
1667
+ role: (m.type === 'user' ? 'human' : 'agent'),
1668
+ content: m.content,
1669
+ }));
1670
+ if (lastTurns.length < 2)
1671
+ return;
1672
+ broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
1673
+ import('../pi-ecosystem-judgment/human-value-pipeline.js')
1674
+ .then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
1675
+ // channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
1676
+ if (!throttleDHook(channelId, 5 * 60_000)) {
1677
+ console.log(`[D-hook ${channelId}] throttled (within 5min)`);
1678
+ broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
1679
+ return null;
1680
+ }
1681
+ broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
1682
+ return detectAndDistillFromChannel(lastTurns, { channelId });
1683
+ })
1684
+ .then((result) => {
1685
+ if (result && result.triggered) {
1686
+ console.log(`[D-hook ${channelId}] stored: ${result.reason}`, result.evolved);
1687
+ broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
1688
+ }
1689
+ else if (result && result.reason) {
1690
+ console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
1691
+ broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
1692
+ }
1693
+ })
1694
+ .catch((err) => {
1695
+ console.warn(`[D-hook ${channelId}] failed:`, err);
1696
+ broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
1697
+ });
1698
+ }
1699
+ catch (err) {
1700
+ console.warn(`[D-hook ${channelId}] sync error:`, err);
1701
+ }
1702
+ });
1608
1703
  // 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
1609
1704
  if (!res.headersSent)
1610
1705
  res.json({ ok: true });
@@ -1615,6 +1710,15 @@ export async function createWebServer(port = 3000, options = {}) {
1615
1710
  if (!res.headersSent)
1616
1711
  res.status(500).json({ error: err.message });
1617
1712
  }
1713
+ finally {
1714
+ // queue dequeue: 跑完或失败都要清状态
1715
+ // 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
1716
+ // 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
1717
+ // (在 client.js 实现). 这里只清状态 + 广播.
1718
+ runState.running = false;
1719
+ runState.abortController = null;
1720
+ broadcastQueueUpdate(channelId);
1721
+ }
1618
1722
  });
1619
1723
  // ---------- 频道元数据后台修复队列 ----------
1620
1724
  // 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
@@ -1623,6 +1727,24 @@ export async function createWebServer(port = 3000, options = {}) {
1623
1727
  const didFixQueue = new Set(); // 待修复的 channelId
1624
1728
  let didFixRunning = false;
1625
1729
  let didFixTimer = null;
1730
+ const channelRunState = new Map();
1731
+ function getOrCreateRunState(channelId) {
1732
+ let s = channelRunState.get(channelId);
1733
+ if (!s) {
1734
+ s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
1735
+ channelRunState.set(channelId, s);
1736
+ }
1737
+ return s;
1738
+ }
1739
+ function broadcastQueueUpdate(channelId) {
1740
+ const s = channelRunState.get(channelId);
1741
+ const queueLength = s ? s.queue.length : 0;
1742
+ const running = s ? s.running : false;
1743
+ try {
1744
+ broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId);
1745
+ }
1746
+ catch { /* */ }
1747
+ }
1626
1748
  function scheduleDidFix(channelId) {
1627
1749
  didFixQueue.add(channelId);
1628
1750
  if (didFixTimer)
@@ -2312,20 +2434,40 @@ export async function createWebServer(port = 3000, options = {}) {
2312
2434
  broadcast({ type: 'regenerating', channelId }, channelId);
2313
2435
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
2314
2436
  let fullResponse = '';
2437
+ let usedJudgmentIds = [];
2315
2438
  const streamCallback = (event) => {
2439
+ // P0.5: 注入门回传
2440
+ if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
2441
+ usedJudgmentIds = event.usedIds;
2442
+ return;
2443
+ }
2316
2444
  if (event.type === 'token' || event.type === 'thinking') {
2317
2445
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
2318
2446
  }
2319
2447
  else if (event.type === 'status' || event.type === 'tool') {
2320
2448
  broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
2321
2449
  }
2450
+ else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
2451
+ // 2026-06-15: 步骤状态机事件 — 原样转发
2452
+ broadcast({
2453
+ type: event.type,
2454
+ tool: event.tool,
2455
+ content: event.content,
2456
+ success: event.success,
2457
+ output: event.output,
2458
+ error: event.error,
2459
+ args: event.args,
2460
+ }, channelId);
2461
+ }
2322
2462
  else if (event.type === 'error') {
2323
2463
  broadcast({ type: 'error', content: event.content }, channelId);
2324
2464
  }
2325
2465
  };
2326
2466
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2467
+ // 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
2327
2468
  const regenHint = await buildJudgmentHint(channel, channelId);
2328
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
2469
+ const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
2470
+ fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
2329
2471
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2330
2472
  // 更新 session
2331
2473
  const existingSession = await loadSession(channelId, currentSessionId);
@@ -2339,7 +2481,8 @@ export async function createWebServer(port = 3000, options = {}) {
2339
2481
  id: crypto.randomUUID(),
2340
2482
  type: 'ai',
2341
2483
  content: fullResponse,
2342
- timestamp: new Date().toISOString()
2484
+ timestamp: new Date().toISOString(),
2485
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
2343
2486
  });
2344
2487
  existingSession.lastUpdated = new Date().toISOString();
2345
2488
  await saveSession(existingSession);
@@ -2494,42 +2637,6 @@ export async function createWebServer(port = 3000, options = {}) {
2494
2637
  res.status(500).json({ error: err.message });
2495
2638
  }
2496
2639
  });
2497
- // 创建并执行工作流
2498
- app.post('/api/workflow', async (req, res) => {
2499
- try {
2500
- const { channelId, title, steps } = req.body;
2501
- if (!channelId || !steps || !Array.isArray(steps)) {
2502
- return res.status(400).json({ error: 'channelId and steps required' });
2503
- }
2504
- const tasks = await loadTaskQueue();
2505
- const task = {
2506
- id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
2507
- type: 'workflow',
2508
- title: title || '工作流',
2509
- description: `包含 ${steps.length} 个步骤的工作流`,
2510
- status: 'pending',
2511
- progress: 0,
2512
- createdAt: new Date().toISOString(),
2513
- updatedAt: new Date().toISOString(),
2514
- steps: steps.map((s, i) => ({
2515
- id: `step_${i}`,
2516
- name: s,
2517
- status: 'pending'
2518
- })),
2519
- currentStep: 0
2520
- };
2521
- tasks.push(task);
2522
- await saveTaskQueue(tasks);
2523
- // 自动开始执行
2524
- if (!isExecutingTask) {
2525
- executeTask(task, channelId);
2526
- }
2527
- res.json({ ok: true, task });
2528
- }
2529
- catch (err) {
2530
- res.status(500).json({ error: err.message });
2531
- }
2532
- });
2533
2640
  // ==================== LLM 配置 API ====================
2534
2641
  // 获取所有 LLM 配置
2535
2642
  app.get('/api/llm-config', async (req, res) => {
@@ -3664,7 +3771,25 @@ export async function createWebServer(port = 3000, options = {}) {
3664
3771
  res.status(500).json({ error: err.message });
3665
3772
  }
3666
3773
  });
3667
- // 主人审阅: 批准 draft
3774
+ // 终止当前 channel 的 LLM 流 (UI 终止按钮)
3775
+ app.post('/api/chat/abort', async (req, res) => {
3776
+ try {
3777
+ const { channelId } = req.body;
3778
+ if (!channelId)
3779
+ return res.status(400).json({ error: 'channelId required' });
3780
+ const s = channelRunState.get(channelId);
3781
+ if (s?.abortController) {
3782
+ s.abortController.abort();
3783
+ console.log(`[abort] user aborted channel=${channelId}`);
3784
+ return res.json({ ok: true, aborted: true });
3785
+ }
3786
+ res.json({ ok: true, aborted: false });
3787
+ }
3788
+ catch (err) {
3789
+ res.status(500).json({ error: err.message });
3790
+ }
3791
+ });
3792
+ // 用户审阅: 批准 draft
3668
3793
  app.post('/api/chat/approve', async (req, res) => {
3669
3794
  try {
3670
3795
  const { messageId, peerDID, finalText } = req.body || {};
@@ -3678,7 +3803,7 @@ export async function createWebServer(port = 3000, options = {}) {
3678
3803
  res.status(500).json({ error: err.message });
3679
3804
  }
3680
3805
  });
3681
- // 主人审阅: 丢弃 draft
3806
+ // 用户审阅: 丢弃 draft
3682
3807
  app.post('/api/chat/dismiss', async (req, res) => {
3683
3808
  try {
3684
3809
  const { messageId, peerDID } = req.body || {};
@@ -4046,20 +4171,414 @@ export async function createWebServer(port = 3000, options = {}) {
4046
4171
  res.status(500).json({ error: err.message });
4047
4172
  }
4048
4173
  });
4049
- app.get('/api/judgments', async (_req, res) => {
4174
+ app.get('/api/judgments', async (req, res) => {
4050
4175
  try {
4051
- const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
4176
+ const { listJudgmentsByStatus, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
4052
4177
  await initializeValueStore();
4053
- const all = await loadAllJudgments();
4054
- // 新的在前
4178
+ const status = (typeof req.query.status === 'string' ? req.query.status : 'all');
4179
+ const all = await listJudgmentsByStatus(status);
4055
4180
  all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
4056
- res.json({ count: all.length, judgments: all });
4181
+ res.json({ count: all.length, status, judgments: all });
4057
4182
  }
4058
4183
  catch (err) {
4059
4184
  console.error('[judgments] GET failed:', err);
4060
4185
  res.status(500).json({ error: err.message });
4061
4186
  }
4062
4187
  });
4188
+ // 蒸馏 B 触发 (人类点按钮) — 同步执行演化对齐
4189
+ app.post('/api/judgments/distill-from-conversation', async (req, res) => {
4190
+ try {
4191
+ const { channelId, messageId, recentTurns } = req.body;
4192
+ if (!channelId) {
4193
+ return res.status(400).json({ error: 'channelId required' });
4194
+ }
4195
+ // 取 channel 最近的对话
4196
+ const channels = await loadChannels();
4197
+ const channel = channels.find((c) => c.id === channelId);
4198
+ if (!channel)
4199
+ return res.status(404).json({ error: 'channel not found' });
4200
+ const currentSessionId = channel.currentSessionId;
4201
+ if (!currentSessionId) {
4202
+ return res.status(400).json({ error: 'no active session in channel' });
4203
+ }
4204
+ const session = await loadSession(channelId, currentSessionId);
4205
+ if (!session)
4206
+ return res.status(404).json({ error: 'session not found' });
4207
+ // 取最近 N 轮 (默认 10), 转成 DistillTurn 格式
4208
+ const limit = Math.min(Math.max(recentTurns ?? 10, 2), 30);
4209
+ const turns = session.messages.slice(-limit).map((m) => ({
4210
+ role: (m.type === 'user' ? 'human' : 'agent'),
4211
+ content: m.content,
4212
+ }));
4213
+ const { distillAndStoreFromChannel } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4214
+ const result = await distillAndStoreFromChannel(turns, { channelId });
4215
+ res.json({
4216
+ ok: true,
4217
+ triggered: result.triggered,
4218
+ reason: result.reason,
4219
+ judgment: result.judgment,
4220
+ evolved: result.evolved,
4221
+ });
4222
+ }
4223
+ catch (err) {
4224
+ console.error('[judgments] distill-from-conversation failed:', err);
4225
+ res.status(500).json({ error: err.message });
4226
+ }
4227
+ });
4228
+ // 蒸馏 D 触发 (AI 被动) — 后台异步,不阻塞 HTTP 响应
4229
+ app.post('/api/judgments/detect-and-distill', async (req, res) => {
4230
+ try {
4231
+ const { channelId, turns } = req.body;
4232
+ // 先立即返回 202, 不等 LLM
4233
+ res.status(202).json({ ok: true, queued: true });
4234
+ if (!channelId || !Array.isArray(turns) || turns.length === 0) {
4235
+ return;
4236
+ }
4237
+ // 异步处理 (不 await, 不阻塞响应)
4238
+ setImmediate(async () => {
4239
+ try {
4240
+ const { detectAndDistillFromChannel } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4241
+ const result = await detectAndDistillFromChannel(turns, { channelId });
4242
+ if (result.triggered) {
4243
+ console.log(`[D-hook] ${channelId}: ${result.reason}`, result.evolved);
4244
+ }
4245
+ }
4246
+ catch (err) {
4247
+ console.warn('[D-hook] background failed:', err);
4248
+ }
4249
+ });
4250
+ }
4251
+ catch (err) {
4252
+ console.error('[judgments] detect-and-distill failed:', err);
4253
+ res.status(500).json({ error: err.message });
4254
+ }
4255
+ });
4256
+ // 判断力使用回溯 (P0.5): 给定 judgmentIds, 反查对应的 decision 文本
4257
+ // 用途: UI 上"这条 AI 回复引用了哪些原则"
4258
+ app.post('/api/judgments/resolve-usage', async (req, res) => {
4259
+ try {
4260
+ const { ids } = req.body;
4261
+ if (!Array.isArray(ids) || ids.length === 0) {
4262
+ return res.json({ items: [] });
4263
+ }
4264
+ const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
4265
+ const all = await loadAllJudgments();
4266
+ const byId = new Map(all.map((j) => [j.id, j]));
4267
+ const items = ids
4268
+ .map((id) => byId.get(id))
4269
+ .filter((j) => Boolean(j))
4270
+ .map((j) => ({
4271
+ id: j.id,
4272
+ decision: j.decision,
4273
+ status: j.status ?? 'active',
4274
+ timestamp: j.timestamp,
4275
+ }));
4276
+ res.json({ items });
4277
+ }
4278
+ catch (err) {
4279
+ console.error('[judgments] resolve-usage failed:', err);
4280
+ res.status(500).json({ error: err.message });
4281
+ }
4282
+ });
4283
+ // 判断力违规日志 (P3 UI): 读 violations.jsonl
4284
+ app.get('/api/judgments/violations', async (req, res) => {
4285
+ try {
4286
+ const { getRecentViolations } = await import('../pi-ecosystem-judgment/monitor-gate.js');
4287
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4288
+ const items = await getRecentViolations(limit);
4289
+ res.json({ count: items.length, items });
4290
+ }
4291
+ catch (err) {
4292
+ console.error('[judgments] violations failed:', err);
4293
+ res.status(500).json({ error: err.message });
4294
+ }
4295
+ });
4296
+ // 类 B 自适应扫描: 读 judgments.json + usage.jsonl, 给出 stale/rising/unused 建议
4297
+ // ?force=1 跳过 24h 缓存
4298
+ app.get('/api/judgments/adaptive-suggestions', async (req, res) => {
4299
+ try {
4300
+ const { getCachedScan } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4301
+ const force = String(req.query.force ?? '') === '1';
4302
+ const result = await getCachedScan(force);
4303
+ res.json(result);
4304
+ }
4305
+ catch (err) {
4306
+ console.error('[judgments] adaptive-scan failed:', err);
4307
+ res.status(500).json({ error: err.message });
4308
+ }
4309
+ });
4310
+ // Bootstrap Context 调试视图: 返出完整 BolloonContext
4311
+ app.get('/api/bolloon/context', async (req, res) => {
4312
+ try {
4313
+ const { getCachedBolloonContext } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4314
+ const force = String(req.query.force ?? '') === '1';
4315
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() }, force);
4316
+ res.json(ctx);
4317
+ }
4318
+ catch (err) {
4319
+ console.error('[bolloon] context failed:', err);
4320
+ res.status(500).json({ error: err.message });
4321
+ }
4322
+ });
4323
+ // 阶段 B: 周报 (weekly-report.ts 产物) — 仅 API 读取, 不做 UI tab
4324
+ // GET /api/reports → { files: ['2026-W24.md', ...] }
4325
+ // GET /api/reports/2026-W24 → { week, content }
4326
+ app.get('/api/reports', async (_req, res) => {
4327
+ try {
4328
+ const dir = path.join(os.homedir(), '.bolloon', 'reports');
4329
+ try {
4330
+ const entries = await fs.readdir(dir);
4331
+ const files = entries
4332
+ .filter((f) => f.endsWith('.md'))
4333
+ .sort()
4334
+ .reverse(); // 新的在前
4335
+ res.json({ dir, files });
4336
+ }
4337
+ catch {
4338
+ res.json({ dir, files: [] });
4339
+ }
4340
+ }
4341
+ catch (err) {
4342
+ res.status(500).json({ error: err.message });
4343
+ }
4344
+ });
4345
+ app.get('/api/reports/:week', async (req, res) => {
4346
+ try {
4347
+ const week = req.params.week;
4348
+ // 严格校验, 防路径穿越
4349
+ if (!/^\d{4}-W\d{1,2}$/.test(week)) {
4350
+ return res.status(400).json({ error: 'week must match YYYY-Www' });
4351
+ }
4352
+ const file = path.join(os.homedir(), '.bolloon', 'reports', `${week}.md`);
4353
+ try {
4354
+ const content = await fs.readFile(file, 'utf-8');
4355
+ res.json({ week, content, length: content.length });
4356
+ }
4357
+ catch {
4358
+ res.status(404).json({ error: 'not found', week });
4359
+ }
4360
+ }
4361
+ catch (err) {
4362
+ res.status(500).json({ error: err.message });
4363
+ }
4364
+ });
4365
+ // 阶段 C 护栏 5: auto-evolve baseline 管理 (无 UI, 仅 API)
4366
+ // GET /api/auto-evolve/baselines → 列出所有 baseline tag
4367
+ // GET /api/auto-evolve/baselines/:tag/diff → 看某 baseline 的 diff 摘要
4368
+ // POST /api/auto-evolve/rollback {tag} → 回滚到指定 baseline
4369
+ app.get('/api/auto-evolve/baselines', async (_req, res) => {
4370
+ try {
4371
+ const { execFile } = await import('child_process');
4372
+ const { promisify } = await import('util');
4373
+ const pExec = promisify(execFile);
4374
+ const { stdout } = await pExec('git', [
4375
+ 'tag', '-l', 'auto-evolve-baseline-*', '--format=%(refname:short)|%(contents)|%(objectname:short)|%(taggerdate:iso)',
4376
+ ], { cwd: process.cwd() });
4377
+ const tags = stdout.trim().split('\n').filter(Boolean).map((line) => {
4378
+ const [tag, msg, sha, date] = line.split('|');
4379
+ return { tag, message: msg || '', sha, date };
4380
+ });
4381
+ res.json({ tags, count: tags.length });
4382
+ }
4383
+ catch (err) {
4384
+ res.status(500).json({ error: err.message });
4385
+ }
4386
+ });
4387
+ app.get('/api/auto-evolve/baselines/:tag/diff', async (req, res) => {
4388
+ try {
4389
+ const { execFile } = await import('child_process');
4390
+ const { promisify } = await import('util');
4391
+ const pExec = promisify(execFile);
4392
+ const tag = req.params.tag;
4393
+ if (!/^auto-evolve-baseline-[\w-]+$/.test(tag)) {
4394
+ return res.status(400).json({ error: 'tag must match auto-evolve-baseline-*' });
4395
+ }
4396
+ const { stdout } = await pExec('git', ['show', '--stat', '--no-color', tag], { cwd: process.cwd() });
4397
+ res.json({ tag, diff: stdout.slice(0, 5000) }); // 限长 5KB
4398
+ }
4399
+ catch (err) {
4400
+ res.status(500).json({ error: err.message });
4401
+ }
4402
+ });
4403
+ // Bootstrap Context → 拼好的 system prompt 片段 (供调试看注入效果)
4404
+ app.get('/api/bolloon/context/system-prompt', async (req, res) => {
4405
+ try {
4406
+ const { getCachedBolloonContext } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4407
+ const { formatContextForSystemPrompt } = await import('../bootstrap/project-context.js');
4408
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() });
4409
+ const systemAddition = formatContextForSystemPrompt(ctx, {
4410
+ maxChars: parseInt(String(req.query.max ?? '4000'), 10) || 4000,
4411
+ });
4412
+ res.json({ systemAddition, length: systemAddition.length, truncated: systemAddition.includes('截断模式') });
4413
+ }
4414
+ catch (err) {
4415
+ console.error('[bolloon] context/system-prompt failed:', err);
4416
+ res.status(500).json({ error: err.message });
4417
+ }
4418
+ });
4419
+ // ============================================================
4420
+ // system-prompt health (P-Action 2 — Harness Gardening)
4421
+ // 返回每层 lifecycle 状态: ok | stale | overdue-review | missing-frontmatter | dynamic
4422
+ // query: ?activeOnly=1 → 只返回当前 context 激活的层
4423
+ // ============================================================
4424
+ app.get('/api/prompt/health', async (req, res) => {
4425
+ try {
4426
+ const { listLayers } = await import('../llm/system-prompt/registry.js');
4427
+ const { evaluateLayers, markActive } = await import('../llm/system-prompt/health.js');
4428
+ const all = listLayers();
4429
+ const baseReport = evaluateLayers(all);
4430
+ // 如果 query 里有 activeOnly, 跑一次 assembleSystemPrompt 拿激活列表
4431
+ if (String(req.query.activeOnly ?? '') === '1') {
4432
+ const { assembleSystemPrompt } = await import('../llm/system-prompt/registry.js');
4433
+ const channel = String(req.query.channel ?? 'local');
4434
+ const role = req.query.role;
4435
+ const tool = req.query.tool;
4436
+ try {
4437
+ const r = await assembleSystemPrompt({ channel, role, tool });
4438
+ const activeIds = new Set(r.layerIds);
4439
+ res.json(markActive(baseReport, activeIds));
4440
+ }
4441
+ catch (err) {
4442
+ console.warn('[prompt-health] assembleSystemPrompt failed (silent, returning base report):', err);
4443
+ res.json(baseReport);
4444
+ }
4445
+ }
4446
+ else {
4447
+ res.json(baseReport);
4448
+ }
4449
+ }
4450
+ catch (err) {
4451
+ console.error('[prompt-health] failed:', err);
4452
+ res.status(500).json({ error: err.message });
4453
+ }
4454
+ });
4455
+ // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4456
+ // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4457
+ // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
4458
+ // 缺省 → 用户在 UI 手动触发, 不查开关 (避免阻塞用户)
4459
+ app.post('/api/judgments/adaptive-apply', async (req, res) => {
4460
+ try {
4461
+ const isAuto = req.query.auto === '1' || req.query.auto === 'true';
4462
+ const { action, suggestion, appliedPatch } = req.body;
4463
+ if (!action || !suggestion?.judgmentId) {
4464
+ return res.status(400).json({ error: 'action and suggestion.judgmentId required' });
4465
+ }
4466
+ const { updateJudgmentStatus } = await import('../pi-ecosystem-judgment/human-value-store.js');
4467
+ const { logEvolution } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4468
+ // accept 时: 真正改库
4469
+ if (action === 'accept') {
4470
+ // 阶段 A: 自动路径需先过 auto-evolve-policy 网关
4471
+ if (isAuto) {
4472
+ const { requireDataLayerAutoEvolve } = await import('../utils/auto-evolve-policy.js');
4473
+ try {
4474
+ await requireDataLayerAutoEvolve('adaptive-apply.auto.deprecate');
4475
+ }
4476
+ catch (err) {
4477
+ return res.status(423).json({
4478
+ error: 'data-layer-auto-evolve-disabled',
4479
+ message: err.message,
4480
+ hint: '设 BOLLOON_AUTO_EVOLVE_DATA=1 或在 self-improve-policy.json 加 dataLayerAutoEvolve: true',
4481
+ });
4482
+ }
4483
+ }
4484
+ if (suggestion.action === 'deprecate') {
4485
+ // 标记 superseded (语义: 不再用, 但保留可回滚)
4486
+ await updateJudgmentStatus(suggestion.judgmentId, 'superseded', {
4487
+ evolutionReason: 'merged', // 借 merged 字段表达"被自适应废弃"
4488
+ });
4489
+ }
4490
+ else if (suggestion.action === 'boost') {
4491
+ // boost: 用户手动接受后, 不改库本身 (weight 在 getRelevantValues 里动态算),
4492
+ // 但写 evolution 留痕, 未来可以基于此调整算法
4493
+ // 当前不直接改库, 仅留痕
4494
+ }
4495
+ // 'review' 类不需要自动改库, 仅 log 接受
4496
+ }
4497
+ await logEvolution({
4498
+ ts: new Date().toISOString(),
4499
+ action,
4500
+ suggestion: suggestion,
4501
+ appliedPatch,
4502
+ });
4503
+ res.json({ ok: true });
4504
+ }
4505
+ catch (err) {
4506
+ console.error('[judgments] adaptive-apply failed:', err);
4507
+ res.status(500).json({ error: err.message });
4508
+ }
4509
+ });
4510
+ // 演化日志 (audit / 一键回滚源)
4511
+ app.get('/api/judgments/evolution-log', async (req, res) => {
4512
+ try {
4513
+ const { readEvolutionLog } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4514
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);
4515
+ const items = await readEvolutionLog(limit);
4516
+ res.json({ count: items.length, items });
4517
+ }
4518
+ catch (err) {
4519
+ console.error('[judgments] evolution-log failed:', err);
4520
+ res.status(500).json({ error: err.message });
4521
+ }
4522
+ });
4523
+ // 阶段 2: Causal-judge 4 个 endpoint
4524
+ app.get('/api/judgments/causal/correlation', async (req, res) => {
4525
+ try {
4526
+ const { runCorrelationAnalysis } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4527
+ const topN = Math.min(Math.max(parseInt(String(req.query.topN ?? '5'), 10) || 5, 1), 50);
4528
+ const useLLM = String(req.query.useLLM ?? '1') !== '0';
4529
+ const items = await runCorrelationAnalysis({ topN, useLLM });
4530
+ res.json({ count: items.length, items });
4531
+ }
4532
+ catch (err) {
4533
+ console.error('[causal] correlation failed:', err);
4534
+ res.status(500).json({ error: err.message });
4535
+ }
4536
+ });
4537
+ app.get('/api/judgments/causal/intervention', async (req, res) => {
4538
+ try {
4539
+ const { runIntervention } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4540
+ const { judgmentId, scenario } = req.query;
4541
+ if (!judgmentId)
4542
+ return res.status(400).json({ error: 'judgmentId required' });
4543
+ const result = await runIntervention(judgmentId, { scenarioContext: scenario });
4544
+ res.json(result);
4545
+ }
4546
+ catch (err) {
4547
+ console.error('[causal] intervention failed:', err);
4548
+ res.status(500).json({ error: err.message });
4549
+ }
4550
+ });
4551
+ app.post('/api/judgments/causal/counterfactual', async (req, res) => {
4552
+ try {
4553
+ const { runCounterfactualAudit } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4554
+ const { userInput, aiReply, violatedPrinciples } = req.body;
4555
+ if (!userInput || !aiReply) {
4556
+ return res.status(400).json({ error: 'userInput and aiReply required' });
4557
+ }
4558
+ const audit = await runCounterfactualAudit({
4559
+ userInput,
4560
+ aiReply,
4561
+ violatedPrinciples: violatedPrinciples ?? [],
4562
+ });
4563
+ res.json(audit);
4564
+ }
4565
+ catch (err) {
4566
+ console.error('[causal] counterfactual failed:', err);
4567
+ res.status(500).json({ error: err.message });
4568
+ }
4569
+ });
4570
+ app.get('/api/judgments/causal/audit-log', async (req, res) => {
4571
+ try {
4572
+ const { readCounterfactualLog } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4573
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4574
+ const items = await readCounterfactualLog(limit);
4575
+ res.json({ count: items.length, items });
4576
+ }
4577
+ catch (err) {
4578
+ console.error('[causal] audit-log failed:', err);
4579
+ res.status(500).json({ error: err.message });
4580
+ }
4581
+ });
4063
4582
  // 导入判断: 接受 { filename, content (base64), context }.
4064
4583
  // 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
4065
4584
  // 解析规则:
@@ -4398,25 +4917,192 @@ export async function createWebServer(port = 3000, options = {}) {
4398
4917
  const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
4399
4918
  res.json(getEventHistory());
4400
4919
  });
4920
+ // ============================================================
4921
+ // Permission Mode (P2.2 — UI 暴露开关)
4922
+ // 优先级: 运行时 session 覆盖 > env BOLLOON_PERM_MODE > 'default'
4923
+ // 运行时覆盖存在 ~/.bolloon/sessions/permission-mode.json, 每次 promptStream 入口读取
4924
+ // ============================================================
4925
+ const PERM_MODE_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode.json');
4926
+ function readPermModeOverride() {
4927
+ try {
4928
+ // 同步读, 文件很小
4929
+ const raw = fsSync.readFileSync(PERM_MODE_FILE, 'utf-8');
4930
+ const obj = JSON.parse(raw);
4931
+ if (obj && typeof obj.mode === 'string') {
4932
+ return { mode: obj.mode, ts: obj.ts || new Date().toISOString() };
4933
+ }
4934
+ }
4935
+ catch { /* 不存在 = 无 override */ }
4936
+ return null;
4937
+ }
4938
+ function writePermModeOverride(mode) {
4939
+ try {
4940
+ const dir = path.dirname(PERM_MODE_FILE);
4941
+ fsSync.mkdirSync(dir, { recursive: true });
4942
+ fsSync.writeFileSync(PERM_MODE_FILE, JSON.stringify({ mode, ts: new Date().toISOString() }, null, 2), 'utf-8');
4943
+ }
4944
+ catch (err) {
4945
+ console.warn('[server] writePermModeOverride failed:', err);
4946
+ }
4947
+ }
4948
+ // 读当前生效 mode (runtime override > env > default)
4949
+ app.get('/api/permission-mode', async (_req, res) => {
4950
+ const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
4951
+ const override = readPermModeOverride();
4952
+ const envMode = process.env.BOLLOON_PERM_MODE || null;
4953
+ const effective = resolvePermissionMode();
4954
+ res.json({
4955
+ effective,
4956
+ override: override?.mode || null,
4957
+ overrideTs: override?.ts || null,
4958
+ env: envMode,
4959
+ allowed: ALL_PERMISSION_MODES,
4960
+ description: {
4961
+ default: '每次工具调用询问; shell 走 shell-guard',
4962
+ acceptEdits: 'edit_*/write_* 跳过黑名单; shell 仍走 shell-guard',
4963
+ bypassPermissions: '非 shell 全部放行; shell 永远走 shell-guard (硬约束)',
4964
+ },
4965
+ });
4966
+ });
4967
+ // 设 runtime override (存盘, 下次 promptStream 入口读取生效)
4968
+ app.post('/api/permission-mode', async (req, res) => {
4969
+ const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
4970
+ const mode = String(req.body?.mode || '');
4971
+ if (!ALL_PERMISSION_MODES.includes(mode)) {
4972
+ return res.status(400).json({
4973
+ error: `Invalid mode. Allowed: ${ALL_PERMISSION_MODES.join(', ')}`,
4974
+ allowed: ALL_PERMISSION_MODES,
4975
+ });
4976
+ }
4977
+ const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
4978
+ writePermModeOverride(mode);
4979
+ // 写历史 (append-only JSONL, 跟 bolloon 其他 audit 一致)
4980
+ try {
4981
+ const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
4982
+ fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
4983
+ fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
4984
+ ts: new Date().toISOString(),
4985
+ from: oldMode,
4986
+ to: mode,
4987
+ source: 'api',
4988
+ }) + '\n', 'utf-8');
4989
+ }
4990
+ catch { /* 历史写失败不阻塞主流程 */ }
4991
+ console.log(`[server] permission-mode override set to "${mode}" via API (was "${oldMode}")`);
4992
+ res.json({
4993
+ ok: true,
4994
+ mode,
4995
+ previousMode: oldMode,
4996
+ ts: new Date().toISOString(),
4997
+ note: '新 mode 在下一次 promptStream 入口生效 (不打断当前对话)',
4998
+ });
4999
+ });
5000
+ // 取消 runtime override, 回到 env 或 default
5001
+ app.delete('/api/permission-mode', async (_req, res) => {
5002
+ const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
5003
+ try {
5004
+ if (fsSync.existsSync(PERM_MODE_FILE))
5005
+ fsSync.unlinkSync(PERM_MODE_FILE);
5006
+ }
5007
+ catch (err) {
5008
+ console.warn('[server] delete perm-mode override failed:', err);
5009
+ }
5010
+ // 写历史
5011
+ try {
5012
+ const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
5013
+ fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
5014
+ fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
5015
+ ts: new Date().toISOString(),
5016
+ from: oldMode,
5017
+ to: 'env-or-default',
5018
+ source: 'api',
5019
+ action: 'delete-override',
5020
+ }) + '\n', 'utf-8');
5021
+ }
5022
+ catch { /* ignore */ }
5023
+ res.json({ ok: true, note: '已删除 runtime override, 回到 env / default' });
5024
+ });
5025
+ // 2026-06-16: 循环检查 — GET /api/loop/inspect?channelId=...
5026
+ // 返回最近一轮 ReAct loop 的产出: 步骤 / 工具调用 / 压缩摘要 / 最终回复 / token 用量.
5027
+ // 用于前端 status bar 的「✓ 检查」按钮弹 modal.
5028
+ app.get('/api/loop/inspect', async (req, res) => {
5029
+ try {
5030
+ const channelId = String(req.query.channelId || '');
5031
+ if (!channelId)
5032
+ return res.status(400).json({ error: 'channelId required' });
5033
+ const s = channelRunState.get(channelId);
5034
+ if (!s)
5035
+ return res.json({ summary: '该 channel 无活跃 loop', steps: [], finalReply: '' });
5036
+ const steps = (s.lastSteps || []).map((st) => ({
5037
+ name: st.name || st.tool || 'step',
5038
+ status: st.status || 'completed',
5039
+ durationMs: st.durationMs,
5040
+ output: st.output || st.result || '',
5041
+ }));
5042
+ res.json({
5043
+ summary: s.lastSummary || (s.running ? 'loop 仍在运行中' : 'loop 已结束'),
5044
+ steps,
5045
+ finalReply: s.lastFinalReply || '',
5046
+ tokens: s.lastTokens || {},
5047
+ });
5048
+ }
5049
+ catch (err) {
5050
+ res.status(500).json({ error: err.message });
5051
+ }
5052
+ });
5053
+ // 历史 (类似 self-improve history, 供前端 timeline)
5054
+ app.get('/api/permission-mode/history', async (_req, res) => {
5055
+ const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
5056
+ try {
5057
+ if (!fsSync.existsSync(HISTORY_FILE))
5058
+ return res.json([]);
5059
+ const lines = fsSync.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean);
5060
+ const entries = lines.map((l) => {
5061
+ try {
5062
+ return JSON.parse(l);
5063
+ }
5064
+ catch {
5065
+ return null;
5066
+ }
5067
+ }).filter(Boolean);
5068
+ res.json(entries.slice(-50)); // 最近 50 条
5069
+ }
5070
+ catch (err) {
5071
+ res.json([]);
5072
+ }
5073
+ });
4401
5074
  // 健康检查错误数 ≥ 2 -> 触发自改信号
5075
+ // 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
4402
5076
  if (healthMonitor) {
4403
- healthMonitor.startPeriodicCheck(60000, (status) => {
4404
- const errorCount = Object.values(status.checks)
4405
- .filter((c) => c.status === 'error').length;
4406
- if (errorCount >= 2) {
4407
- import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
4408
- const failedKeys = Object.entries(status.checks)
4409
- .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
4410
- reportSelfImproveEvent({
4411
- kind: 'silent-timeout',
4412
- details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
5077
+ if (selfImproveEnabled) {
5078
+ healthMonitor.startPeriodicCheck(60000, (status) => {
5079
+ const errorCount = Object.values(status.checks)
5080
+ .filter((c) => c.status === 'error').length;
5081
+ if (errorCount >= 2) {
5082
+ import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
5083
+ const failedKeys = Object.entries(status.checks)
5084
+ .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
5085
+ reportSelfImproveEvent({
5086
+ kind: 'silent-timeout',
5087
+ details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
5088
+ });
4413
5089
  });
4414
- });
4415
- }
4416
- });
5090
+ }
5091
+ });
5092
+ }
5093
+ else {
5094
+ // 用户模式: 只跑监控不打自改信号, 心跳仍工作
5095
+ healthMonitor.startPeriodicCheck(60000);
5096
+ console.log('[24h] Health monitor periodic check (no self-improve)');
5097
+ }
5098
+ }
5099
+ // 安装自改总线 -> SSE 桥 (开发者模式才装, 用户模式靠 /api/self-improve/trigger 手动触发)
5100
+ if (selfImproveEnabled) {
5101
+ void installSelfImproveHook();
5102
+ }
5103
+ else {
5104
+ console.log('[self-improve] 用户模式, installSelfImproveHook 跳过 (可用 POST /api/self-improve/trigger 手动触发)');
4417
5105
  }
4418
- // 安装自改总线 -> SSE 桥
4419
- void installSelfImproveHook();
4420
5106
  // 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
4421
5107
  return new Promise((resolve, reject) => {
4422
5108
  const maxAttempts = 10;
@@ -4436,11 +5122,28 @@ export async function createWebServer(port = 3000, options = {}) {
4436
5122
  console.log('服务器已监听');
4437
5123
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
4438
5124
  void installChatBusHook();
5125
+ // 2026-06-16: ping 改为 data: {"type":"ping"} — 之前是 SSE 注释格式 (: ping\n\n),
5126
+ // 浏览器 EventSource 不触发 onmessage, 客户端 60s 阈值 (现已 30s) 误判死链.
5127
+ // 改后前端 onmessage 收到 ping 就重置 lastEventTime, 真死链才 30s 后重建.
4439
5128
  setInterval(() => {
4440
5129
  for (const client of sseClients) {
4441
- client.res.write(': ping\n\n');
5130
+ try {
5131
+ client.res.write('data: {"type":"ping"}\n\n');
5132
+ }
5133
+ catch (err) {
5134
+ // socket 已断, 跳过 — client 端 onerror 会触发重连
5135
+ console.warn('[SSE ping] write 失败, 跳过该客户端:', err.message);
5136
+ }
4442
5137
  }
4443
5138
  }, 30000);
5139
+ // 2026-06-16: 全局捕获 socket error 事件, 避免未处理 EPIPE/ETIMEDOUT 让进程崩
5140
+ currentServer.on('clientError', (err, socket) => {
5141
+ console.warn('[server] clientError:', err.code, err.message);
5142
+ try {
5143
+ socket.end();
5144
+ }
5145
+ catch { }
5146
+ });
4444
5147
  resolve({ app, server: currentServer, port: currentPort });
4445
5148
  });
4446
5149
  };