@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
package/src/web/server.ts CHANGED
@@ -40,14 +40,14 @@ function resolveWebRoot(): string {
40
40
  }
41
41
  const d = _baseDirname;
42
42
  const candidates = [
43
- path.join(d), // dist/web
44
- 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 跑时)
45
45
  path.join(d, '..', 'web'), // dist/ → web/ 兄弟
46
46
  ];
47
47
  for (const c of candidates) {
48
48
  if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
49
49
  }
50
- return candidates[1];
50
+ return candidates[0];
51
51
  }
52
52
  const webRoot = resolveWebRoot();
53
53
  console.log(`[web] webRoot = ${webRoot}`);
@@ -238,11 +238,10 @@ async function saveTheme(theme: 'light' | 'dark', agentId: string): Promise<void
238
238
  // ==================== Task Queue & Workflow System ====================
239
239
 
240
240
  const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
241
- const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
242
241
 
243
242
  interface Task {
244
243
  id: string;
245
- type: 'read' | 'summarize' | 'improve' | 'chat' | 'workflow';
244
+ type: 'read' | 'summarize' | 'improve' | 'chat';
246
245
  title: string;
247
246
  description?: string;
248
247
  status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
@@ -251,23 +250,10 @@ interface Task {
251
250
  error?: string;
252
251
  createdAt: string;
253
252
  updatedAt: string;
254
- steps?: TaskStep[];
253
+ steps?: any[];
255
254
  currentStep?: number;
256
255
  }
257
256
 
258
- interface TaskStep {
259
- id: string;
260
- name: string;
261
- status: 'pending' | 'running' | 'completed' | 'failed';
262
- result?: string;
263
- }
264
-
265
- interface WorkflowState {
266
- channelId: string;
267
- tasks: Task[];
268
- lastUpdated: string;
269
- }
270
-
271
257
  async function loadTaskQueue(): Promise<Task[]> {
272
258
  try {
273
259
  const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
@@ -281,32 +267,6 @@ async function saveTaskQueue(tasks: Task[]): Promise<void> {
281
267
  await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
282
268
  }
283
269
 
284
- async function loadWorkflowState(channelId: string): Promise<WorkflowState | null> {
285
- try {
286
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
287
- const states = JSON.parse(data) as WorkflowState[];
288
- return states.find(s => s.channelId === channelId) || null;
289
- } catch {
290
- return null;
291
- }
292
- }
293
-
294
- async function saveWorkflowState(state: WorkflowState): Promise<void> {
295
- try {
296
- const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
297
- const states = JSON.parse(data) as WorkflowState[];
298
- const index = states.findIndex(s => s.channelId === state.channelId);
299
- if (index >= 0) {
300
- states[index] = state;
301
- } else {
302
- states.push(state);
303
- }
304
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
305
- } catch {
306
- await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
307
- }
308
- }
309
-
310
270
  let isExecutingTask = false;
311
271
  let executionTaskId: string | null = null;
312
272
 
@@ -355,37 +315,6 @@ async function executeTask(task: Task, channelId: string): Promise<void> {
355
315
  }
356
316
  break;
357
317
 
358
- case 'workflow':
359
- // 执行多步骤工作流
360
- if (task.steps && task.steps.length > 0) {
361
- let loopCount = 0;
362
- for (let i = 0; i < task.steps.length; i++) {
363
- // 广播循环开始
364
- loopCount++;
365
- broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
366
-
367
- task.steps[i].status = 'running';
368
- broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
369
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
370
-
371
- // 执行步骤 - 模拟流式输出
372
- for (let j = 0; j < 3; j++) {
373
- await new Promise(resolve => setTimeout(resolve, 300));
374
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
375
- }
376
-
377
- task.steps[i].status = 'completed';
378
- task.progress = Math.round(((i + 1) / task.steps.length) * 100);
379
-
380
- broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
381
- broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
382
- broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
383
- }
384
- result = '✅ 工作流执行完成';
385
- broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
386
- }
387
- break;
388
-
389
318
  default:
390
319
  result = '未知任务类型';
391
320
  }
@@ -792,7 +721,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
792
721
  const { getMinimax } = await import('../constraints/index.js');
793
722
  const llm = getMinimax();
794
723
  // v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
795
- 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`;
796
725
  // v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
797
726
  let dirHint = '';
798
727
  const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
@@ -813,7 +742,10 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
813
742
  }
814
743
  dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
815
744
  }
816
- 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`;
817
749
  let fullResponse = '';
818
750
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
819
751
  let lastFlushAt = 0;
@@ -1697,6 +1629,32 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1697
1629
  broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
1698
1630
  broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
1699
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 */ }
1700
1658
  } else if (event.type === 'error') {
1701
1659
  broadcast({ type: 'error', content: event.content }, channelId);
1702
1660
  }
@@ -1708,7 +1666,7 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1708
1666
  let contextHint = '';
1709
1667
  if (realChannelDid) contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
1710
1668
  // v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
1711
- contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "主人" 即可.\n`;
1669
+ contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
1712
1670
  if (boundWalletAddress) {
1713
1671
  contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
1714
1672
  }
@@ -1848,7 +1806,13 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1848
1806
 
1849
1807
  if (contextHint) contextHint += '\n';
1850
1808
  try {
1851
- fullResponse = await agent.promptStream(contextHint + text, streamCallback, runState.abortController?.signal, channelId);
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);
1852
1816
  } catch (err: any) {
1853
1817
  // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
1854
1818
  if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
@@ -1975,12 +1939,17 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1975
1939
  running: boolean;
1976
1940
  queue: PendingMessage[];
1977
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 };
1978
1947
  }
1979
1948
  const channelRunState: Map<string, ChannelRunState> = new Map();
1980
1949
  function getOrCreateRunState(channelId: string): ChannelRunState {
1981
1950
  let s = channelRunState.get(channelId);
1982
1951
  if (!s) {
1983
- s = { running: false, queue: [], abortController: null };
1952
+ s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
1984
1953
  channelRunState.set(channelId, s);
1985
1954
  }
1986
1955
  return s;
@@ -2719,14 +2688,27 @@ app.get('/channels', async (_req, res) => {
2719
2688
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
2720
2689
  } else if (event.type === 'status' || event.type === 'tool') {
2721
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);
2722
2702
  } else if (event.type === 'error') {
2723
2703
  broadcast({ type: 'error', content: event.content }, channelId);
2724
2704
  }
2725
2705
  };
2726
2706
 
2727
2707
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2708
+ // 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
2728
2709
  const regenHint = await buildJudgmentHint(channel, channelId);
2729
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback, undefined, channelId);
2710
+ const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
2711
+ fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
2730
2712
 
2731
2713
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2732
2714
 
@@ -2914,46 +2896,6 @@ app.get('/channels', async (_req, res) => {
2914
2896
  }
2915
2897
  });
2916
2898
 
2917
- // 创建并执行工作流
2918
- app.post('/api/workflow', async (req, res) => {
2919
- try {
2920
- const { channelId, title, steps } = req.body;
2921
- if (!channelId || !steps || !Array.isArray(steps)) {
2922
- return res.status(400).json({ error: 'channelId and steps required' });
2923
- }
2924
-
2925
- const tasks = await loadTaskQueue();
2926
- const task: Task = {
2927
- id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
2928
- type: 'workflow',
2929
- title: title || '工作流',
2930
- description: `包含 ${steps.length} 个步骤的工作流`,
2931
- status: 'pending',
2932
- progress: 0,
2933
- createdAt: new Date().toISOString(),
2934
- updatedAt: new Date().toISOString(),
2935
- steps: steps.map((s: string, i: number) => ({
2936
- id: `step_${i}`,
2937
- name: s,
2938
- status: 'pending'
2939
- })),
2940
- currentStep: 0
2941
- };
2942
-
2943
- tasks.push(task);
2944
- await saveTaskQueue(tasks);
2945
-
2946
- // 自动开始执行
2947
- if (!isExecutingTask) {
2948
- executeTask(task, channelId);
2949
- }
2950
-
2951
- res.json({ ok: true, task });
2952
- } catch (err: any) {
2953
- res.status(500).json({ error: err.message });
2954
- }
2955
- });
2956
-
2957
2899
  // ==================== LLM 配置 API ====================
2958
2900
 
2959
2901
  // 获取所有 LLM 配置
@@ -4178,7 +4120,7 @@ app.get('/channels', async (_req, res) => {
4178
4120
  }
4179
4121
  });
4180
4122
 
4181
- // 主人审阅: 批准 draft
4123
+ // 用户审阅: 批准 draft
4182
4124
  app.post('/api/chat/approve', async (req, res) => {
4183
4125
  try {
4184
4126
  const { messageId, peerDID, finalText } = req.body || {};
@@ -4191,7 +4133,7 @@ app.get('/channels', async (_req, res) => {
4191
4133
  }
4192
4134
  });
4193
4135
 
4194
- // 主人审阅: 丢弃 draft
4136
+ // 用户审阅: 丢弃 draft
4195
4137
  app.post('/api/chat/dismiss', async (req, res) => {
4196
4138
  try {
4197
4139
  const { messageId, peerDID } = req.body || {};
@@ -4871,6 +4813,41 @@ app.get('/channels', async (_req, res) => {
4871
4813
  }
4872
4814
  });
4873
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
+
4874
4851
  // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4875
4852
  // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4876
4853
  // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
@@ -5353,26 +5330,196 @@ app.get('/channels', async (_req, res) => {
5353
5330
  res.json(getEventHistory());
5354
5331
  });
5355
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
+
5356
5492
  // 健康检查错误数 ≥ 2 -> 触发自改信号
5493
+ // 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
5357
5494
  if (healthMonitor) {
5358
- healthMonitor.startPeriodicCheck(60000, (status: any) => {
5359
- const errorCount = Object.values(status.checks as Record<string, { status: string }>)
5360
- .filter((c) => c.status === 'error').length;
5361
- if (errorCount >= 2) {
5362
- import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
5363
- const failedKeys = Object.entries(status.checks as Record<string, { status: string }>)
5364
- .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
5365
- reportSelfImproveEvent({
5366
- kind: 'silent-timeout',
5367
- 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
+ });
5368
5507
  });
5369
- });
5370
- }
5371
- });
5508
+ }
5509
+ });
5510
+ } else {
5511
+ // 用户模式: 只跑监控不打自改信号, 心跳仍工作
5512
+ healthMonitor.startPeriodicCheck(60000);
5513
+ console.log('[24h] Health monitor periodic check (no self-improve)');
5514
+ }
5372
5515
  }
5373
5516
 
5374
- // 安装自改总线 -> SSE 桥
5375
- 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
+ }
5376
5523
 
5377
5524
  // 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
5378
5525
  return new Promise<{ app: express.Express; server: ReturnType<typeof createServer>; port: number }>((resolve, reject) => {
@@ -5394,11 +5541,24 @@ app.get('/channels', async (_req, res) => {
5394
5541
  console.log('服务器已监听');
5395
5542
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
5396
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 后重建.
5397
5547
  setInterval(() => {
5398
5548
  for (const client of sseClients) {
5399
- 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
+ }
5400
5555
  }
5401
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
+ });
5402
5562
  resolve({ app, server: currentServer, port: currentPort });
5403
5563
  });
5404
5564
  };
@@ -5568,4 +5728,4 @@ export async function openBrowser(url: string) {
5568
5728
  console.error('打开浏览器失败:', err.message);
5569
5729
  }
5570
5730
  });
5571
- }
5731
+ }