@bolloon/bolloon-agent 0.1.35 → 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 (194) hide show
  1. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  2. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  3. package/Bolloon.md +45 -4
  4. package/Dive-into/CITATION.cff +17 -0
  5. package/Dive-into/LICENSE +25 -0
  6. package/Dive-into/README.md +598 -0
  7. package/Dive-into/README_zh.md +599 -0
  8. package/Dive-into/assets/context.png +0 -0
  9. package/Dive-into/assets/extensibility.png +0 -0
  10. package/Dive-into/assets/iteration.png +0 -0
  11. package/Dive-into/assets/layered_architecture.png +0 -0
  12. package/Dive-into/assets/main_structure.png +0 -0
  13. package/Dive-into/assets/permission.png +0 -0
  14. package/Dive-into/assets/session_compact.png +0 -0
  15. package/Dive-into/assets/subagent.png +0 -0
  16. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  17. package/README.md +1 -1
  18. package/dist/agents/p2p-chat-tools.js +6 -6
  19. package/dist/agents/permission-mode.js +115 -0
  20. package/dist/agents/pi-sdk.js +435 -25
  21. package/dist/agents/pre-tool-validator.js +194 -0
  22. package/dist/agents/workflow-pivot-loop.js +113 -12
  23. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  24. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  25. package/dist/bollharness/src/scripts/context_router.js +1 -1
  26. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  27. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  28. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  29. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  30. package/dist/bollharness-integration/context-router.js +2 -2
  31. package/dist/bollharness-integration/guard-checker.js +1 -1
  32. package/dist/bootstrap/bootstrap.js +3 -0
  33. package/dist/bootstrap/context-collector.js +17 -1
  34. package/dist/bootstrap/context-hierarchy.js +218 -0
  35. package/dist/bootstrap/lifecycle-hooks.js +86 -24
  36. package/dist/bootstrap/project-context.js +10 -3
  37. package/dist/context-compaction/auto-compact.js +144 -0
  38. package/dist/context-compaction/budget-gate.js +28 -0
  39. package/dist/context-compaction/budget-reduce.js +35 -0
  40. package/dist/context-compaction/context-collapse.js +66 -0
  41. package/dist/context-compaction/index.js +21 -0
  42. package/dist/context-compaction/microcompact.js +51 -0
  43. package/dist/context-compaction/pipeline.js +123 -0
  44. package/dist/context-compaction/snip.js +45 -0
  45. package/dist/context-compaction/token-estimator.js +35 -0
  46. package/dist/context-compaction/types.js +19 -0
  47. package/dist/heartbeat/HealthMonitor.js +3 -2
  48. package/dist/index.js +38 -2
  49. package/dist/llm/llm-judgment-client.js +32 -30
  50. package/dist/llm/pi-ai.js +103 -16
  51. package/dist/llm/system-prompt/health.js +129 -0
  52. package/dist/llm/system-prompt/registry.js +246 -0
  53. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  54. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  55. package/dist/llm/tool-manifest/bash.js +23 -0
  56. package/dist/llm/tool-manifest/create_file.js +24 -0
  57. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  58. package/dist/llm/tool-manifest/image_search.js +24 -0
  59. package/dist/llm/tool-manifest/index.js +69 -0
  60. package/dist/llm/tool-manifest/mcp.js +43 -0
  61. package/dist/llm/tool-manifest/message_compose.js +31 -0
  62. package/dist/llm/tool-manifest/places.js +83 -0
  63. package/dist/llm/tool-manifest/present_files.js +21 -0
  64. package/dist/llm/tool-manifest/recipe.js +40 -0
  65. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  66. package/dist/llm/tool-manifest/str_replace.js +27 -0
  67. package/dist/llm/tool-manifest/types.js +7 -0
  68. package/dist/llm/tool-manifest/view.js +24 -0
  69. package/dist/llm/tool-manifest/weather.js +27 -0
  70. package/dist/llm/tool-manifest/web.js +51 -0
  71. package/dist/network/p2p-direct.js +23 -0
  72. package/dist/network/source-intent-broadcaster.js +203 -0
  73. package/dist/network/source-intent.js +100 -0
  74. package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
  75. package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
  76. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  77. package/dist/security/context-router-tool.js +1 -1
  78. package/dist/security/input-scanner.js +223 -0
  79. package/dist/web/client.js +3677 -4161
  80. package/dist/web/components/p2p/P2PModal.js +188 -0
  81. package/dist/web/components/p2p/index.js +264 -226
  82. package/dist/web/components/p2p/p2p-modal.js +657 -0
  83. package/dist/web/components/p2p/p2p-tools.js +248 -0
  84. package/dist/web/index.html +58 -67
  85. package/dist/web/server.js +299 -117
  86. package/dist/web/style.css +531 -249
  87. package/dist/web/ui/message-renderer.js +463 -0
  88. package/dist/web/ui/step-timeline.js +375 -0
  89. package/lefthook.yml +4 -0
  90. package/package.json +1 -1
  91. package/scripts/auto-evolve-loop.ts +105 -0
  92. package/scripts/build-web.ts +35 -1
  93. package/scripts/validate-system-prompt.ts +142 -0
  94. package/src/agents/p2p-chat-tools.ts +6 -6
  95. package/src/agents/permission-mode.ts +127 -0
  96. package/src/agents/pi-sdk.ts +461 -28
  97. package/src/agents/pre-tool-validator.ts +213 -0
  98. package/src/agents/workflow-pivot-loop.ts +110 -19
  99. package/src/bollharness/CLAUDE.md +1 -1
  100. package/src/bollharness/README.md +2 -2
  101. package/src/bollharness/README.zh-CN.md +2 -2
  102. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  103. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  104. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  105. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  106. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  107. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  108. package/src/bollharness/src/scripts/context_router.ts +1 -1
  109. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  110. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  111. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  112. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  113. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  114. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  115. package/src/bollharness-integration/context-router.ts +2 -2
  116. package/src/bollharness-integration/guard-checker.ts +1 -1
  117. package/src/bootstrap/bootstrap.ts +3 -0
  118. package/src/bootstrap/context-collector.ts +30 -1
  119. package/src/bootstrap/context-hierarchy.ts +283 -0
  120. package/src/bootstrap/lifecycle-hooks.ts +139 -26
  121. package/src/bootstrap/project-context.ts +11 -3
  122. package/src/context-compaction/auto-compact.ts +153 -0
  123. package/src/context-compaction/budget-gate.ts +32 -0
  124. package/src/context-compaction/budget-reduce.ts +37 -0
  125. package/src/context-compaction/context-collapse.ts +72 -0
  126. package/src/context-compaction/index.ts +24 -0
  127. package/src/context-compaction/microcompact.ts +54 -0
  128. package/src/context-compaction/pipeline.ts +133 -0
  129. package/src/context-compaction/snip.ts +51 -0
  130. package/src/context-compaction/token-estimator.ts +36 -0
  131. package/src/context-compaction/types.ts +99 -0
  132. package/src/heartbeat/HealthMonitor.ts +3 -2
  133. package/src/index.ts +36 -2
  134. package/src/llm/llm-judgment-client.ts +36 -35
  135. package/src/llm/pi-ai.ts +111 -16
  136. package/src/llm/system-prompt/health.ts +159 -0
  137. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  138. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  139. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  140. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  141. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  142. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  143. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  144. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  145. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  146. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  147. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  148. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  149. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  150. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  151. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  152. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  153. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  154. package/src/llm/system-prompt/layers/role/security.md +15 -0
  155. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  156. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  157. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  158. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  159. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  160. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  161. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  162. package/src/llm/system-prompt/registry.ts +325 -0
  163. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  164. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  165. package/src/llm/tool-manifest/bash.ts +25 -0
  166. package/src/llm/tool-manifest/create_file.ts +26 -0
  167. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  168. package/src/llm/tool-manifest/image_search.ts +26 -0
  169. package/src/llm/tool-manifest/index.ts +88 -0
  170. package/src/llm/tool-manifest/mcp.ts +46 -0
  171. package/src/llm/tool-manifest/message_compose.ts +33 -0
  172. package/src/llm/tool-manifest/places.ts +86 -0
  173. package/src/llm/tool-manifest/present_files.ts +23 -0
  174. package/src/llm/tool-manifest/recipe.ts +42 -0
  175. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  176. package/src/llm/tool-manifest/str_replace.ts +29 -0
  177. package/src/llm/tool-manifest/types.ts +52 -0
  178. package/src/llm/tool-manifest/view.ts +26 -0
  179. package/src/llm/tool-manifest/weather.ts +29 -0
  180. package/src/llm/tool-manifest/web.ts +54 -0
  181. package/src/network/p2p-direct.ts +22 -0
  182. package/src/network/source-intent-broadcaster.ts +242 -0
  183. package/src/network/source-intent.ts +167 -0
  184. package/src/security/context-router-tool.ts +1 -1
  185. package/src/security/input-scanner.ts +287 -0
  186. package/src/web/client.js +271 -2817
  187. package/src/web/client.ts +4326 -0
  188. package/src/web/index.html +58 -67
  189. package/src/web/server.ts +301 -141
  190. package/src/web/style.css +531 -249
  191. package/src/web/ui/message-renderer.ts +540 -0
  192. package/src/web/ui/step-timeline.ts +394 -0
  193. package/test-results/.last-run.json +6 -0
  194. 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
@@ -31,15 +31,15 @@ function resolveWebRoot() {
31
31
  }
32
32
  const d = _baseDirname;
33
33
  const candidates = [
34
- path.join(d), // dist/web
35
- 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 跑时)
36
36
  path.join(d, '..', 'web'), // dist/ → web/ 兄弟
37
37
  ];
38
38
  for (const c of candidates) {
39
39
  if (fsSync.existsSync(path.join(c, 'index.html')))
40
40
  return c;
41
41
  }
42
- return candidates[1];
42
+ return candidates[0];
43
43
  }
44
44
  const webRoot = resolveWebRoot();
45
45
  console.log(`[web] webRoot = ${webRoot}`);
@@ -140,7 +140,6 @@ async function saveTheme(theme, agentId) {
140
140
  }
141
141
  // ==================== Task Queue & Workflow System ====================
142
142
  const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
143
- const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
144
143
  async function loadTaskQueue() {
145
144
  try {
146
145
  const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
@@ -153,33 +152,6 @@ async function loadTaskQueue() {
153
152
  async function saveTaskQueue(tasks) {
154
153
  await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
155
154
  }
156
- async function loadWorkflowState(channelId) {
157
- try {
158
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
159
- const states = JSON.parse(data);
160
- return states.find(s => s.channelId === channelId) || null;
161
- }
162
- catch {
163
- return null;
164
- }
165
- }
166
- async function saveWorkflowState(state) {
167
- try {
168
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
169
- const states = JSON.parse(data);
170
- const index = states.findIndex(s => s.channelId === state.channelId);
171
- if (index >= 0) {
172
- states[index] = state;
173
- }
174
- else {
175
- states.push(state);
176
- }
177
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
178
- }
179
- catch {
180
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
181
- }
182
- }
183
155
  let isExecutingTask = false;
184
156
  let executionTaskId = null;
185
157
  async function executeTask(task, channelId) {
@@ -221,32 +193,6 @@ async function executeTask(task, channelId) {
221
193
  result = `📝 文档总结:\n\n${summary.summary}`;
222
194
  }
223
195
  break;
224
- case 'workflow':
225
- // 执行多步骤工作流
226
- if (task.steps && task.steps.length > 0) {
227
- let loopCount = 0;
228
- for (let i = 0; i < task.steps.length; i++) {
229
- // 广播循环开始
230
- loopCount++;
231
- broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
232
- task.steps[i].status = 'running';
233
- broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
234
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
235
- // 执行步骤 - 模拟流式输出
236
- for (let j = 0; j < 3; j++) {
237
- await new Promise(resolve => setTimeout(resolve, 300));
238
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
239
- }
240
- task.steps[i].status = 'completed';
241
- task.progress = Math.round(((i + 1) / task.steps.length) * 100);
242
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
243
- broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
244
- broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
245
- }
246
- result = '✅ 工作流执行完成';
247
- broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
248
- }
249
- break;
250
196
  default:
251
197
  result = '未知任务类型';
252
198
  }
@@ -632,7 +578,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
632
578
  const { getMinimax } = await import('../constraints/index.js');
633
579
  const llm = getMinimax();
634
580
  // v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
635
- 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`;
636
582
  // v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
637
583
  let dirHint = '';
638
584
  const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
@@ -654,7 +600,10 @@ async function handleV3P2PMessage(parsed, conn, comm) {
654
600
  }
655
601
  dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
656
602
  }
657
- 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`;
658
607
  let fullResponse = '';
659
608
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
660
609
  let lastFlushAt = 0;
@@ -1475,6 +1424,35 @@ export async function createWebServer(port = 3000, options = {}) {
1475
1424
  broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
1476
1425
  console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
1477
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
+ }
1478
1456
  else if (event.type === 'error') {
1479
1457
  broadcast({ type: 'error', content: event.content }, channelId);
1480
1458
  }
@@ -1485,7 +1463,7 @@ export async function createWebServer(port = 3000, options = {}) {
1485
1463
  if (realChannelDid)
1486
1464
  contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
1487
1465
  // v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
1488
- contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "主人" 即可.\n`;
1466
+ contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
1489
1467
  if (boundWalletAddress) {
1490
1468
  contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
1491
1469
  }
@@ -1632,7 +1610,13 @@ export async function createWebServer(port = 3000, options = {}) {
1632
1610
  if (contextHint)
1633
1611
  contextHint += '\n';
1634
1612
  try {
1635
- fullResponse = await agent.promptStream(contextHint + text, streamCallback, runState.abortController?.signal, channelId);
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);
1636
1620
  }
1637
1621
  catch (err) {
1638
1622
  // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
@@ -1747,7 +1731,7 @@ export async function createWebServer(port = 3000, options = {}) {
1747
1731
  function getOrCreateRunState(channelId) {
1748
1732
  let s = channelRunState.get(channelId);
1749
1733
  if (!s) {
1750
- s = { running: false, queue: [], abortController: null };
1734
+ s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
1751
1735
  channelRunState.set(channelId, s);
1752
1736
  }
1753
1737
  return s;
@@ -2463,13 +2447,27 @@ export async function createWebServer(port = 3000, options = {}) {
2463
2447
  else if (event.type === 'status' || event.type === 'tool') {
2464
2448
  broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
2465
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
+ }
2466
2462
  else if (event.type === 'error') {
2467
2463
  broadcast({ type: 'error', content: event.content }, channelId);
2468
2464
  }
2469
2465
  };
2470
2466
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2467
+ // 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
2471
2468
  const regenHint = await buildJudgmentHint(channel, channelId);
2472
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback, undefined, channelId);
2469
+ const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
2470
+ fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
2473
2471
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2474
2472
  // 更新 session
2475
2473
  const existingSession = await loadSession(channelId, currentSessionId);
@@ -2639,42 +2637,6 @@ export async function createWebServer(port = 3000, options = {}) {
2639
2637
  res.status(500).json({ error: err.message });
2640
2638
  }
2641
2639
  });
2642
- // 创建并执行工作流
2643
- app.post('/api/workflow', async (req, res) => {
2644
- try {
2645
- const { channelId, title, steps } = req.body;
2646
- if (!channelId || !steps || !Array.isArray(steps)) {
2647
- return res.status(400).json({ error: 'channelId and steps required' });
2648
- }
2649
- const tasks = await loadTaskQueue();
2650
- const task = {
2651
- id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
2652
- type: 'workflow',
2653
- title: title || '工作流',
2654
- description: `包含 ${steps.length} 个步骤的工作流`,
2655
- status: 'pending',
2656
- progress: 0,
2657
- createdAt: new Date().toISOString(),
2658
- updatedAt: new Date().toISOString(),
2659
- steps: steps.map((s, i) => ({
2660
- id: `step_${i}`,
2661
- name: s,
2662
- status: 'pending'
2663
- })),
2664
- currentStep: 0
2665
- };
2666
- tasks.push(task);
2667
- await saveTaskQueue(tasks);
2668
- // 自动开始执行
2669
- if (!isExecutingTask) {
2670
- executeTask(task, channelId);
2671
- }
2672
- res.json({ ok: true, task });
2673
- }
2674
- catch (err) {
2675
- res.status(500).json({ error: err.message });
2676
- }
2677
- });
2678
2640
  // ==================== LLM 配置 API ====================
2679
2641
  // 获取所有 LLM 配置
2680
2642
  app.get('/api/llm-config', async (req, res) => {
@@ -3827,7 +3789,7 @@ export async function createWebServer(port = 3000, options = {}) {
3827
3789
  res.status(500).json({ error: err.message });
3828
3790
  }
3829
3791
  });
3830
- // 主人审阅: 批准 draft
3792
+ // 用户审阅: 批准 draft
3831
3793
  app.post('/api/chat/approve', async (req, res) => {
3832
3794
  try {
3833
3795
  const { messageId, peerDID, finalText } = req.body || {};
@@ -3841,7 +3803,7 @@ export async function createWebServer(port = 3000, options = {}) {
3841
3803
  res.status(500).json({ error: err.message });
3842
3804
  }
3843
3805
  });
3844
- // 主人审阅: 丢弃 draft
3806
+ // 用户审阅: 丢弃 draft
3845
3807
  app.post('/api/chat/dismiss', async (req, res) => {
3846
3808
  try {
3847
3809
  const { messageId, peerDID } = req.body || {};
@@ -4454,6 +4416,42 @@ export async function createWebServer(port = 3000, options = {}) {
4454
4416
  res.status(500).json({ error: err.message });
4455
4417
  }
4456
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
+ });
4457
4455
  // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4458
4456
  // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4459
4457
  // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
@@ -4919,25 +4917,192 @@ export async function createWebServer(port = 3000, options = {}) {
4919
4917
  const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
4920
4918
  res.json(getEventHistory());
4921
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
+ });
4922
5074
  // 健康检查错误数 ≥ 2 -> 触发自改信号
5075
+ // 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
4923
5076
  if (healthMonitor) {
4924
- healthMonitor.startPeriodicCheck(60000, (status) => {
4925
- const errorCount = Object.values(status.checks)
4926
- .filter((c) => c.status === 'error').length;
4927
- if (errorCount >= 2) {
4928
- import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
4929
- const failedKeys = Object.entries(status.checks)
4930
- .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
4931
- reportSelfImproveEvent({
4932
- kind: 'silent-timeout',
4933
- 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
+ });
4934
5089
  });
4935
- });
4936
- }
4937
- });
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 手动触发)');
4938
5105
  }
4939
- // 安装自改总线 -> SSE 桥
4940
- void installSelfImproveHook();
4941
5106
  // 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
4942
5107
  return new Promise((resolve, reject) => {
4943
5108
  const maxAttempts = 10;
@@ -4957,11 +5122,28 @@ export async function createWebServer(port = 3000, options = {}) {
4957
5122
  console.log('服务器已监听');
4958
5123
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
4959
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 后重建.
4960
5128
  setInterval(() => {
4961
5129
  for (const client of sseClients) {
4962
- 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
+ }
4963
5137
  }
4964
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
+ });
4965
5147
  resolve({ app, server: currentServer, port: currentPort });
4966
5148
  });
4967
5149
  };