@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
package/src/web/server.ts CHANGED
@@ -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 {
9
10
  HyperswarmCommunicator,
10
11
  createHyperswarmCommunicator,
@@ -39,14 +40,14 @@ function resolveWebRoot(): string {
39
40
  }
40
41
  const d = _baseDirname;
41
42
  const candidates = [
42
- path.join(d), // dist/web
43
- path.join(d, '..', '..', 'dist', 'web'), // src/web dist/web
43
+ path.join(d, '..', '..', 'dist', 'web'), // 2026-06-15 优先: src/web → dist/web (build 产物)
44
+ path.join(d), // dist/web (npm 跑时)
44
45
  path.join(d, '..', 'web'), // dist/ → web/ 兄弟
45
46
  ];
46
47
  for (const c of candidates) {
47
48
  if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
48
49
  }
49
- return candidates[1];
50
+ return candidates[0];
50
51
  }
51
52
  const webRoot = resolveWebRoot();
52
53
  console.log(`[web] webRoot = ${webRoot}`);
@@ -237,11 +238,10 @@ async function saveTheme(theme: 'light' | 'dark', agentId: string): Promise<void
237
238
  // ==================== Task Queue & Workflow System ====================
238
239
 
239
240
  const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
240
- const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
241
241
 
242
242
  interface Task {
243
243
  id: string;
244
- type: 'read' | 'summarize' | 'improve' | 'chat' | 'workflow';
244
+ type: 'read' | 'summarize' | 'improve' | 'chat';
245
245
  title: string;
246
246
  description?: string;
247
247
  status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
@@ -250,23 +250,10 @@ interface Task {
250
250
  error?: string;
251
251
  createdAt: string;
252
252
  updatedAt: string;
253
- steps?: TaskStep[];
253
+ steps?: any[];
254
254
  currentStep?: number;
255
255
  }
256
256
 
257
- interface TaskStep {
258
- id: string;
259
- name: string;
260
- status: 'pending' | 'running' | 'completed' | 'failed';
261
- result?: string;
262
- }
263
-
264
- interface WorkflowState {
265
- channelId: string;
266
- tasks: Task[];
267
- lastUpdated: string;
268
- }
269
-
270
257
  async function loadTaskQueue(): Promise<Task[]> {
271
258
  try {
272
259
  const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
@@ -280,32 +267,6 @@ async function saveTaskQueue(tasks: Task[]): Promise<void> {
280
267
  await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
281
268
  }
282
269
 
283
- async function loadWorkflowState(channelId: string): Promise<WorkflowState | null> {
284
- try {
285
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
286
- const states = JSON.parse(data) as WorkflowState[];
287
- return states.find(s => s.channelId === channelId) || null;
288
- } catch {
289
- return null;
290
- }
291
- }
292
-
293
- async function saveWorkflowState(state: WorkflowState): Promise<void> {
294
- try {
295
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
296
- const states = JSON.parse(data) as WorkflowState[];
297
- const index = states.findIndex(s => s.channelId === state.channelId);
298
- if (index >= 0) {
299
- states[index] = state;
300
- } else {
301
- states.push(state);
302
- }
303
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
304
- } catch {
305
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
306
- }
307
- }
308
-
309
270
  let isExecutingTask = false;
310
271
  let executionTaskId: string | null = null;
311
272
 
@@ -354,37 +315,6 @@ async function executeTask(task: Task, channelId: string): Promise<void> {
354
315
  }
355
316
  break;
356
317
 
357
- case 'workflow':
358
- // 执行多步骤工作流
359
- if (task.steps && task.steps.length > 0) {
360
- let loopCount = 0;
361
- for (let i = 0; i < task.steps.length; i++) {
362
- // 广播循环开始
363
- loopCount++;
364
- broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
365
-
366
- task.steps[i].status = 'running';
367
- broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
368
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
369
-
370
- // 执行步骤 - 模拟流式输出
371
- for (let j = 0; j < 3; j++) {
372
- await new Promise(resolve => setTimeout(resolve, 300));
373
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
374
- }
375
-
376
- task.steps[i].status = 'completed';
377
- task.progress = Math.round(((i + 1) / task.steps.length) * 100);
378
-
379
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
380
- broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
381
- broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
382
- }
383
- result = '✅ 工作流执行完成';
384
- broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
385
- }
386
- break;
387
-
388
318
  default:
389
319
  result = '未知任务类型';
390
320
  }
@@ -791,7 +721,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
791
721
  const { getMinimax } = await import('../constraints/index.js');
792
722
  const llm = getMinimax();
793
723
  // v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
794
- const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "主人".\n\n`;
724
+ const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "用户".\n\n`;
795
725
  // v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
796
726
  let dirHint = '';
797
727
  const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
@@ -812,11 +742,20 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
812
742
  }
813
743
  dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
814
744
  }
815
- const fullPrompt = `${visitorHint}${dirHint}${judgmentHint}${text}`;
745
+ // 2026-06-15: P2P 远端访客路径也用显式 marker 包裹 text
746
+ // 2026-06-15 二次修: 把 text 放在最前 (与主路径 server.ts:1868 对齐),
747
+ // 避免 LLM 被 judgmentHint 末尾的 "..." 误判为整个 input 被截断
748
+ const fullPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${visitorHint}${dirHint}${judgmentHint}\n`;
816
749
  let fullResponse = '';
817
750
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
818
751
  let lastFlushAt = 0;
752
+ let usedJudgmentIds: string[] = [];
819
753
  const streamCallback: any = (event: any) => {
754
+ // P0.5: 注入门回传
755
+ if (event?.type === 'used_judgments' && Array.isArray(event.usedIds)) {
756
+ usedJudgmentIds = event.usedIds;
757
+ return;
758
+ }
820
759
  if (event.type === 'token') {
821
760
  fullResponse += event.content;
822
761
  if (fullResponse.length - lastFlushAt >= 20) {
@@ -830,7 +769,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
830
769
  }
831
770
  };
832
771
  const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
833
- fullResponse = await agent.promptStream(fullPrompt, streamCallback);
772
+ fullResponse = await agent.promptStream(fullPrompt, streamCallback, undefined, channelId);
834
773
 
835
774
  // v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
836
775
  try {
@@ -842,6 +781,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
842
781
  id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
843
782
  type: 'ai',
844
783
  content: fullResponse,
784
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
845
785
  timestamp: new Date().toISOString()
846
786
  });
847
787
  session.lastUpdated = new Date().toISOString();
@@ -1170,6 +1110,18 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1170
1110
  console.error('[警告] 未处理的 Promise 拒绝:', reason);
1171
1111
  });
1172
1112
 
1113
+ // Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
1114
+ // 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
1115
+ try {
1116
+ const { bootstrapBolloon } = await import(
1117
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
1118
+ );
1119
+ const bs = await bootstrapBolloon({ cwd: process.cwd() });
1120
+ console.log(`[createWebServer] bootstrap 完成 (${bs.durationMs}ms)`);
1121
+ } catch (err) {
1122
+ console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
1123
+ }
1124
+
1173
1125
  // 重置旧的 agent session,确保使用新的 LLM 配置
1174
1126
  const { resetAgentSession } = await import('../agents/pi-sdk.js');
1175
1127
  resetAgentSession();
@@ -1630,11 +1582,42 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1630
1582
  // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
1631
1583
  const channelForJudgment = channel;
1632
1584
 
1585
+ // per-channel queue 检查: 已在跑就入队, 等当前跑完自动接上
1586
+ const runState = getOrCreateRunState(channelId);
1587
+ if (runState.running) {
1588
+ runState.queue.push({ channelId, text, boundWalletAddress, autoToolsEnabled });
1589
+ broadcastQueueUpdate(channelId);
1590
+ console.log(`[queue] /message 入队 channel=${channelId}, queue len=${runState.queue.length}`);
1591
+ return;
1592
+ }
1593
+ runState.running = true;
1594
+ runState.abortController = new AbortController();
1595
+ broadcastQueueUpdate(channelId);
1596
+
1633
1597
  try {
1634
1598
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
1635
1599
  let fullResponse = '';
1600
+ // P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
1601
+ let usedJudgmentIds: string[] = [];
1636
1602
 
1637
1603
  const streamCallback: StreamCallback = (event: StreamEvent) => {
1604
+ // P0.5: 捕获注入门回传
1605
+ if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
1606
+ usedJudgmentIds = (event as any).usedIds;
1607
+ // 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
1608
+ broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
1609
+ return;
1610
+ }
1611
+ // 阶段事件 (注入门 / D 触发)
1612
+ if ((event as any).type === 'phase') {
1613
+ broadcast({
1614
+ type: 'phase',
1615
+ phase: (event as any).phase,
1616
+ detail: (event as any).detail,
1617
+ usedCount: (event as any).usedCount,
1618
+ }, channelId);
1619
+ return;
1620
+ }
1638
1621
  // 同时发送给流式显示和工作流显示
1639
1622
  if (event.type === 'token' || event.type === 'thinking') {
1640
1623
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
@@ -1646,6 +1629,32 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1646
1629
  broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
1647
1630
  broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
1648
1631
  console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
1632
+ } else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
1633
+ // 2026-06-15: 步骤状态机事件 — 原样转发 (前端 step-timeline 组件订阅)
1634
+ broadcast({
1635
+ type: event.type,
1636
+ tool: event.tool,
1637
+ content: event.content,
1638
+ success: event.success,
1639
+ output: event.output,
1640
+ error: event.error,
1641
+ args: event.args,
1642
+ }, channelId);
1643
+ // 2026-06-16: 累积 step 到 runState, 供 /api/loop/inspect 读取
1644
+ try {
1645
+ if (event.type === 'step_done' || event.type === 'step_error') {
1646
+ const rs = channelRunState.get(channelId);
1647
+ if (rs) {
1648
+ if (!rs.lastSteps) rs.lastSteps = [];
1649
+ rs.lastSteps.push({
1650
+ name: String(event.tool || event.content || 'step').slice(0, 60),
1651
+ status: event.type === 'step_error' ? 'failed' : (event.success === false ? 'failed' : 'ok'),
1652
+ durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
1653
+ output: event.output ? String(event.output).slice(0, 800) : (event.error ? String(event.error).slice(0, 800) : undefined),
1654
+ });
1655
+ }
1656
+ }
1657
+ } catch { /* non-fatal */ }
1649
1658
  } else if (event.type === 'error') {
1650
1659
  broadcast({ type: 'error', content: event.content }, channelId);
1651
1660
  }
@@ -1657,7 +1666,7 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1657
1666
  let contextHint = '';
1658
1667
  if (realChannelDid) contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
1659
1668
  // v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
1660
- contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "主人" 即可.\n`;
1669
+ contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
1661
1670
  if (boundWalletAddress) {
1662
1671
  contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
1663
1672
  }
@@ -1796,7 +1805,26 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1796
1805
  }
1797
1806
 
1798
1807
  if (contextHint) contextHint += '\n';
1799
- fullResponse = await agent.promptStream(contextHint + text, streamCallback);
1808
+ try {
1809
+ // 2026-06-15: 把 user text 单独 marker 包起来, LLM 不会被 8K+ 的 system context 吞掉
1810
+ // (之前 contextHint + text 拼成一整段当 user role, 24 字符的 user input 埋在 8K+ 里看不出)
1811
+ // 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
1812
+ // 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
1813
+ // 的 "..." 误判为整个 input 截断)
1814
+ const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
1815
+ fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
1816
+ } catch (err: any) {
1817
+ // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
1818
+ if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
1819
+ console.log(`[chat] aborted channel=${channelId}`);
1820
+ } else {
1821
+ throw err;
1822
+ }
1823
+ }
1824
+ // abort 模式: 给 partial 拼后缀
1825
+ if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
1826
+ fullResponse = fullResponse + '\n\n_[生成已中断]_';
1827
+ }
1800
1828
 
1801
1829
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
1802
1830
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
@@ -1808,7 +1836,15 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1808
1836
  session.sessionId = currentSessionId;
1809
1837
  // v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
1810
1838
  session.messages.push({ id: crypto.randomUUID(), type: 'user' as const, content: text, timestamp: new Date().toISOString(), source: 'local' as any });
1811
- session.messages.push({ id: crypto.randomUUID(), type: 'ai' as const, content: fullResponse, timestamp: new Date().toISOString(), source: 'local' as any });
1839
+ session.messages.push({
1840
+ id: crypto.randomUUID(),
1841
+ type: 'ai' as const,
1842
+ content: fullResponse,
1843
+ timestamp: new Date().toISOString(),
1844
+ source: 'local' as any,
1845
+ // P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
1846
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
1847
+ });
1812
1848
  session.lastUpdated = new Date().toISOString();
1813
1849
  await saveSession(session);
1814
1850
 
@@ -1823,12 +1859,62 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1823
1859
  }
1824
1860
 
1825
1861
  broadcast({ type: 'done' }, channelId);
1862
+
1863
+ // D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
1864
+ setImmediate(() => {
1865
+ try {
1866
+ const lastTurns = session.messages.slice(-6).map((m) => ({
1867
+ role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
1868
+ content: m.content,
1869
+ }));
1870
+ if (lastTurns.length < 2) return;
1871
+ broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
1872
+ import('../pi-ecosystem-judgment/human-value-pipeline.js')
1873
+ .then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
1874
+ // channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
1875
+ if (!throttleDHook(channelId, 5 * 60_000)) {
1876
+ console.log(`[D-hook ${channelId}] throttled (within 5min)`);
1877
+ broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
1878
+ return null;
1879
+ }
1880
+ broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
1881
+ return detectAndDistillFromChannel(lastTurns, { channelId });
1882
+ })
1883
+ .then((result) => {
1884
+ if (result && result.triggered) {
1885
+ console.log(
1886
+ `[D-hook ${channelId}] stored: ${result.reason}`,
1887
+ result.evolved
1888
+ );
1889
+ broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
1890
+ } else if (result && result.reason) {
1891
+ console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
1892
+ broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
1893
+ }
1894
+ })
1895
+ .catch((err) => {
1896
+ console.warn(`[D-hook ${channelId}] failed:`, err);
1897
+ broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
1898
+ });
1899
+ } catch (err) {
1900
+ console.warn(`[D-hook ${channelId}] sync error:`, err);
1901
+ }
1902
+ });
1903
+
1826
1904
  // 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
1827
1905
  if (!res.headersSent) res.json({ ok: true });
1828
1906
  } catch (err: any) {
1829
1907
  broadcast({ type: 'error', content: err.message }, channelId);
1830
1908
  broadcast({ type: 'done' }, channelId);
1831
1909
  if (!res.headersSent) res.status(500).json({ error: err.message });
1910
+ } finally {
1911
+ // queue dequeue: 跑完或失败都要清状态
1912
+ // 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
1913
+ // 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
1914
+ // (在 client.js 实现). 这里只清状态 + 广播.
1915
+ runState.running = false;
1916
+ runState.abortController = null;
1917
+ broadcastQueueUpdate(channelId);
1832
1918
  }
1833
1919
  });
1834
1920
 
@@ -1840,6 +1926,41 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1840
1926
  let didFixRunning = false;
1841
1927
  let didFixTimer: NodeJS.Timeout | null = null;
1842
1928
 
1929
+ // ---------- per-channel 消息 queue + abort 状态 ----------
1930
+ // 同 channel 串行 (避免 LLM 调用互踩上下文), 跨 channel 互不干扰
1931
+ interface PendingMessage {
1932
+ channelId: string;
1933
+ text: string;
1934
+ boundWalletAddress?: string;
1935
+ autoToolsEnabled?: boolean;
1936
+ // (req, res 已经在 /message 里 res.status(202) 返回, 入队的只是要重跑的内容参数)
1937
+ }
1938
+ interface ChannelRunState {
1939
+ running: boolean;
1940
+ queue: PendingMessage[];
1941
+ abortController: AbortController | null;
1942
+ // 2026-06-16: loop 检查 — 最近一轮的步骤/摘要/最终回复/token
1943
+ lastSteps?: Array<{ name: string; status: string; durationMs?: number; output?: string }>;
1944
+ lastSummary?: string;
1945
+ lastFinalReply?: string;
1946
+ lastTokens?: { input?: number; output?: number };
1947
+ }
1948
+ const channelRunState: Map<string, ChannelRunState> = new Map();
1949
+ function getOrCreateRunState(channelId: string): ChannelRunState {
1950
+ let s = channelRunState.get(channelId);
1951
+ if (!s) {
1952
+ s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
1953
+ channelRunState.set(channelId, s);
1954
+ }
1955
+ return s;
1956
+ }
1957
+ function broadcastQueueUpdate(channelId: string): void {
1958
+ const s = channelRunState.get(channelId);
1959
+ const queueLength = s ? s.queue.length : 0;
1960
+ const running = s ? s.running : false;
1961
+ try { broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId); } catch { /* */ }
1962
+ }
1963
+
1843
1964
  function scheduleDidFix(channelId: string) {
1844
1965
  didFixQueue.add(channelId);
1845
1966
  if (didFixTimer) return;
@@ -2555,20 +2676,39 @@ app.get('/channels', async (_req, res) => {
2555
2676
 
2556
2677
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
2557
2678
  let fullResponse = '';
2679
+ let usedJudgmentIds: string[] = [];
2558
2680
 
2559
2681
  const streamCallback: StreamCallback = (event: StreamEvent) => {
2682
+ // P0.5: 注入门回传
2683
+ if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
2684
+ usedJudgmentIds = (event as any).usedIds;
2685
+ return;
2686
+ }
2560
2687
  if (event.type === 'token' || event.type === 'thinking') {
2561
2688
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
2562
2689
  } else if (event.type === 'status' || event.type === 'tool') {
2563
2690
  broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
2691
+ } else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
2692
+ // 2026-06-15: 步骤状态机事件 — 原样转发
2693
+ broadcast({
2694
+ type: event.type,
2695
+ tool: event.tool,
2696
+ content: event.content,
2697
+ success: event.success,
2698
+ output: event.output,
2699
+ error: event.error,
2700
+ args: event.args,
2701
+ }, channelId);
2564
2702
  } else if (event.type === 'error') {
2565
2703
  broadcast({ type: 'error', content: event.content }, channelId);
2566
2704
  }
2567
2705
  };
2568
2706
 
2569
2707
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2708
+ // 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
2570
2709
  const regenHint = await buildJudgmentHint(channel, channelId);
2571
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
2710
+ const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
2711
+ fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
2572
2712
 
2573
2713
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2574
2714
 
@@ -2584,7 +2724,8 @@ app.get('/channels', async (_req, res) => {
2584
2724
  id: crypto.randomUUID(),
2585
2725
  type: 'ai' as const,
2586
2726
  content: fullResponse,
2587
- timestamp: new Date().toISOString()
2727
+ timestamp: new Date().toISOString(),
2728
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
2588
2729
  });
2589
2730
  existingSession.lastUpdated = new Date().toISOString();
2590
2731
  await saveSession(existingSession);
@@ -2755,46 +2896,6 @@ app.get('/channels', async (_req, res) => {
2755
2896
  }
2756
2897
  });
2757
2898
 
2758
- // 创建并执行工作流
2759
- app.post('/api/workflow', async (req, res) => {
2760
- try {
2761
- const { channelId, title, steps } = req.body;
2762
- if (!channelId || !steps || !Array.isArray(steps)) {
2763
- return res.status(400).json({ error: 'channelId and steps required' });
2764
- }
2765
-
2766
- const tasks = await loadTaskQueue();
2767
- const task: Task = {
2768
- id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
2769
- type: 'workflow',
2770
- title: title || '工作流',
2771
- description: `包含 ${steps.length} 个步骤的工作流`,
2772
- status: 'pending',
2773
- progress: 0,
2774
- createdAt: new Date().toISOString(),
2775
- updatedAt: new Date().toISOString(),
2776
- steps: steps.map((s: string, i: number) => ({
2777
- id: `step_${i}`,
2778
- name: s,
2779
- status: 'pending'
2780
- })),
2781
- currentStep: 0
2782
- };
2783
-
2784
- tasks.push(task);
2785
- await saveTaskQueue(tasks);
2786
-
2787
- // 自动开始执行
2788
- if (!isExecutingTask) {
2789
- executeTask(task, channelId);
2790
- }
2791
-
2792
- res.json({ ok: true, task });
2793
- } catch (err: any) {
2794
- res.status(500).json({ error: err.message });
2795
- }
2796
- });
2797
-
2798
2899
  // ==================== LLM 配置 API ====================
2799
2900
 
2800
2901
  // 获取所有 LLM 配置
@@ -4002,7 +4103,24 @@ app.get('/channels', async (_req, res) => {
4002
4103
  }
4003
4104
  });
4004
4105
 
4005
- // 主人审阅: 批准 draft
4106
+ // 终止当前 channel 的 LLM 流 (UI 终止按钮)
4107
+ app.post('/api/chat/abort', async (req, res) => {
4108
+ try {
4109
+ const { channelId } = req.body as { channelId?: string };
4110
+ if (!channelId) return res.status(400).json({ error: 'channelId required' });
4111
+ const s = channelRunState.get(channelId);
4112
+ if (s?.abortController) {
4113
+ s.abortController.abort();
4114
+ console.log(`[abort] user aborted channel=${channelId}`);
4115
+ return res.json({ ok: true, aborted: true });
4116
+ }
4117
+ res.json({ ok: true, aborted: false });
4118
+ } catch (err: any) {
4119
+ res.status(500).json({ error: err.message });
4120
+ }
4121
+ });
4122
+
4123
+ // 用户审阅: 批准 draft
4006
4124
  app.post('/api/chat/approve', async (req, res) => {
4007
4125
  try {
4008
4126
  const { messageId, peerDID, finalText } = req.body || {};
@@ -4015,7 +4133,7 @@ app.get('/channels', async (_req, res) => {
4015
4133
  }
4016
4134
  });
4017
4135
 
4018
- // 主人审阅: 丢弃 draft
4136
+ // 用户审阅: 丢弃 draft
4019
4137
  app.post('/api/chat/dismiss', async (req, res) => {
4020
4138
  try {
4021
4139
  const { messageId, peerDID } = req.body || {};
@@ -4417,22 +4535,467 @@ app.get('/channels', async (_req, res) => {
4417
4535
  }
4418
4536
  });
4419
4537
 
4420
- app.get('/api/judgments', async (_req, res) => {
4538
+ app.get('/api/judgments', async (req, res) => {
4421
4539
  try {
4422
- const { loadAllJudgments, initializeValueStore } = await import(
4540
+ const { listJudgmentsByStatus, initializeValueStore } = await import(
4423
4541
  '../pi-ecosystem-judgment/human-value-store.js'
4424
4542
  );
4425
4543
  await initializeValueStore();
4426
- const all = await loadAllJudgments();
4427
- // 新的在前
4544
+ const status = (typeof req.query.status === 'string' ? req.query.status : 'all') as
4545
+ | 'active'
4546
+ | 'pending'
4547
+ | 'superseded'
4548
+ | 'rejected'
4549
+ | 'all';
4550
+ const all = await listJudgmentsByStatus(status);
4428
4551
  all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
4429
- res.json({ count: all.length, judgments: all });
4552
+ res.json({ count: all.length, status, judgments: all });
4430
4553
  } catch (err: any) {
4431
4554
  console.error('[judgments] GET failed:', err);
4432
4555
  res.status(500).json({ error: err.message });
4433
4556
  }
4434
4557
  });
4435
4558
 
4559
+ // 蒸馏 B 触发 (人类点按钮) — 同步执行演化对齐
4560
+ app.post('/api/judgments/distill-from-conversation', async (req, res) => {
4561
+ try {
4562
+ const { channelId, messageId, recentTurns } = req.body as {
4563
+ channelId?: string;
4564
+ messageId?: string;
4565
+ recentTurns?: number;
4566
+ };
4567
+ if (!channelId) {
4568
+ return res.status(400).json({ error: 'channelId required' });
4569
+ }
4570
+
4571
+ // 取 channel 最近的对话
4572
+ const channels = await loadChannels();
4573
+ const channel = channels.find((c) => c.id === channelId);
4574
+ if (!channel) return res.status(404).json({ error: 'channel not found' });
4575
+
4576
+ const currentSessionId = channel.currentSessionId;
4577
+ if (!currentSessionId) {
4578
+ return res.status(400).json({ error: 'no active session in channel' });
4579
+ }
4580
+ const session = await loadSession(channelId, currentSessionId);
4581
+ if (!session) return res.status(404).json({ error: 'session not found' });
4582
+
4583
+ // 取最近 N 轮 (默认 10), 转成 DistillTurn 格式
4584
+ const limit = Math.min(Math.max(recentTurns ?? 10, 2), 30);
4585
+ const turns = session.messages.slice(-limit).map((m) => ({
4586
+ role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
4587
+ content: m.content,
4588
+ }));
4589
+
4590
+ const { distillAndStoreFromChannel } = await import(
4591
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4592
+ );
4593
+ const result = await distillAndStoreFromChannel(turns, { channelId });
4594
+
4595
+ res.json({
4596
+ ok: true,
4597
+ triggered: result.triggered,
4598
+ reason: result.reason,
4599
+ judgment: result.judgment,
4600
+ evolved: result.evolved,
4601
+ });
4602
+ } catch (err: any) {
4603
+ console.error('[judgments] distill-from-conversation failed:', err);
4604
+ res.status(500).json({ error: err.message });
4605
+ }
4606
+ });
4607
+
4608
+ // 蒸馏 D 触发 (AI 被动) — 后台异步,不阻塞 HTTP 响应
4609
+ app.post('/api/judgments/detect-and-distill', async (req, res) => {
4610
+ try {
4611
+ const { channelId, turns } = req.body as {
4612
+ channelId?: string;
4613
+ turns?: Array<{ role: 'human' | 'agent'; content: string }>;
4614
+ };
4615
+
4616
+ // 先立即返回 202, 不等 LLM
4617
+ res.status(202).json({ ok: true, queued: true });
4618
+
4619
+ if (!channelId || !Array.isArray(turns) || turns.length === 0) {
4620
+ return;
4621
+ }
4622
+
4623
+ // 异步处理 (不 await, 不阻塞响应)
4624
+ setImmediate(async () => {
4625
+ try {
4626
+ const { detectAndDistillFromChannel } = await import(
4627
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4628
+ );
4629
+ const result = await detectAndDistillFromChannel(turns, { channelId });
4630
+ if (result.triggered) {
4631
+ console.log(`[D-hook] ${channelId}: ${result.reason}`, result.evolved);
4632
+ }
4633
+ } catch (err) {
4634
+ console.warn('[D-hook] background failed:', err);
4635
+ }
4636
+ });
4637
+ } catch (err: any) {
4638
+ console.error('[judgments] detect-and-distill failed:', err);
4639
+ res.status(500).json({ error: err.message });
4640
+ }
4641
+ });
4642
+
4643
+ // 判断力使用回溯 (P0.5): 给定 judgmentIds, 反查对应的 decision 文本
4644
+ // 用途: UI 上"这条 AI 回复引用了哪些原则"
4645
+ app.post('/api/judgments/resolve-usage', async (req, res) => {
4646
+ try {
4647
+ const { ids } = req.body as { ids?: string[] };
4648
+ if (!Array.isArray(ids) || ids.length === 0) {
4649
+ return res.json({ items: [] });
4650
+ }
4651
+ const { loadAllJudgments } = await import(
4652
+ '../pi-ecosystem-judgment/human-value-store.js'
4653
+ );
4654
+ const all = await loadAllJudgments();
4655
+ const byId = new Map(all.map((j) => [j.id, j]));
4656
+ const items = ids
4657
+ .map((id) => byId.get(id))
4658
+ .filter((j): j is NonNullable<typeof j> => Boolean(j))
4659
+ .map((j) => ({
4660
+ id: j.id,
4661
+ decision: j.decision,
4662
+ status: j.status ?? 'active',
4663
+ timestamp: j.timestamp,
4664
+ }));
4665
+ res.json({ items });
4666
+ } catch (err: any) {
4667
+ console.error('[judgments] resolve-usage failed:', err);
4668
+ res.status(500).json({ error: err.message });
4669
+ }
4670
+ });
4671
+
4672
+ // 判断力违规日志 (P3 UI): 读 violations.jsonl
4673
+ app.get('/api/judgments/violations', async (req, res) => {
4674
+ try {
4675
+ const { getRecentViolations } = await import(
4676
+ '../pi-ecosystem-judgment/monitor-gate.js'
4677
+ );
4678
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4679
+ const items = await getRecentViolations(limit);
4680
+ res.json({ count: items.length, items });
4681
+ } catch (err: any) {
4682
+ console.error('[judgments] violations failed:', err);
4683
+ res.status(500).json({ error: err.message });
4684
+ }
4685
+ });
4686
+
4687
+ // 类 B 自适应扫描: 读 judgments.json + usage.jsonl, 给出 stale/rising/unused 建议
4688
+ // ?force=1 跳过 24h 缓存
4689
+ app.get('/api/judgments/adaptive-suggestions', async (req, res) => {
4690
+ try {
4691
+ const { getCachedScan } = await import(
4692
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4693
+ );
4694
+ const force = String(req.query.force ?? '') === '1';
4695
+ const result = await getCachedScan(force);
4696
+ res.json(result);
4697
+ } catch (err: any) {
4698
+ console.error('[judgments] adaptive-scan failed:', err);
4699
+ res.status(500).json({ error: err.message });
4700
+ }
4701
+ });
4702
+
4703
+ // Bootstrap Context 调试视图: 返出完整 BolloonContext
4704
+ app.get('/api/bolloon/context', async (req, res) => {
4705
+ try {
4706
+ const { getCachedBolloonContext } = await import(
4707
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4708
+ );
4709
+ const force = String(req.query.force ?? '') === '1';
4710
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() }, force);
4711
+ res.json(ctx);
4712
+ } catch (err: any) {
4713
+ console.error('[bolloon] context failed:', err);
4714
+ res.status(500).json({ error: err.message });
4715
+ }
4716
+ });
4717
+
4718
+ // 阶段 B: 周报 (weekly-report.ts 产物) — 仅 API 读取, 不做 UI tab
4719
+ // GET /api/reports → { files: ['2026-W24.md', ...] }
4720
+ // GET /api/reports/2026-W24 → { week, content }
4721
+ app.get('/api/reports', async (_req, res) => {
4722
+ try {
4723
+ const dir = path.join(os.homedir(), '.bolloon', 'reports');
4724
+ try {
4725
+ const entries = await fs.readdir(dir);
4726
+ const files = entries
4727
+ .filter((f) => f.endsWith('.md'))
4728
+ .sort()
4729
+ .reverse(); // 新的在前
4730
+ res.json({ dir, files });
4731
+ } catch {
4732
+ res.json({ dir, files: [] });
4733
+ }
4734
+ } catch (err: any) {
4735
+ res.status(500).json({ error: err.message });
4736
+ }
4737
+ });
4738
+
4739
+ app.get('/api/reports/:week', async (req, res) => {
4740
+ try {
4741
+ const week = req.params.week;
4742
+ // 严格校验, 防路径穿越
4743
+ if (!/^\d{4}-W\d{1,2}$/.test(week)) {
4744
+ return res.status(400).json({ error: 'week must match YYYY-Www' });
4745
+ }
4746
+ const file = path.join(os.homedir(), '.bolloon', 'reports', `${week}.md`);
4747
+ try {
4748
+ const content = await fs.readFile(file, 'utf-8');
4749
+ res.json({ week, content, length: content.length });
4750
+ } catch {
4751
+ res.status(404).json({ error: 'not found', week });
4752
+ }
4753
+ } catch (err: any) {
4754
+ res.status(500).json({ error: err.message });
4755
+ }
4756
+ });
4757
+
4758
+ // 阶段 C 护栏 5: auto-evolve baseline 管理 (无 UI, 仅 API)
4759
+ // GET /api/auto-evolve/baselines → 列出所有 baseline tag
4760
+ // GET /api/auto-evolve/baselines/:tag/diff → 看某 baseline 的 diff 摘要
4761
+ // POST /api/auto-evolve/rollback {tag} → 回滚到指定 baseline
4762
+ app.get('/api/auto-evolve/baselines', async (_req, res) => {
4763
+ try {
4764
+ const { execFile } = await import('child_process');
4765
+ const { promisify } = await import('util');
4766
+ const pExec = promisify(execFile);
4767
+ const { stdout } = await pExec('git', [
4768
+ 'tag', '-l', 'auto-evolve-baseline-*', '--format=%(refname:short)|%(contents)|%(objectname:short)|%(taggerdate:iso)',
4769
+ ], { cwd: process.cwd() });
4770
+ const tags = stdout.trim().split('\n').filter(Boolean).map((line) => {
4771
+ const [tag, msg, sha, date] = line.split('|');
4772
+ return { tag, message: msg || '', sha, date };
4773
+ });
4774
+ res.json({ tags, count: tags.length });
4775
+ } catch (err: any) {
4776
+ res.status(500).json({ error: err.message });
4777
+ }
4778
+ });
4779
+
4780
+ app.get('/api/auto-evolve/baselines/:tag/diff', async (req, res) => {
4781
+ try {
4782
+ const { execFile } = await import('child_process');
4783
+ const { promisify } = await import('util');
4784
+ const pExec = promisify(execFile);
4785
+ const tag = req.params.tag;
4786
+ if (!/^auto-evolve-baseline-[\w-]+$/.test(tag)) {
4787
+ return res.status(400).json({ error: 'tag must match auto-evolve-baseline-*' });
4788
+ }
4789
+ const { stdout } = await pExec('git', ['show', '--stat', '--no-color', tag], { cwd: process.cwd() });
4790
+ res.json({ tag, diff: stdout.slice(0, 5000) }); // 限长 5KB
4791
+ } catch (err: any) {
4792
+ res.status(500).json({ error: err.message });
4793
+ }
4794
+ });
4795
+
4796
+ // Bootstrap Context → 拼好的 system prompt 片段 (供调试看注入效果)
4797
+ app.get('/api/bolloon/context/system-prompt', async (req, res) => {
4798
+ try {
4799
+ const { getCachedBolloonContext } = await import(
4800
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4801
+ );
4802
+ const { formatContextForSystemPrompt } = await import(
4803
+ '../bootstrap/project-context.js'
4804
+ );
4805
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() });
4806
+ const systemAddition = formatContextForSystemPrompt(ctx, {
4807
+ maxChars: parseInt(String(req.query.max ?? '4000'), 10) || 4000,
4808
+ });
4809
+ res.json({ systemAddition, length: systemAddition.length, truncated: systemAddition.includes('截断模式') });
4810
+ } catch (err: any) {
4811
+ console.error('[bolloon] context/system-prompt failed:', err);
4812
+ res.status(500).json({ error: err.message });
4813
+ }
4814
+ });
4815
+
4816
+ // ============================================================
4817
+ // system-prompt health (P-Action 2 — Harness Gardening)
4818
+ // 返回每层 lifecycle 状态: ok | stale | overdue-review | missing-frontmatter | dynamic
4819
+ // query: ?activeOnly=1 → 只返回当前 context 激活的层
4820
+ // ============================================================
4821
+ app.get('/api/prompt/health', async (req, res) => {
4822
+ try {
4823
+ const { listLayers } = await import('../llm/system-prompt/registry.js');
4824
+ const { evaluateLayers, markActive } = await import('../llm/system-prompt/health.js');
4825
+ const all = listLayers() as Array<any>;
4826
+ const baseReport = evaluateLayers(all);
4827
+
4828
+ // 如果 query 里有 activeOnly, 跑一次 assembleSystemPrompt 拿激活列表
4829
+ if (String(req.query.activeOnly ?? '') === '1') {
4830
+ const { assembleSystemPrompt } = await import('../llm/system-prompt/registry.js');
4831
+ const channel = String(req.query.channel ?? 'local') as 'local' | 'p2p-visitor' | 'p2p-agent';
4832
+ const role = req.query.role as any;
4833
+ const tool = req.query.tool as any;
4834
+ try {
4835
+ const r = await assembleSystemPrompt({ channel, role, tool });
4836
+ const activeIds = new Set(r.layerIds);
4837
+ res.json(markActive(baseReport, activeIds));
4838
+ } catch (err: any) {
4839
+ console.warn('[prompt-health] assembleSystemPrompt failed (silent, returning base report):', err);
4840
+ res.json(baseReport);
4841
+ }
4842
+ } else {
4843
+ res.json(baseReport);
4844
+ }
4845
+ } catch (err: any) {
4846
+ console.error('[prompt-health] failed:', err);
4847
+ res.status(500).json({ error: err.message });
4848
+ }
4849
+ });
4850
+
4851
+ // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4852
+ // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4853
+ // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
4854
+ // 缺省 → 用户在 UI 手动触发, 不查开关 (避免阻塞用户)
4855
+ app.post('/api/judgments/adaptive-apply', async (req, res) => {
4856
+ try {
4857
+ const isAuto = req.query.auto === '1' || req.query.auto === 'true';
4858
+ const { action, suggestion, appliedPatch } = req.body as {
4859
+ action: 'accept' | 'reject' | 'revert';
4860
+ suggestion: { judgmentId: string; kind: string; decision: string; reason: string; action: string; metrics: unknown; scannedAt: string; key: string };
4861
+ appliedPatch?: Record<string, unknown>;
4862
+ };
4863
+ if (!action || !suggestion?.judgmentId) {
4864
+ return res.status(400).json({ error: 'action and suggestion.judgmentId required' });
4865
+ }
4866
+ const { updateJudgmentStatus } = await import(
4867
+ '../pi-ecosystem-judgment/human-value-store.js'
4868
+ );
4869
+ const { logEvolution } = await import(
4870
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4871
+ );
4872
+ // accept 时: 真正改库
4873
+ if (action === 'accept') {
4874
+ // 阶段 A: 自动路径需先过 auto-evolve-policy 网关
4875
+ if (isAuto) {
4876
+ const { requireDataLayerAutoEvolve } = await import(
4877
+ '../utils/auto-evolve-policy.js'
4878
+ );
4879
+ try {
4880
+ await requireDataLayerAutoEvolve('adaptive-apply.auto.deprecate');
4881
+ } catch (err: any) {
4882
+ return res.status(423).json({
4883
+ error: 'data-layer-auto-evolve-disabled',
4884
+ message: err.message,
4885
+ hint: '设 BOLLOON_AUTO_EVOLVE_DATA=1 或在 self-improve-policy.json 加 dataLayerAutoEvolve: true',
4886
+ });
4887
+ }
4888
+ }
4889
+ if (suggestion.action === 'deprecate') {
4890
+ // 标记 superseded (语义: 不再用, 但保留可回滚)
4891
+ await updateJudgmentStatus(suggestion.judgmentId, 'superseded', {
4892
+ evolutionReason: 'merged', // 借 merged 字段表达"被自适应废弃"
4893
+ });
4894
+ } else if (suggestion.action === 'boost') {
4895
+ // boost: 用户手动接受后, 不改库本身 (weight 在 getRelevantValues 里动态算),
4896
+ // 但写 evolution 留痕, 未来可以基于此调整算法
4897
+ // 当前不直接改库, 仅留痕
4898
+ }
4899
+ // 'review' 类不需要自动改库, 仅 log 接受
4900
+ }
4901
+ await logEvolution({
4902
+ ts: new Date().toISOString(),
4903
+ action,
4904
+ suggestion: suggestion as any,
4905
+ appliedPatch,
4906
+ });
4907
+ res.json({ ok: true });
4908
+ } catch (err: any) {
4909
+ console.error('[judgments] adaptive-apply failed:', err);
4910
+ res.status(500).json({ error: err.message });
4911
+ }
4912
+ });
4913
+
4914
+ // 演化日志 (audit / 一键回滚源)
4915
+ app.get('/api/judgments/evolution-log', async (req, res) => {
4916
+ try {
4917
+ const { readEvolutionLog } = await import(
4918
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4919
+ );
4920
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);
4921
+ const items = await readEvolutionLog(limit);
4922
+ res.json({ count: items.length, items });
4923
+ } catch (err: any) {
4924
+ console.error('[judgments] evolution-log failed:', err);
4925
+ res.status(500).json({ error: err.message });
4926
+ }
4927
+ });
4928
+
4929
+ // 阶段 2: Causal-judge 4 个 endpoint
4930
+ app.get('/api/judgments/causal/correlation', async (req, res) => {
4931
+ try {
4932
+ const { runCorrelationAnalysis } = await import(
4933
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4934
+ );
4935
+ const topN = Math.min(Math.max(parseInt(String(req.query.topN ?? '5'), 10) || 5, 1), 50);
4936
+ const useLLM = String(req.query.useLLM ?? '1') !== '0';
4937
+ const items = await runCorrelationAnalysis({ topN, useLLM });
4938
+ res.json({ count: items.length, items });
4939
+ } catch (err: any) {
4940
+ console.error('[causal] correlation failed:', err);
4941
+ res.status(500).json({ error: err.message });
4942
+ }
4943
+ });
4944
+
4945
+ app.get('/api/judgments/causal/intervention', async (req, res) => {
4946
+ try {
4947
+ const { runIntervention } = await import(
4948
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4949
+ );
4950
+ const { judgmentId, scenario } = req.query as { judgmentId?: string; scenario?: string };
4951
+ if (!judgmentId) return res.status(400).json({ error: 'judgmentId required' });
4952
+ const result = await runIntervention(judgmentId, { scenarioContext: scenario });
4953
+ res.json(result);
4954
+ } catch (err: any) {
4955
+ console.error('[causal] intervention failed:', err);
4956
+ res.status(500).json({ error: err.message });
4957
+ }
4958
+ });
4959
+
4960
+ app.post('/api/judgments/causal/counterfactual', async (req, res) => {
4961
+ try {
4962
+ const { runCounterfactualAudit } = await import(
4963
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4964
+ );
4965
+ const { userInput, aiReply, violatedPrinciples } = req.body as {
4966
+ userInput?: string;
4967
+ aiReply?: string;
4968
+ violatedPrinciples?: Array<{ principle: string; reason: string }>;
4969
+ };
4970
+ if (!userInput || !aiReply) {
4971
+ return res.status(400).json({ error: 'userInput and aiReply required' });
4972
+ }
4973
+ const audit = await runCounterfactualAudit({
4974
+ userInput,
4975
+ aiReply,
4976
+ violatedPrinciples: violatedPrinciples ?? [],
4977
+ });
4978
+ res.json(audit);
4979
+ } catch (err: any) {
4980
+ console.error('[causal] counterfactual failed:', err);
4981
+ res.status(500).json({ error: err.message });
4982
+ }
4983
+ });
4984
+
4985
+ app.get('/api/judgments/causal/audit-log', async (req, res) => {
4986
+ try {
4987
+ const { readCounterfactualLog } = await import(
4988
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4989
+ );
4990
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4991
+ const items = await readCounterfactualLog(limit);
4992
+ res.json({ count: items.length, items });
4993
+ } catch (err: any) {
4994
+ console.error('[causal] audit-log failed:', err);
4995
+ res.status(500).json({ error: err.message });
4996
+ }
4997
+ });
4998
+
4436
4999
  // 导入判断: 接受 { filename, content (base64), context }.
4437
5000
  // 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
4438
5001
  // 解析规则:
@@ -4767,26 +5330,196 @@ app.get('/channels', async (_req, res) => {
4767
5330
  res.json(getEventHistory());
4768
5331
  });
4769
5332
 
5333
+ // ============================================================
5334
+ // Permission Mode (P2.2 — UI 暴露开关)
5335
+ // 优先级: 运行时 session 覆盖 > env BOLLOON_PERM_MODE > 'default'
5336
+ // 运行时覆盖存在 ~/.bolloon/sessions/permission-mode.json, 每次 promptStream 入口读取
5337
+ // ============================================================
5338
+
5339
+ const PERM_MODE_FILE = path.join(
5340
+ process.env.HOME || os.homedir() || '/tmp',
5341
+ '.bolloon', 'sessions', 'permission-mode.json'
5342
+ );
5343
+
5344
+ function readPermModeOverride(): { mode: string; ts: string } | null {
5345
+ try {
5346
+ // 同步读, 文件很小
5347
+ const raw = fsSync.readFileSync(PERM_MODE_FILE, 'utf-8');
5348
+ const obj = JSON.parse(raw);
5349
+ if (obj && typeof obj.mode === 'string') {
5350
+ return { mode: obj.mode, ts: obj.ts || new Date().toISOString() };
5351
+ }
5352
+ } catch { /* 不存在 = 无 override */ }
5353
+ return null;
5354
+ }
5355
+
5356
+ function writePermModeOverride(mode: string): void {
5357
+ try {
5358
+ const dir = path.dirname(PERM_MODE_FILE);
5359
+ fsSync.mkdirSync(dir, { recursive: true });
5360
+ fsSync.writeFileSync(PERM_MODE_FILE, JSON.stringify({ mode, ts: new Date().toISOString() }, null, 2), 'utf-8');
5361
+ } catch (err) {
5362
+ console.warn('[server] writePermModeOverride failed:', err);
5363
+ }
5364
+ }
5365
+
5366
+ // 读当前生效 mode (runtime override > env > default)
5367
+ app.get('/api/permission-mode', async (_req: any, res: any) => {
5368
+ const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
5369
+ const override = readPermModeOverride();
5370
+ const envMode = process.env.BOLLOON_PERM_MODE || null;
5371
+ const effective = resolvePermissionMode();
5372
+ res.json({
5373
+ effective,
5374
+ override: override?.mode || null,
5375
+ overrideTs: override?.ts || null,
5376
+ env: envMode,
5377
+ allowed: ALL_PERMISSION_MODES,
5378
+ description: {
5379
+ default: '每次工具调用询问; shell 走 shell-guard',
5380
+ acceptEdits: 'edit_*/write_* 跳过黑名单; shell 仍走 shell-guard',
5381
+ bypassPermissions: '非 shell 全部放行; shell 永远走 shell-guard (硬约束)',
5382
+ },
5383
+ });
5384
+ });
5385
+
5386
+ // 设 runtime override (存盘, 下次 promptStream 入口读取生效)
5387
+ app.post('/api/permission-mode', async (req: any, res: any) => {
5388
+ const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
5389
+ const mode = String(req.body?.mode || '');
5390
+ if (!ALL_PERMISSION_MODES.includes(mode as any)) {
5391
+ return res.status(400).json({
5392
+ error: `Invalid mode. Allowed: ${ALL_PERMISSION_MODES.join(', ')}`,
5393
+ allowed: ALL_PERMISSION_MODES,
5394
+ });
5395
+ }
5396
+ const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
5397
+ writePermModeOverride(mode);
5398
+ // 写历史 (append-only JSONL, 跟 bolloon 其他 audit 一致)
5399
+ try {
5400
+ const HISTORY_FILE = path.join(
5401
+ process.env.HOME || os.homedir() || '/tmp',
5402
+ '.bolloon', 'sessions', 'permission-mode-history.jsonl'
5403
+ );
5404
+ fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
5405
+ fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
5406
+ ts: new Date().toISOString(),
5407
+ from: oldMode,
5408
+ to: mode,
5409
+ source: 'api',
5410
+ }) + '\n', 'utf-8');
5411
+ } catch { /* 历史写失败不阻塞主流程 */ }
5412
+ console.log(`[server] permission-mode override set to "${mode}" via API (was "${oldMode}")`);
5413
+ res.json({
5414
+ ok: true,
5415
+ mode,
5416
+ previousMode: oldMode,
5417
+ ts: new Date().toISOString(),
5418
+ note: '新 mode 在下一次 promptStream 入口生效 (不打断当前对话)',
5419
+ });
5420
+ });
5421
+
5422
+ // 取消 runtime override, 回到 env 或 default
5423
+ app.delete('/api/permission-mode', async (_req: any, res: any) => {
5424
+ const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
5425
+ try {
5426
+ if (fsSync.existsSync(PERM_MODE_FILE)) fsSync.unlinkSync(PERM_MODE_FILE);
5427
+ } catch (err) {
5428
+ console.warn('[server] delete perm-mode override failed:', err);
5429
+ }
5430
+ // 写历史
5431
+ try {
5432
+ const HISTORY_FILE = path.join(
5433
+ process.env.HOME || os.homedir() || '/tmp',
5434
+ '.bolloon', 'sessions', 'permission-mode-history.jsonl'
5435
+ );
5436
+ fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
5437
+ fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
5438
+ ts: new Date().toISOString(),
5439
+ from: oldMode,
5440
+ to: 'env-or-default',
5441
+ source: 'api',
5442
+ action: 'delete-override',
5443
+ }) + '\n', 'utf-8');
5444
+ } catch { /* ignore */ }
5445
+ res.json({ ok: true, note: '已删除 runtime override, 回到 env / default' });
5446
+ });
5447
+
5448
+ // 2026-06-16: 循环检查 — GET /api/loop/inspect?channelId=...
5449
+ // 返回最近一轮 ReAct loop 的产出: 步骤 / 工具调用 / 压缩摘要 / 最终回复 / token 用量.
5450
+ // 用于前端 status bar 的「✓ 检查」按钮弹 modal.
5451
+ app.get('/api/loop/inspect', async (req: any, res: any) => {
5452
+ try {
5453
+ const channelId = String(req.query.channelId || '');
5454
+ if (!channelId) return res.status(400).json({ error: 'channelId required' });
5455
+ const s = channelRunState.get(channelId);
5456
+ if (!s) return res.json({ summary: '该 channel 无活跃 loop', steps: [], finalReply: '' });
5457
+ const steps = (s.lastSteps || []).map((st: any) => ({
5458
+ name: st.name || st.tool || 'step',
5459
+ status: st.status || 'completed',
5460
+ durationMs: st.durationMs,
5461
+ output: st.output || st.result || '',
5462
+ }));
5463
+ res.json({
5464
+ summary: s.lastSummary || (s.running ? 'loop 仍在运行中' : 'loop 已结束'),
5465
+ steps,
5466
+ finalReply: s.lastFinalReply || '',
5467
+ tokens: s.lastTokens || {},
5468
+ });
5469
+ } catch (err: any) {
5470
+ res.status(500).json({ error: err.message });
5471
+ }
5472
+ });
5473
+
5474
+ // 历史 (类似 self-improve history, 供前端 timeline)
5475
+ app.get('/api/permission-mode/history', async (_req: any, res: any) => {
5476
+ const HISTORY_FILE = path.join(
5477
+ process.env.HOME || os.homedir() || '/tmp',
5478
+ '.bolloon', 'sessions', 'permission-mode-history.jsonl'
5479
+ );
5480
+ try {
5481
+ if (!fsSync.existsSync(HISTORY_FILE)) return res.json([]);
5482
+ const lines = fsSync.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean);
5483
+ const entries = lines.map((l: string) => {
5484
+ try { return JSON.parse(l); } catch { return null; }
5485
+ }).filter(Boolean);
5486
+ res.json(entries.slice(-50)); // 最近 50 条
5487
+ } catch (err) {
5488
+ res.json([]);
5489
+ }
5490
+ });
5491
+
4770
5492
  // 健康检查错误数 ≥ 2 -> 触发自改信号
5493
+ // 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
4771
5494
  if (healthMonitor) {
4772
- healthMonitor.startPeriodicCheck(60000, (status: any) => {
4773
- const errorCount = Object.values(status.checks as Record<string, { status: string }>)
4774
- .filter((c) => c.status === 'error').length;
4775
- if (errorCount >= 2) {
4776
- import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
4777
- const failedKeys = Object.entries(status.checks as Record<string, { status: string }>)
4778
- .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
4779
- reportSelfImproveEvent({
4780
- kind: 'silent-timeout',
4781
- details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
5495
+ if (selfImproveEnabled) {
5496
+ healthMonitor.startPeriodicCheck(60000, (status: any) => {
5497
+ const errorCount = Object.values(status.checks as Record<string, { status: string }>)
5498
+ .filter((c) => c.status === 'error').length;
5499
+ if (errorCount >= 2) {
5500
+ import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
5501
+ const failedKeys = Object.entries(status.checks as Record<string, { status: string }>)
5502
+ .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
5503
+ reportSelfImproveEvent({
5504
+ kind: 'silent-timeout',
5505
+ details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
5506
+ });
4782
5507
  });
4783
- });
4784
- }
4785
- });
5508
+ }
5509
+ });
5510
+ } else {
5511
+ // 用户模式: 只跑监控不打自改信号, 心跳仍工作
5512
+ healthMonitor.startPeriodicCheck(60000);
5513
+ console.log('[24h] Health monitor periodic check (no self-improve)');
5514
+ }
4786
5515
  }
4787
5516
 
4788
- // 安装自改总线 -> SSE 桥
4789
- void installSelfImproveHook();
5517
+ // 安装自改总线 -> SSE 桥 (开发者模式才装, 用户模式靠 /api/self-improve/trigger 手动触发)
5518
+ if (selfImproveEnabled) {
5519
+ void installSelfImproveHook();
5520
+ } else {
5521
+ console.log('[self-improve] 用户模式, installSelfImproveHook 跳过 (可用 POST /api/self-improve/trigger 手动触发)');
5522
+ }
4790
5523
 
4791
5524
  // 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
4792
5525
  return new Promise<{ app: express.Express; server: ReturnType<typeof createServer>; port: number }>((resolve, reject) => {
@@ -4808,11 +5541,24 @@ app.get('/channels', async (_req, res) => {
4808
5541
  console.log('服务器已监听');
4809
5542
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
4810
5543
  void installChatBusHook();
5544
+ // 2026-06-16: ping 改为 data: {"type":"ping"} — 之前是 SSE 注释格式 (: ping\n\n),
5545
+ // 浏览器 EventSource 不触发 onmessage, 客户端 60s 阈值 (现已 30s) 误判死链.
5546
+ // 改后前端 onmessage 收到 ping 就重置 lastEventTime, 真死链才 30s 后重建.
4811
5547
  setInterval(() => {
4812
5548
  for (const client of sseClients) {
4813
- client.res.write(': ping\n\n');
5549
+ try {
5550
+ client.res.write('data: {"type":"ping"}\n\n');
5551
+ } catch (err) {
5552
+ // socket 已断, 跳过 — client 端 onerror 会触发重连
5553
+ console.warn('[SSE ping] write 失败, 跳过该客户端:', (err as Error).message);
5554
+ }
4814
5555
  }
4815
5556
  }, 30000);
5557
+ // 2026-06-16: 全局捕获 socket error 事件, 避免未处理 EPIPE/ETIMEDOUT 让进程崩
5558
+ currentServer.on('clientError', (err, socket) => {
5559
+ console.warn('[server] clientError:', (err as any).code, err.message);
5560
+ try { socket.end(); } catch {}
5561
+ });
4816
5562
  resolve({ app, server: currentServer, port: currentPort });
4817
5563
  });
4818
5564
  };
@@ -4982,4 +5728,4 @@ export async function openBrowser(url: string) {
4982
5728
  console.error('打开浏览器失败:', err.message);
4983
5729
  }
4984
5730
  });
4985
- }
5731
+ }