@bolloon/bolloon-agent 0.2.11 → 0.2.13

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 (284) hide show
  1. package/README.md +495 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon-daemon.sh +207 -0
  4. package/bin/bolloon.cjs +0 -0
  5. package/bin/ipfs +0 -0
  6. package/dist/agents/agent-manifest-protocol.js +52 -0
  7. package/dist/agents/chat-segmenter.js +298 -0
  8. package/dist/agents/constraint-layer.js +19 -19
  9. package/dist/agents/judgment-protocol.js +479 -0
  10. package/dist/agents/parse-tool-call.js +304 -0
  11. package/dist/agents/peer-manifest-loader.js +210 -0
  12. package/dist/agents/pi-sdk-session-factory.js +110 -0
  13. package/dist/agents/pi-sdk-session-manager.js +297 -0
  14. package/dist/agents/pi-sdk-tools.js +1288 -0
  15. package/dist/agents/pi-sdk-types.js +13 -0
  16. package/dist/agents/pi-sdk.js +553 -1306
  17. package/dist/agents/react-loop.js +120 -0
  18. package/dist/agents/session-store.js +171 -0
  19. package/dist/agents/shell-guard.js +2 -2
  20. package/dist/agents/tool-registry.js +136 -0
  21. package/dist/agents/workflow-pivot-loop.js +200 -22
  22. package/dist/bollharness/src/scripts/context_router.js +7 -0
  23. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  24. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  25. package/dist/bollharness-integration/context-router.js +292 -292
  26. package/dist/bollharness-integration/gate-state-machine.js +26 -13
  27. package/dist/bollharness-integration/integration.js +71 -0
  28. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  29. package/dist/bollharness-integration/skill-adapter.js +57 -127
  30. package/dist/bootstrap/chat-archiver.js +276 -0
  31. package/dist/bootstrap/context-collector.js +6 -3
  32. package/dist/bootstrap/event-log.js +160 -0
  33. package/dist/bootstrap/lifecycle-hooks.js +15 -1
  34. package/dist/bootstrap/memory-compressor.js +170 -0
  35. package/dist/bootstrap/persona-loader.js +94 -0
  36. package/dist/bootstrap/project-state.js +159 -0
  37. package/dist/bootstrap/remote-mirror.js +97 -0
  38. package/dist/bootstrap/session-window.js +121 -0
  39. package/dist/bootstrap/vector-index.js +248 -0
  40. package/dist/cli/loading-tui.js +40 -0
  41. package/dist/cli-entry.js +41 -37
  42. package/dist/context-compaction/auto-compact.js +7 -7
  43. package/dist/electron/config.js +21 -0
  44. package/dist/electron/config.js.map +1 -0
  45. package/dist/electron/dialogs.js +108 -0
  46. package/dist/electron/dialogs.js.map +1 -0
  47. package/dist/electron/first-run.js +170 -0
  48. package/dist/electron/first-run.js.map +1 -0
  49. package/dist/electron/ipc.js +20 -0
  50. package/dist/electron/ipc.js.map +1 -0
  51. package/dist/electron/logger.js +114 -0
  52. package/dist/electron/logger.js.map +1 -0
  53. package/dist/electron/main.js +63 -0
  54. package/dist/electron/main.js.map +1 -0
  55. package/dist/electron/menu.js +145 -0
  56. package/dist/electron/menu.js.map +1 -0
  57. package/dist/electron/paths.js +75 -0
  58. package/dist/electron/paths.js.map +1 -0
  59. package/dist/electron/server.js +119 -0
  60. package/dist/electron/server.js.map +1 -0
  61. package/dist/electron/tray.js +111 -0
  62. package/dist/electron/tray.js.map +1 -0
  63. package/dist/electron/window.js +111 -0
  64. package/dist/electron/window.js.map +1 -0
  65. package/dist/electron-preload.js +26 -6
  66. package/dist/electron-preload.js.map +1 -1
  67. package/dist/electron.js +6 -168
  68. package/dist/electron.js.map +1 -1
  69. package/dist/index.js +323 -266
  70. package/dist/llm/config-store.js +24 -8
  71. package/dist/llm/llm-judgment-client.js +102 -102
  72. package/dist/llm/pi-ai.js +137 -85
  73. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  74. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  75. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  76. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  77. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  78. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  79. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  80. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  81. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  82. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  83. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  84. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  85. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  86. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  87. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  88. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  89. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  90. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  91. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  92. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  93. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  94. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  95. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  96. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  97. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  98. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  99. package/dist/llm/tool-manifest/bash.js +3 -3
  100. package/dist/llm/tool-manifest/create_file.js +4 -4
  101. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  102. package/dist/llm/tool-manifest/image_search.js +2 -2
  103. package/dist/llm/tool-manifest/mcp.js +2 -2
  104. package/dist/llm/tool-manifest/message_compose.js +3 -3
  105. package/dist/llm/tool-manifest/places.js +2 -2
  106. package/dist/llm/tool-manifest/present_files.js +1 -1
  107. package/dist/llm/tool-manifest/recipe.js +2 -2
  108. package/dist/llm/tool-manifest/str_replace.js +5 -5
  109. package/dist/llm/tool-manifest/view.js +3 -3
  110. package/dist/llm/tool-manifest/weather.js +4 -4
  111. package/dist/llm/tool-manifest/web.js +4 -4
  112. package/dist/network/local-inbox-bus.js +73 -0
  113. package/dist/network/p2p-outbox.js +161 -0
  114. package/dist/network/peer-fs.js +420 -0
  115. package/dist/network/peer-resource-bridge.js +216 -0
  116. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  117. package/dist/pi-ecosystem-judgment/cleanup.js +126 -0
  118. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  119. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  120. package/dist/pi-ecosystem-judgment/human-value-store.js +45 -11
  121. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  122. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  123. package/dist/security/context-router-tool.js +15 -15
  124. package/dist/security/tool-gate.js +11 -0
  125. package/dist/web/api-config.html +520 -520
  126. package/dist/web/client-loop-status.js +220 -0
  127. package/dist/web/client.js +1677 -244
  128. package/dist/web/client.js.map +7 -0
  129. package/dist/web/components/wallet-viem.mjs +118 -118
  130. package/dist/web/index.html +375 -374
  131. package/dist/web/input-validator.js +103 -0
  132. package/dist/web/manifest.json +20 -20
  133. package/dist/web/routes-judgments.js +738 -0
  134. package/dist/web/routes-llm-config.js +279 -0
  135. package/dist/web/routes-tasks.js +222 -0
  136. package/dist/web/server-sse.js +121 -0
  137. package/dist/web/server-storage.js +154 -0
  138. package/dist/web/server-types.js +15 -0
  139. package/dist/web/server-v3-p2p.js +224 -0
  140. package/dist/web/server.js +1070 -1307
  141. package/dist/web/style.css +4742 -4737
  142. package/dist/web/util/safe-name.js +32 -0
  143. package/package.json +162 -160
  144. package/scripts/build-cli.js +215 -215
  145. package/scripts/build-web.ts +130 -125
  146. package/scripts/postinstall.js +152 -152
  147. package/bin/bolloon.js +0 -157
  148. package/dist/constraint-runtime/src/_archive_helper.js +0 -9
  149. package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
  150. package/dist/constraint-runtime/src/agent/index.js +0 -1
  151. package/dist/constraint-runtime/src/assistant/index.js +0 -12
  152. package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
  153. package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
  154. package/dist/constraint-runtime/src/bridge/index.js +0 -12
  155. package/dist/constraint-runtime/src/buddy/index.js +0 -12
  156. package/dist/constraint-runtime/src/cli/index.js +0 -12
  157. package/dist/constraint-runtime/src/command_graph.js +0 -10
  158. package/dist/constraint-runtime/src/commands.js +0 -60
  159. package/dist/constraint-runtime/src/components/index.js +0 -12
  160. package/dist/constraint-runtime/src/constants/index.js +0 -12
  161. package/dist/constraint-runtime/src/constraint/budget.js +0 -22
  162. package/dist/constraint-runtime/src/constraint/index.js +0 -2
  163. package/dist/constraint-runtime/src/constraint/permission.js +0 -20
  164. package/dist/constraint-runtime/src/context.js +0 -30
  165. package/dist/constraint-runtime/src/coordinator/index.js +0 -12
  166. package/dist/constraint-runtime/src/cost_hook.js +0 -4
  167. package/dist/constraint-runtime/src/cost_tracker.js +0 -8
  168. package/dist/constraint-runtime/src/deferred_init.js +0 -10
  169. package/dist/constraint-runtime/src/direct_modes.js +0 -6
  170. package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
  171. package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
  172. package/dist/constraint-runtime/src/execution_registry.js +0 -44
  173. package/dist/constraint-runtime/src/history.js +0 -9
  174. package/dist/constraint-runtime/src/hooks/index.js +0 -12
  175. package/dist/constraint-runtime/src/index.js +0 -26
  176. package/dist/constraint-runtime/src/ink.js +0 -4
  177. package/dist/constraint-runtime/src/keybindings/index.js +0 -12
  178. package/dist/constraint-runtime/src/memdir/index.js +0 -12
  179. package/dist/constraint-runtime/src/migrations/index.js +0 -12
  180. package/dist/constraint-runtime/src/models.js +0 -1
  181. package/dist/constraint-runtime/src/moreright/index.js +0 -12
  182. package/dist/constraint-runtime/src/native_ts/index.js +0 -12
  183. package/dist/constraint-runtime/src/output_styles/index.js +0 -12
  184. package/dist/constraint-runtime/src/parity_audit.js +0 -12
  185. package/dist/constraint-runtime/src/plugins/index.js +0 -12
  186. package/dist/constraint-runtime/src/port_manifest.js +0 -11
  187. package/dist/constraint-runtime/src/prefetch.js +0 -9
  188. package/dist/constraint-runtime/src/query.js +0 -1
  189. package/dist/constraint-runtime/src/remote/index.js +0 -12
  190. package/dist/constraint-runtime/src/remote_runtime.js +0 -9
  191. package/dist/constraint-runtime/src/runtime/index.js +0 -1
  192. package/dist/constraint-runtime/src/runtime/session.js +0 -35
  193. package/dist/constraint-runtime/src/schemas/index.js +0 -12
  194. package/dist/constraint-runtime/src/screens/index.js +0 -12
  195. package/dist/constraint-runtime/src/server/index.js +0 -12
  196. package/dist/constraint-runtime/src/services/index.js +0 -12
  197. package/dist/constraint-runtime/src/session_store.js +0 -22
  198. package/dist/constraint-runtime/src/setup.js +0 -30
  199. package/dist/constraint-runtime/src/skills/index.js +0 -1
  200. package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
  201. package/dist/constraint-runtime/src/state/index.js +0 -12
  202. package/dist/constraint-runtime/src/system_init.js +0 -20
  203. package/dist/constraint-runtime/src/thinking/engine.js +0 -42
  204. package/dist/constraint-runtime/src/thinking/index.js +0 -1
  205. package/dist/constraint-runtime/src/tool_pool.js +0 -8
  206. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
  207. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
  208. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
  209. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
  210. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
  211. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
  212. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
  213. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
  214. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
  215. package/dist/constraint-runtime/src/tools.js +0 -80
  216. package/dist/constraint-runtime/src/transcript.js +0 -19
  217. package/dist/constraint-runtime/src/types/index.js +0 -12
  218. package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
  219. package/dist/constraint-runtime/src/utils/index.js +0 -12
  220. package/dist/constraint-runtime/src/vim/index.js +0 -12
  221. package/dist/constraint-runtime/src/voice/index.js +0 -12
  222. package/dist/constraint-runtime/tests/agent.test.js +0 -16
  223. package/dist/constraint-runtime/tests/constraint.test.js +0 -41
  224. package/dist/constraint-runtime/tests/skill.test.js +0 -19
  225. package/dist/constraint-runtime/tests/thinking.test.js +0 -22
  226. package/dist/pi-ecosystem-colony/index.js +0 -365
  227. package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
  228. package/dist/social/ant-colony/PheromoneEngine.js +0 -227
  229. package/dist/social/ant-colony/index.js +0 -6
  230. package/dist/social/ant-colony/types.js +0 -24
  231. package/dist/test/ai-judgment-test.js +0 -80
  232. package/dist/test/bollharness-integration.test.js +0 -318
  233. package/dist/test/channel-agent-multi-dialogue.js +0 -205
  234. package/dist/test/channel-heartbeat-agent-test.js +0 -201
  235. package/dist/test/constraint-layer.test.js +0 -164
  236. package/dist/test/diap-identity-test.js +0 -172
  237. package/dist/test/diap-quick-test.js +0 -62
  238. package/dist/test/global-shared-context.test.js +0 -315
  239. package/dist/test/harness-judgment-injection.test.js +0 -246
  240. package/dist/test/harness-workflow-integrator-test.js +0 -228
  241. package/dist/test/human-value-store.test.js +0 -243
  242. package/dist/test/hybrid-integration-test.js +0 -118
  243. package/dist/test/hybrid-messenger-verify.js +0 -55
  244. package/dist/test/iroh-bistream-debug.js +0 -38
  245. package/dist/test/iroh-communication.test.js +0 -66
  246. package/dist/test/iroh-debug-test.js +0 -57
  247. package/dist/test/iroh-diap-test.js +0 -71
  248. package/dist/test/iroh-direct-connect.js +0 -55
  249. package/dist/test/iroh-e2e-fixed.js +0 -89
  250. package/dist/test/iroh-e2e-same-process.js +0 -63
  251. package/dist/test/iroh-e2e.js +0 -66
  252. package/dist/test/iroh-final-e2e.js +0 -72
  253. package/dist/test/iroh-relay-test.js +0 -37
  254. package/dist/test/iroh-simple-test.js +0 -41
  255. package/dist/test/iroh-transport-verify.js +0 -54
  256. package/dist/test/iroh-transport.test.js +0 -37
  257. package/dist/test/iroh-two-nodes.js +0 -70
  258. package/dist/test/iroh-verify.js +0 -44
  259. package/dist/test/judgment-decision.test.js +0 -219
  260. package/dist/test/llm-judgment-integration.test.js +0 -220
  261. package/dist/test/p2p-agent-complex-dialogue.js +0 -385
  262. package/dist/test/p2p-agent-dialogue.js +0 -341
  263. package/dist/test/p2p-agent-full-bidirectional.js +0 -510
  264. package/dist/test/p2p-agent-harness-flow.js +0 -437
  265. package/dist/test/p2p-agent-harness-single.js +0 -143
  266. package/dist/test/p2p-ai-dialogue-test.js +0 -318
  267. package/dist/test/p2p-cid-connect-test.js +0 -195
  268. package/dist/test/p2p-connect-receiver.js +0 -69
  269. package/dist/test/p2p-doc-transfer.js +0 -110
  270. package/dist/test/p2p-identity-page-test.js +0 -77
  271. package/dist/test/p2p-iroh-test.js +0 -171
  272. package/dist/test/p2p-minimal-test.js +0 -241
  273. package/dist/test/p2p-node-1.js +0 -148
  274. package/dist/test/p2p-node-2.js +0 -148
  275. package/dist/test/p2p-server.js +0 -281
  276. package/dist/test/p2p-two-nodes-test.js +0 -438
  277. package/dist/test/pi-sdk.test.js +0 -44
  278. package/dist/test/set-persona.js +0 -40
  279. package/dist/test/simple.test.js +0 -9
  280. package/dist/test/storage-integration.test.js +0 -150
  281. package/dist/test/subagent-manager.test.js +0 -276
  282. package/dist/test/test-gate-flow.test.js +0 -81
  283. package/dist/test/workflow-engine.test.js +0 -87
  284. package/dist/test/workflow-pivot-loop.test.js +0 -246
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Persona Documents Loader — ~/.bolloon/persona/<agentId>/*.md
3
+ *
4
+ * 失败静默: 文件不存在 → 字段 = '', 不抛错
5
+ * 安全: agentId sanitize (防路径穿越)
6
+ * 6 段输出顺序: identity → soul → project → user → agent → wiki
7
+ */
8
+ import * as fs from 'fs/promises';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ const FILE_KEYS = ['soul', 'identity', 'project', 'user', 'agent', 'wiki'];
12
+ /**
13
+ * 安全转 agentId: 只保留 [a-zA-Z0-9_-], 限长 64
14
+ * 防止路径穿越 ('/', '..', '\\\\' 都变 '_')
15
+ */
16
+ export function sanitizeAgentId(id) {
17
+ return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
18
+ }
19
+ /**
20
+ * 读 ~/.bolloon/persona/<sanitizedAgentId>/{6 files}
21
+ * 文件不存在 → 该字段 = ''
22
+ */
23
+ export async function loadPersonaDocs(agentId, home) {
24
+ const safeId = sanitizeAgentId(agentId);
25
+ const root = home || os.homedir();
26
+ const baseDir = path.join(root, '.bolloon', 'persona', safeId);
27
+ const docs = {
28
+ agentId: safeId,
29
+ soul: '',
30
+ identity: '',
31
+ project: '',
32
+ user: '',
33
+ agent: '',
34
+ wiki: '',
35
+ };
36
+ await Promise.all(FILE_KEYS.map(async (key) => {
37
+ const file = path.join(baseDir, `${key}.md`);
38
+ try {
39
+ const content = await fs.readFile(file, 'utf-8');
40
+ docs[key] = content;
41
+ }
42
+ catch {
43
+ docs[key] = '';
44
+ }
45
+ }));
46
+ return docs;
47
+ }
48
+ const DEFAULT_MAX_CHARS = 4000;
49
+ const SECTION_LABELS = {
50
+ identity: 'Identity',
51
+ soul: 'Soul',
52
+ project: 'Project',
53
+ user: 'User',
54
+ agent: 'Agent',
55
+ wiki: 'Wiki',
56
+ };
57
+ const OUTPUT_ORDER = ['identity', 'soul', 'project', 'user', 'agent', 'wiki'];
58
+ /**
59
+ * 把 PersonaDocs 格式化成 markdown 段, 拼到 system prompt 头部.
60
+ *
61
+ * 超 maxChars 时按比例截断: 每个字段都保留头部,
62
+ * 保证 6 段标识都出现, 不砍段.
63
+ */
64
+ export function formatPersonaForSystemPrompt(docs, maxChars) {
65
+ const cap = maxChars ?? DEFAULT_MAX_CHARS;
66
+ // 段: 标识 + 字段值 (空字段跳过)
67
+ const sections = [];
68
+ for (const key of OUTPUT_ORDER) {
69
+ const v = docs[key];
70
+ if (v && v.length > 0) {
71
+ sections.push({ key, text: `## ${SECTION_LABELS[key]}\n${v}` });
72
+ }
73
+ }
74
+ if (sections.length === 0)
75
+ return '';
76
+ // 预算每段 (去掉 ## 标识和换行的固定开销)
77
+ const header = `# Persona (agentId=${docs.agentId})\n\n`;
78
+ const fixedOverhead = header.length + (sections.length - 1) * 2;
79
+ const perSectionBudget = Math.max(50, Math.floor((cap - fixedOverhead) / sections.length));
80
+ const parts = [header.trim()];
81
+ for (const sec of sections) {
82
+ let body = sec.text;
83
+ if (body.length > perSectionBudget) {
84
+ body = body.substring(0, perSectionBudget) + '\n... (截断)';
85
+ }
86
+ parts.push(body);
87
+ }
88
+ let result = parts.join('\n\n');
89
+ if (result.length > cap) {
90
+ const truncateMarker = '\n... (截断)';
91
+ result = result.substring(0, Math.max(0, cap - truncateMarker.length)) + truncateMarker;
92
+ }
93
+ return result;
94
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * project-state.ts — 项目当前状态 (Layer 3)
3
+ *
4
+ * 2026-07-07 新增. 解决:
5
+ * - judgment 是"决策规则", 项目当前状态/目标/约束/待办没有结构化记录
6
+ * - 用户开启新 session 时无法 auto-inject "上次我们做到哪"
7
+ * - 远端/本地协作时, 双方项目状态不同步
8
+ *
9
+ * 设计:
10
+ * - state.json: ~/.bolloon/project/<safe-channelId>/state.json
11
+ * - 字段: goal / constraints / todos / done / updatedAt / updatedBy / version
12
+ * - 写盘走 append-only diff (每次写盘生成新版本号, 不覆盖历史快照)
13
+ * - 失败静默不阻塞主对话
14
+ *
15
+ * 触发:
16
+ * - server.ts: LLM 回答完后, prompt 注入当前 state (LLM 不会自动改, 只读取)
17
+ * - 用户可手动调 PATCH /api/state/:channelId 改写
18
+ * - future: 加 LLM 自动建议更新 (UI 弹 confirm)
19
+ */
20
+ import * as fs from 'fs/promises';
21
+ import * as path from 'path';
22
+ import * as os from 'os';
23
+ // ============== 路径 ==============
24
+ function sanitize(s) {
25
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
26
+ }
27
+ function getStateDir(channelId, home) {
28
+ return path.join(home || os.homedir(), '.bolloon', 'project', sanitize(channelId));
29
+ }
30
+ export function getStatePath(channelId, home) {
31
+ return path.join(getStateDir(channelId, home), 'state.json');
32
+ }
33
+ export function getStateBackupPath(channelId, home) {
34
+ return path.join(getStateDir(channelId, home), 'state.backup.jsonl');
35
+ }
36
+ // ============== 默认 ==============
37
+ export function defaultState(channelId) {
38
+ return {
39
+ goal: '',
40
+ constraints: [],
41
+ todos: [],
42
+ done: [],
43
+ updatedAt: new Date(0).toISOString(),
44
+ updatedBy: 'system',
45
+ version: 0,
46
+ };
47
+ }
48
+ // ============== 读取 ==============
49
+ /**
50
+ * 读 state.json. 文件不存在 → 返回默认空 state.
51
+ * 损坏 → 返回默认 state + console.warn, 不抛错.
52
+ */
53
+ export async function readState(opts) {
54
+ const filePath = getStatePath(opts.channelId, opts.home);
55
+ try {
56
+ const raw = await fs.readFile(filePath, 'utf-8');
57
+ const parsed = JSON.parse(raw);
58
+ if (typeof parsed !== 'object' || !parsed)
59
+ return defaultState(opts.channelId);
60
+ return {
61
+ goal: typeof parsed.goal === 'string' ? parsed.goal : '',
62
+ constraints: Array.isArray(parsed.constraints) ? parsed.constraints.filter((x) => typeof x === 'string') : [],
63
+ todos: Array.isArray(parsed.todos) ? parsed.todos.filter((x) => typeof x === 'string') : [],
64
+ done: Array.isArray(parsed.done) ? parsed.done.filter((x) => typeof x === 'string') : [],
65
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
66
+ updatedBy: typeof parsed.updatedBy === 'string' ? parsed.updatedBy : 'system',
67
+ version: typeof parsed.version === 'number' ? parsed.version : 0,
68
+ };
69
+ }
70
+ catch (e) {
71
+ if (e?.code !== 'ENOENT')
72
+ console.warn(`[project-state] ${opts.channelId} 损坏: ${e?.message || e}`);
73
+ return defaultState(opts.channelId);
74
+ }
75
+ }
76
+ // ============== 写 ==============
77
+ /**
78
+ * 完整覆盖写入 state.json. 自动:
79
+ * 1. 备份当前 state 到 state.backup.jsonl (append-only)
80
+ * 2. 设置 updatedAt + version +1
81
+ * 3. done 数组截断到 20 条
82
+ */
83
+ export async function writeState(opts) {
84
+ const filePath = getStatePath(opts.channelId, opts.home);
85
+ const backupPath = getStateBackupPath(opts.channelId, opts.home);
86
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
87
+ // 备份当前 (append-only)
88
+ const cur = await readState(opts);
89
+ if (cur.version > 0) {
90
+ const line = JSON.stringify({ ...cur, backupAt: new Date().toISOString() }) + '\n';
91
+ try {
92
+ await fs.appendFile(backupPath, line, 'utf-8');
93
+ }
94
+ catch (e) {
95
+ console.warn(`[project-state] backup 失败: ${e?.message || e}`);
96
+ }
97
+ }
98
+ const next = {
99
+ goal: opts.state.goal,
100
+ constraints: opts.state.constraints || [],
101
+ todos: opts.state.todos || [],
102
+ done: (opts.state.done || []).slice(-20),
103
+ updatedAt: new Date().toISOString(),
104
+ updatedBy: opts.state.updatedBy || 'system',
105
+ version: cur.version + 1,
106
+ };
107
+ await fs.writeFile(filePath, JSON.stringify(next, null, 2), 'utf-8');
108
+ return { path: filePath, version: next.version };
109
+ }
110
+ /**
111
+ * 部分字段 patch. 合并现有 state + patch.
112
+ */
113
+ export async function mergeState(opts) {
114
+ const cur = await readState(opts);
115
+ const merged = {
116
+ goal: opts.patch.goal ?? cur.goal,
117
+ constraints: opts.patch.constraints ?? cur.constraints,
118
+ todos: opts.patch.todos ?? cur.todos,
119
+ done: opts.patch.done ?? cur.done,
120
+ updatedBy: opts.updatedBy,
121
+ };
122
+ const r = await writeState({ channelId: opts.channelId, state: merged, home: opts.home });
123
+ return { ...merged, updatedAt: new Date().toISOString(), version: r.version };
124
+ }
125
+ // ============== 简易操作 ==============
126
+ /** 加一条 todo */
127
+ export async function addTodo(opts) {
128
+ const cur = await readState(opts);
129
+ const todos = [...cur.todos, opts.text];
130
+ return mergeState({ channelId: opts.channelId, patch: { todos }, updatedBy: 'user', home: opts.home });
131
+ }
132
+ /** 标记 todo 完成 (移到 done) */
133
+ export async function markTodoDone(opts) {
134
+ const cur = await readState(opts);
135
+ const todos = cur.todos.filter(t => t !== opts.text);
136
+ const done = [opts.text, ...cur.done].slice(0, 20);
137
+ return mergeState({ channelId: opts.channelId, patch: { todos, done }, updatedBy: 'user', home: opts.home });
138
+ }
139
+ /** 清空所有 todos (批量完成) */
140
+ export async function clearTodos(opts) {
141
+ return mergeState({ channelId: opts.channelId, patch: { todos: [] }, updatedBy: 'user', home: opts.home });
142
+ }
143
+ /** 格式化 state 为 LLM prompt 注入文本 (单段落, ≤ 600 字) */
144
+ export function formatStateForPrompt(state) {
145
+ if (state.version === 0 && !state.goal)
146
+ return '';
147
+ const lines = [];
148
+ if (state.goal)
149
+ lines.push(`目标: ${state.goal}`);
150
+ if (state.constraints.length > 0)
151
+ lines.push(`约束: ${state.constraints.slice(0, 5).join('; ')}`);
152
+ if (state.todos.length > 0)
153
+ lines.push(`待办 (${state.todos.length}): ${state.todos.slice(0, 5).join('; ')}`);
154
+ if (state.done.length > 0)
155
+ lines.push(`已完成 (最近): ${state.done.slice(0, 3).join('; ')}`);
156
+ if (lines.length === 0)
157
+ return '';
158
+ return `## 项目当前状态 (v${state.version}, ${state.updatedBy} @ ${state.updatedAt.slice(0, 16)})\n${lines.join('\n')}`;
159
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * remote-mirror.ts — 远端 channel 历史镜像 (B 端本地副本)
3
+ *
4
+ * 2026-07-07 新增. 解决:
5
+ * - 远端 channel 历史在 B 端零持久化, A 端 session 删/损坏即丢
6
+ * - A 端下线/不再分享该 channel, B 端再拉就 15s timeout
7
+ * - 切回远端 channel 每次都重新 RPC, 无 L0 缓存
8
+ *
9
+ * 设计:
10
+ * - B 端收到 agent.history.get.reply 后, 立刻镜像到
11
+ * ~/.bolloon/peers/<pk>/sessions/<channelId>.json (覆盖式 atomic)
12
+ * - 同时维护窗口 <channelId>.window.json (复用 session-window LRU)
13
+ * - loadRemoteHistory 优先读本地镜像, 文件不存在/陈旧才走 RPC
14
+ *
15
+ * 触发:
16
+ * - server.ts: agent.history.get.reply handler 写完 → mirrorRemoteHistory
17
+ * - client.ts: openRemoteChannelChat → loadRemoteHistory 优先读镜像
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as path from 'path';
21
+ import * as os from 'os';
22
+ import { saveWindow as saveSessionWindow } from './session-window.js';
23
+ // ============== 路径 ==============
24
+ function sanitize(s) {
25
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
26
+ }
27
+ /** ~/.bolloon/peers/<pk>/sessions/<safe-channelId>.json */
28
+ export function getRemoteMirrorPath(targetPublicKey, channelId, home) {
29
+ const root = path.join(home || os.homedir(), '.bolloon', 'peers', sanitize(targetPublicKey), 'sessions');
30
+ return path.join(root, `${sanitize(channelId)}.json`);
31
+ }
32
+ /** ~/.bolloon/peers/<pk>/sessions/<safe-channelId>.window.json — 复用 session-window 路径规则 */
33
+ export function getRemoteMirrorWindowPath(targetPublicKey, channelId, home) {
34
+ const root = path.join(home || os.homedir(), '.bolloon', 'peers', sanitize(targetPublicKey), 'sessions');
35
+ return path.join(root, `${sanitize(channelId)}.window.json`);
36
+ }
37
+ // ============== 写入 ==============
38
+ /**
39
+ * 把 A 端的 history 镜像到 B 端本地. atomic (单文件 writeFile, 中途崩溃顶多旧版本留下).
40
+ * 失败静默不阻塞 RPC reply 返回.
41
+ */
42
+ export async function mirrorRemoteHistory(opts) {
43
+ try {
44
+ const home = opts.home || os.homedir();
45
+ const mirrorPath = getRemoteMirrorPath(opts.targetPublicKey, opts.channelId, home);
46
+ const windowPath = getRemoteMirrorWindowPath(opts.targetPublicKey, opts.channelId, home);
47
+ await fs.mkdir(path.dirname(mirrorPath), { recursive: true });
48
+ // 主体镜像
49
+ const payload = {
50
+ channelId: opts.channelId,
51
+ channelName: opts.channelName,
52
+ fromPublicKey: opts.targetPublicKey,
53
+ messages: opts.messages,
54
+ lastUpdated: opts.lastUpdated || new Date().toISOString(),
55
+ mirroredAt: new Date().toISOString(),
56
+ };
57
+ await fs.writeFile(mirrorPath, JSON.stringify(payload, null, 2), 'utf-8');
58
+ // 窗口联动
59
+ await saveSessionWindow(opts.channelId, `remote-${opts.targetPublicKey.slice(0, 12)}`, opts.messages, { home, windowSize: 30 });
60
+ return { ok: true, mirrorPath, windowPath };
61
+ }
62
+ catch (e) {
63
+ return { ok: false, error: e?.message || String(e) };
64
+ }
65
+ }
66
+ /**
67
+ * 读远端 channel 的本地镜像. 失败/不存在 → 返回 null.
68
+ */
69
+ export async function loadRemoteMirror(targetPublicKey, channelId, home) {
70
+ const mirrorPath = getRemoteMirrorPath(targetPublicKey, channelId, home);
71
+ try {
72
+ const raw = await fs.readFile(mirrorPath, 'utf-8');
73
+ const parsed = JSON.parse(raw);
74
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.messages))
75
+ return null;
76
+ return parsed;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * 删镜像 (远端 channel 在本地的 share 列表移除时调用).
84
+ */
85
+ export async function deleteRemoteMirror(targetPublicKey, channelId, home) {
86
+ const mirrorPath = getRemoteMirrorPath(targetPublicKey, channelId, home);
87
+ const windowPath = getRemoteMirrorWindowPath(targetPublicKey, channelId, home);
88
+ for (const p of [mirrorPath, windowPath]) {
89
+ try {
90
+ await fs.unlink(p);
91
+ }
92
+ catch (e) {
93
+ if (e?.code !== 'ENOENT')
94
+ throw e;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * session-window.ts — 显式 LRU 窗口 (Layer 0 热缓存)
3
+ *
4
+ * 2026-07-07 新增. 解决:
5
+ * - sessions/cache/<key>.json 单文件无限增长直到 50MB 拒加载
6
+ * - loadSession 返回 null 后前端 fallback "你好! 我是 Bolloon Agent" 用户以为历史没了
7
+ * - 切 channel 首屏读整文件慢
8
+ *
9
+ * 设计:
10
+ * - 独立窗口文件 <key>.window.json, 永远保持最近 N 条 (默认 30) raw messages
11
+ * - LRU 语义: saveSession 后同步调用 saveWindow, append 新消息、pop 最老的
12
+ * - loadSession fallback 链: window (秒开) → full json → summary
13
+ * - 窗口与 full json 用 lastUpdated 校验版本, 防重复显示
14
+ *
15
+ * 触发:
16
+ * - server.ts: saveSession 之后调 saveWindow
17
+ * - server.ts: loadSession 失败/超大时 fallback 到 loadWindow
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as path from 'path';
21
+ import * as os from 'os';
22
+ // ============== 路径 ==============
23
+ /** 跟 server-storage.ts:loadSession 完全一致的 key 计算 */
24
+ export function getWindowKey(channelId, sessionId) {
25
+ return sessionId ? `${channelId}:${sessionId}` : channelId;
26
+ }
27
+ /** ~/.bolloon/sessions/cache/<key>.window.json — 跟 session 文件同目录, 前缀区分 */
28
+ export function getWindowPath(channelId, sessionId, home) {
29
+ const root = path.join(home || os.homedir(), '.bolloon', 'sessions', 'cache');
30
+ const key = getWindowKey(channelId, sessionId);
31
+ // Windows 兼容: 转义 : → __
32
+ const safeKey = key.replace(/:/g, '__');
33
+ return path.join(root, `${safeKey}.window.json`);
34
+ }
35
+ // ============== 写入 ==============
36
+ /**
37
+ * 保存最近 N 条消息到窗口文件. LRU 语义.
38
+ *
39
+ * @param channelId channel id
40
+ * @param sessionId session id (没有就用 'default')
41
+ * @param messages session 完整消息列表 (任意长度, 内部取尾 N 条)
42
+ * @param opts 窗口容量配置
43
+ */
44
+ export async function saveWindow(channelId, sessionId, messages, opts = {}) {
45
+ const windowSize = opts.windowSize ?? 30;
46
+ const winPath = getWindowPath(channelId, sessionId, opts.home);
47
+ // LRU: 取尾 N 条
48
+ const windowMessages = messages.length > windowSize
49
+ ? messages.slice(-windowSize)
50
+ : messages.slice();
51
+ const win = {
52
+ channelId,
53
+ sessionId,
54
+ windowSize,
55
+ lastUpdated: new Date().toISOString(),
56
+ messages: windowMessages,
57
+ totalBehind: Math.max(0, messages.length - windowMessages.length),
58
+ };
59
+ await fs.mkdir(path.dirname(winPath), { recursive: true });
60
+ await fs.writeFile(winPath, JSON.stringify(win, null, 2), 'utf-8');
61
+ return {
62
+ windowPath: winPath,
63
+ windowSize: win.windowSize,
64
+ totalBehind: win.totalBehind,
65
+ };
66
+ }
67
+ // ============== 读取 ==============
68
+ /**
69
+ * 读窗口文件. 文件不存在 / 损坏 → 返回 null (调用方应 fallback 到 full session 或 summary).
70
+ *
71
+ * @param channelId
72
+ * @param sessionId
73
+ * @param opts.windowSize 期望窗口大小, 跟落盘不一致时按期望截断
74
+ */
75
+ export async function loadWindow(channelId, sessionId, opts = {}) {
76
+ const winPath = getWindowPath(channelId, sessionId, opts.home);
77
+ let raw;
78
+ try {
79
+ raw = await fs.readFile(winPath, 'utf-8');
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(raw);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ // 防御: 字段缺失/类型不对
92
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.messages)) {
93
+ return null;
94
+ }
95
+ // 期望容量调整
96
+ const wantSize = opts.windowSize ?? parsed.windowSize ?? 30;
97
+ if (parsed.messages.length > wantSize) {
98
+ const dropped = parsed.messages.length - wantSize;
99
+ parsed = {
100
+ ...parsed,
101
+ windowSize: wantSize,
102
+ messages: parsed.messages.slice(-wantSize),
103
+ totalBehind: parsed.totalBehind + dropped,
104
+ };
105
+ }
106
+ return parsed;
107
+ }
108
+ // ============== 删除 ==============
109
+ /**
110
+ * 删窗口文件 (跟 session 文件一起删). 文件不存在不报错.
111
+ */
112
+ export async function deleteWindow(channelId, sessionId, home) {
113
+ const winPath = getWindowPath(channelId, sessionId, home);
114
+ try {
115
+ await fs.unlink(winPath);
116
+ }
117
+ catch (e) {
118
+ if (e?.code !== 'ENOENT')
119
+ throw e;
120
+ }
121
+ }