@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,276 @@
1
+ /**
2
+ * chat-archiver.ts — 按 peer 月度归档聊天记录 + 接 memory 摘要
3
+ *
4
+ * 设计目的 (2026-07-05):
5
+ * 解决"对方不在线也能看到历史"的核心问题.
6
+ * sessions/cache/<channelId>.json 单文件无限增长, 超过 50MB 直接拒绝加载.
7
+ *
8
+ * 新方案:
9
+ * - 每次 /message 处理后, 同时写到 3 个地方:
10
+ * ① sessions/cache/<channelId>.json (原有, 最近窗口用)
11
+ * ② ~/.bolloon/peers/<pk>/chat-<YYYY-MM>.md (按 peer 月度归档)
12
+ * ③ ~/.bolloon/memory/<agentId>/peers/<pk>/<YYYY-MM>.summary.md (月度摘要)
13
+ *
14
+ * 触发时机:
15
+ * - appendChatArchive(): 每次保存远端/本地 user + ai 消息后立即调用 (高频)
16
+ * - compressMonthlyArchive(): 月底 / 90 天滚动 / 显式调用 (低频, 调 LLM 摘要)
17
+ *
18
+ * peer 维度 而不是 channel 维度: 同一个远端节点可能跨多个 channel 协作,
19
+ * 按 peer 归档才能完整还原"与对方的对话历史".
20
+ */
21
+ import * as fs from 'fs/promises';
22
+ import * as fsSync from 'fs';
23
+ import * as path from 'path';
24
+ import * as os from 'os';
25
+ import * as peerFs from '../network/peer-fs.js';
26
+ // ============== 路径 ==============
27
+ const HOME = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
28
+ export function getPeerSummaryDir(agentId, publicKey) {
29
+ const safeAgent = sanitizeId(agentId);
30
+ const safePeer = sanitizeId(publicKey);
31
+ return path.join(HOME, 'memory', safeAgent, 'peers', safePeer);
32
+ }
33
+ export function getPeerSummaryPath(agentId, publicKey, yearMonth) {
34
+ return path.join(getPeerSummaryDir(agentId, publicKey), `${yearMonth}.summary.md`);
35
+ }
36
+ export function getPeerSummaryCursorPath(agentId, publicKey, yearMonth) {
37
+ return path.join(getPeerSummaryDir(agentId, publicKey), `${yearMonth}.cursor`);
38
+ }
39
+ function sanitizeId(s) {
40
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
41
+ }
42
+ // ============== appendChatArchive — 高频, 每次 message 都调 ==============
43
+ /**
44
+ * 追加一条 chat entry 到 peer 的月度归档.
45
+ * 不依赖 LLM, 纯文件 IO; 失败静默不阻塞主对话.
46
+ */
47
+ export async function appendChatArchive(opts) {
48
+ try {
49
+ await peerFs.appendChat(opts.publicKey, opts.entry);
50
+ return { ok: true, path: peerFs.getPeerChatPath(opts.publicKey) };
51
+ }
52
+ catch (err) {
53
+ return { ok: false, error: err?.message || String(err) };
54
+ }
55
+ }
56
+ // ============== compressMonthlyArchive — 低频, 月底/显式调 ==============
57
+ /**
58
+ * 读取月度 markdown 归档, 调 LLM 摘要, append 写到 memory.
59
+ * 用 cursor 跟踪已摘要到第几行, 增量压缩.
60
+ */
61
+ async function readArchiveLines(publicKey, yearMonth) {
62
+ const file = peerFs.getPeerChatPath(publicKey, yearMonth);
63
+ try {
64
+ const raw = await fs.readFile(file, 'utf-8');
65
+ return raw.split('\n');
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ async function readSummaryCursor(cursorPath) {
72
+ try {
73
+ const raw = await fs.readFile(cursorPath, 'utf-8');
74
+ const n = parseInt(raw.trim(), 10);
75
+ return Number.isFinite(n) && n >= 0 ? n : 0;
76
+ }
77
+ catch {
78
+ return 0;
79
+ }
80
+ }
81
+ async function writeSummaryCursor(cursorPath, value) {
82
+ await fs.mkdir(path.dirname(cursorPath), { recursive: true });
83
+ await fs.writeFile(cursorPath, String(value), 'utf-8');
84
+ }
85
+ /**
86
+ * 尝试调 LLM 生成月度摘要. 失败 → 抛错, 由 caller fallback 到模板.
87
+ */
88
+ async function tryMonthlyLlm(systemPrompt, userPrompt) {
89
+ try {
90
+ const piAi = await import('../llm/pi-ai.js');
91
+ const generateText = piAi.generateText;
92
+ if (typeof generateText !== 'function')
93
+ throw new Error('generateText not exported');
94
+ const result = await generateText({
95
+ messages: [
96
+ { role: 'system', content: systemPrompt },
97
+ { role: 'user', content: userPrompt },
98
+ ],
99
+ temperature: 0.2,
100
+ maxTokens: 1000,
101
+ });
102
+ const text = result?.reply || result?.text || '';
103
+ if (typeof text !== 'string' || text.length < 20)
104
+ throw new Error('LLM returned too-short text');
105
+ return text;
106
+ }
107
+ catch (e) {
108
+ throw new Error(`LLM call failed: ${e?.message || String(e)}`);
109
+ }
110
+ }
111
+ /**
112
+ * 模板 fallback: 简单统计 user/ai 数量, 列前 5 条关键内容.
113
+ */
114
+ function templateMonthlySummary(opts) {
115
+ const user = opts.entries.filter(e => e.msgType === 'user' || e.source === 'local' || e.source === 'remote');
116
+ const ai = opts.entries.filter(e => e.msgType === 'ai' || e.source?.startsWith('ai-mention'));
117
+ const channelSet = new Set(opts.entries.map(e => e.channelName).filter(Boolean));
118
+ const sampleUser = user.slice(0, 5).map(e => ` - [${e.ts}] ${e.text.slice(0, 80)}`).join('\n');
119
+ const sampleAi = ai.slice(0, 5).map(e => ` - [${e.ts}] ${e.text.slice(0, 80)}`).join('\n');
120
+ return `# 月度对话摘要 — ${opts.yearMonth} (peer ${opts.publicKey.slice(0, 12)}…)
121
+
122
+ 时间: ${opts.timestamp}
123
+ 对话轮次: ${opts.entries.length} (user=${user.length}, ai=${ai.length})
124
+ 涉及 channel: ${Array.from(channelSet).slice(0, 10).join(', ') || '(无)'}
125
+
126
+ ## 用户关键提问
127
+ ${sampleUser || ' (无)'}
128
+
129
+ ## AI 关键回答
130
+ ${sampleAi || ' (无)'}
131
+
132
+ ## 备注
133
+ - 此摘要由模板生成 (LLM 调用失败 fallback)
134
+ `;
135
+ }
136
+ /**
137
+ * 解析 markdown 归档为 entries (用 "### " 切分)
138
+ */
139
+ function parseArchiveEntries(lines) {
140
+ const out = [];
141
+ let i = 0;
142
+ while (i < lines.length) {
143
+ if (lines[i].startsWith('### ')) {
144
+ const header = lines[i];
145
+ // 解析 "### 2026-07-05 10:30:45 UTC [channelName] — source"
146
+ const m = header.match(/^### (.+?)\s*(?:\[(.+?)\])?\s*—\s*(\S+?)(?:\s+\(.+?\))?$/);
147
+ if (m) {
148
+ const ts = m[1].replace(/\s+UTC$/, '').trim();
149
+ const channelName = m[2];
150
+ const source = m[3];
151
+ // 找下一段 "### " 或 "---" 之间的内容
152
+ let j = i + 1;
153
+ const bodyLines = [];
154
+ while (j < lines.length && !lines[j].startsWith('### ') && !lines[j].startsWith('---')) {
155
+ bodyLines.push(lines[j]);
156
+ j++;
157
+ }
158
+ const text = bodyLines.join('\n').trim();
159
+ out.push({
160
+ startLine: i,
161
+ entry: {
162
+ ts: ts.includes('T') ? ts : ts.replace(' ', 'T') + 'Z',
163
+ source,
164
+ channelName,
165
+ text,
166
+ }
167
+ });
168
+ i = j;
169
+ continue;
170
+ }
171
+ }
172
+ i++;
173
+ }
174
+ return out;
175
+ }
176
+ /**
177
+ * 主入口: 压缩月度归档到 memory.
178
+ */
179
+ export async function compressMonthlyArchive(opts) {
180
+ const agentId = sanitizeId(opts.agentId);
181
+ const publicKey = opts.publicKey;
182
+ const yearMonth = opts.yearMonth || peerFs.currentYearMonth();
183
+ const home = opts.home || HOME;
184
+ const summaryPath = getPeerSummaryPath(agentId, publicKey, yearMonth);
185
+ const cursorPath = getPeerSummaryCursorPath(agentId, publicKey, yearMonth);
186
+ const archivePath = peerFs.getPeerChatPath(publicKey, yearMonth);
187
+ // 读归档
188
+ const lines = await readArchiveLines(publicKey, yearMonth);
189
+ if (lines.length === 0) {
190
+ return { summaryPath, cursorPath, entriesCount: 0, bytesWritten: 0, skipped: 'no-archive' };
191
+ }
192
+ // 解析 entries
193
+ const allEntries = parseArchiveEntries(lines);
194
+ const cursor = await readSummaryCursor(cursorPath);
195
+ const newEntries = allEntries.slice(cursor);
196
+ const minNew = opts.minNewEntries ?? 20;
197
+ if (newEntries.length < minNew) {
198
+ return { summaryPath, cursorPath, entriesCount: newEntries.length, bytesWritten: 0, skipped: 'too-few-entries' };
199
+ }
200
+ const timestamp = new Date().toISOString();
201
+ let summaryBody;
202
+ try {
203
+ const sysPrompt = '你是 bolloon 月度记忆压缩助手. 输入是按 peer 月度归档的聊天记录 markdown, 输出 400-800 字中文摘要, 包含: 1) 关键合作主题; 2) 共同完成的成果; 3) 待跟进事项; 4) 对方能力偏好. 不要寒暄, 不复述已知.';
204
+ const recentSnippet = newEntries.slice(-30).map(e => {
205
+ const ts = e.entry.ts;
206
+ const chan = e.entry.channelName ? `[${e.entry.channelName}]` : '';
207
+ return `[${ts}]${chan} ${e.entry.source}: ${e.entry.text}`;
208
+ }).join('\n').slice(0, 8000);
209
+ const userPrompt = `Peer: ${publicKey}\n月份: ${yearMonth}\n时间: ${timestamp}\n新增条数: ${newEntries.length}\n\n最近对话:\n${recentSnippet}`;
210
+ summaryBody = await tryMonthlyLlm(sysPrompt, userPrompt);
211
+ }
212
+ catch (e) {
213
+ summaryBody = templateMonthlySummary({
214
+ publicKey,
215
+ yearMonth,
216
+ entries: newEntries.map(e => e.entry),
217
+ timestamp,
218
+ });
219
+ }
220
+ const block = `\n\n---\n\n## 增量摘要 @ ${timestamp} (${newEntries.length} entries)\n\n${summaryBody.trim()}\n`;
221
+ await fs.mkdir(path.dirname(summaryPath), { recursive: true });
222
+ await fs.appendFile(summaryPath, block, 'utf-8');
223
+ await writeSummaryCursor(cursorPath, allEntries.length);
224
+ return {
225
+ summaryPath,
226
+ cursorPath,
227
+ entriesCount: newEntries.length,
228
+ bytesWritten: Buffer.byteLength(block, 'utf-8'),
229
+ };
230
+ }
231
+ /**
232
+ * 把 session.json 中的所有消息按 peer 切分, 一次性 archive 到各 peer 月度文件.
233
+ * 用于: ① 启动时迁移已有 session 历史到 peer archive; ② 兜底补档.
234
+ */
235
+ export async function archiveSessionMessagesForPeer(opts) {
236
+ let written = 0;
237
+ let errors = 0;
238
+ for (const m of opts.messages) {
239
+ const ts = m.timestamp || new Date().toISOString();
240
+ const entry = {
241
+ ts,
242
+ source: m.source || (m.type === 'user' ? 'remote' : 'ai-mention'),
243
+ channelId: m.channelId,
244
+ channelName: m.channelName,
245
+ text: m.content,
246
+ fromPublicKey: m.fromPublicKey || opts.publicKey,
247
+ msgType: m.type,
248
+ };
249
+ const r = await appendChatArchive({ publicKey: opts.publicKey, entry });
250
+ if (r.ok)
251
+ written++;
252
+ else
253
+ errors++;
254
+ }
255
+ return { written, errors };
256
+ }
257
+ /**
258
+ * 列出指定 peer 所有可用的月度摘要 (按月份倒序)
259
+ */
260
+ export async function listPeerSummaries(agentId, publicKey) {
261
+ const dir = getPeerSummaryDir(agentId, publicKey);
262
+ try {
263
+ const entries = await fs.readdir(dir);
264
+ return entries
265
+ .filter(e => /^\d{4}-\d{2}\.summary\.md$/.test(e))
266
+ .map(e => ({
267
+ yearMonth: e.replace(/\.summary\.md$/, ''),
268
+ path: path.join(dir, e),
269
+ size: fsSync.statSync(path.join(dir, e)).size,
270
+ }))
271
+ .sort((a, b) => b.yearMonth.localeCompare(a.yearMonth));
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ }
@@ -136,10 +136,13 @@ async function collectJudgmentsSummary(topN) {
136
136
  topValues,
137
137
  };
138
138
  }
139
- async function collectSkills() {
139
+ async function collectSkills(projectCwd) {
140
140
  const home = process.env.HOME || os.homedir() || '/tmp';
141
141
  const userSkillsDir = path.join(home, '.bolloon', 'skills');
142
- const projectSkillsDir = path.join(process.cwd(), '.bolloon', 'skills');
142
+ // 2026-07-04: opts.cwd 而不是 process.cwd(), collectBolloonContext({cwd})
143
+ // 在测试或子进程里能正确隔离项目级 skill 目录 (之前 process.cwd() 会读到
144
+ // caller 的 cwd, 测试空目录时泄漏真实项目 skills).
145
+ const projectSkillsDir = path.join(projectCwd || process.cwd(), '.bolloon', 'skills');
143
146
  const out = [];
144
147
  const seen = new Set();
145
148
  for (const dir of [userSkillsDir, projectSkillsDir]) {
@@ -273,7 +276,7 @@ export async function collectBolloonContext(opts) {
273
276
  collectProject(cwd, bolloonMdMaxBytes),
274
277
  collectGit(cwd, gitCommitLimit),
275
278
  collectPersona(),
276
- collectSkills(),
279
+ collectSkills(cwd),
277
280
  Promise.resolve(collectEnv()),
278
281
  collectPending(opts),
279
282
  collectHierarchy(cwd),
@@ -0,0 +1,160 @@
1
+ /**
2
+ * event-log.ts — 项目事件日志 (Layer 2)
3
+ *
4
+ * 2026-07-07 新增. 解决:
5
+ * - 当前 judgment 是"决策规则", 项目状态/feature 生命周期没有结构化记录
6
+ * - 用户问"这个项目我们做了什么"时无法结构化回放
7
+ * - LLM prompt 缺少"最近变更"上下文
8
+ *
9
+ * 设计:
10
+ * - JSONL 格式 (append-only), 路径: ~/.bolloon/project/<safe-channelId>/events.jsonl
11
+ * - 5 类事件: project_created / feature_added / feature_removed / requirement_changed / task_completed
12
+ * - 每条带 schema_version, 便于后续演化
13
+ * - 不依赖 LLM, 纯文件 IO; 失败静默不阻塞主对话
14
+ *
15
+ * 触发:
16
+ * - server.ts: /message 处理后根据用户/AI 内容检测事件关键词 → appendEvent
17
+ * - server.ts: LLM prompt 装配 → getRecentEvents(channelId, 5)
18
+ * - client.ts: channel 头部 → 显示事件时间线折叠块
19
+ */
20
+ import * as fs from 'fs/promises';
21
+ import * as path from 'path';
22
+ import * as os from 'os';
23
+ // ============== 类型 ==============
24
+ export const EVENT_SCHEMA_VERSION = 1;
25
+ export const EVENT_TYPES = [
26
+ 'project_created',
27
+ 'feature_added',
28
+ 'feature_removed',
29
+ 'requirement_changed',
30
+ 'task_completed',
31
+ ];
32
+ // ============== 路径 ==============
33
+ function sanitize(s) {
34
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
35
+ }
36
+ /** ~/.bolloon/project/<safe-channelId>/events.jsonl */
37
+ export function getEventLogPath(channelId, home) {
38
+ const root = path.join(home || os.homedir(), '.bolloon', 'project', sanitize(channelId));
39
+ return path.join(root, 'events.jsonl');
40
+ }
41
+ // ============== 追加 ==============
42
+ /**
43
+ * 追加一条事件. JSONL 格式 (1 行 1 JSON).
44
+ * 失败抛错 — 由 caller 决定是否静默.
45
+ */
46
+ export async function appendEvent(opts) {
47
+ const filePath = getEventLogPath(opts.channelId, opts.home);
48
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
49
+ const event = {
50
+ schema_version: EVENT_SCHEMA_VERSION,
51
+ type: opts.type,
52
+ ts: new Date().toISOString(),
53
+ channelId: opts.channelId,
54
+ agentId: opts.agentId,
55
+ summary: opts.summary,
56
+ detail: opts.detail || {},
57
+ source: opts.source || 'system',
58
+ };
59
+ const line = JSON.stringify(event) + '\n';
60
+ await fs.appendFile(filePath, line, 'utf-8');
61
+ return { path: filePath, bytesWritten: Buffer.byteLength(line, 'utf-8') };
62
+ }
63
+ // ============== 读取 ==============
64
+ /**
65
+ * 列出事件. 倒序 (最新在前), 可选 limit / type 过滤.
66
+ * 文件不存在 → 返回 []. 单行损坏 → 跳过, 不抛错.
67
+ */
68
+ export async function listEvents(opts) {
69
+ const filePath = getEventLogPath(opts.channelId, opts.home);
70
+ let raw;
71
+ try {
72
+ raw = await fs.readFile(filePath, 'utf-8');
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ const events = [];
78
+ const lines = raw.split('\n');
79
+ for (const line of lines) {
80
+ const trimmed = line.trim();
81
+ if (!trimmed)
82
+ continue;
83
+ try {
84
+ const ev = JSON.parse(trimmed);
85
+ if (opts.type && ev.type !== opts.type)
86
+ continue;
87
+ if (typeof ev !== 'object' || !ev.type || !ev.ts)
88
+ continue;
89
+ events.push(ev);
90
+ }
91
+ catch {
92
+ // 跳过损坏行
93
+ }
94
+ }
95
+ // 倒序 (最新在前)
96
+ events.reverse();
97
+ if (opts.limit && opts.limit > 0) {
98
+ return events.slice(0, opts.limit);
99
+ }
100
+ return events;
101
+ }
102
+ /**
103
+ * 取最近 N 条事件 (alias for listEvents, 默认 limit=5, 给 prompt 注入用).
104
+ */
105
+ export async function getRecentEvents(channelId, limit = 5, home) {
106
+ return listEvents({ channelId, home, limit });
107
+ }
108
+ // ============== 删除 ==============
109
+ /**
110
+ * 删事件日志 (channel 删除时调用). 文件不存在不报错.
111
+ */
112
+ export async function deleteEventLog(channelId, home) {
113
+ const filePath = getEventLogPath(channelId, home);
114
+ try {
115
+ await fs.unlink(filePath);
116
+ }
117
+ catch (e) {
118
+ if (e?.code !== 'ENOENT')
119
+ throw e;
120
+ }
121
+ }
122
+ // ============== 简易事件检测 (heuristic) ==============
123
+ /**
124
+ * 从用户消息中检测可能的事件 (基于关键词). 返回最多 N 条事件.
125
+ * LLM 检测更准, 但本函数作为 fallback / 实时检测用, 不阻塞主对话.
126
+ */
127
+ export function detectEventsFromText(opts) {
128
+ const text = opts.text.toLowerCase();
129
+ const out = [];
130
+ // project_created: "建项目" / "create project" / "new project" / "新项目"
131
+ if (/(建项目|创建项目|新项目|create\s*project|new\s*project)/i.test(text)) {
132
+ out.push({ type: 'project_created', summary: opts.text.slice(0, 80), detail: { raw: opts.text.slice(0, 200) } });
133
+ }
134
+ // feature_added: "加入" / "增加" / "添加" + 跟一个 "功能/feature" 关键字
135
+ if (/(加入|增加|添加|添加了|加\s*一个|add\s*feature|add\s*function)/i.test(text)) {
136
+ const m = text.match(/(加入|增加|添加|添加了|加\s*一个|add\s*feature|add\s*function)\s*[::]?\s*([^。.!?\n]{2,40})/i);
137
+ if (m) {
138
+ out.push({ type: 'feature_added', summary: `+ ${m[2].trim()}`, detail: { name: m[2].trim(), raw: opts.text.slice(0, 200) } });
139
+ }
140
+ }
141
+ // feature_removed: "不要" / "去掉" / "删除" + "功能"
142
+ if (/(不要|去掉|删除|移除|remove|delete|drop)\s*(这个|该|那)?\s*(功能|特性|feature)/i.test(text)) {
143
+ const m = text.match(/(不要|去掉|删除|移除|remove|delete|drop)\s*(?:了)?\s*[::]?\s*([^。.!?\n]{2,40})/i);
144
+ if (m) {
145
+ out.push({ type: 'feature_removed', summary: `- ${m[2].trim()}`, detail: { name: m[2].trim(), raw: opts.text.slice(0, 200) } });
146
+ }
147
+ }
148
+ // requirement_changed: "改成" / "修改" / "调整为"
149
+ if (/(改成|修改|调整|需求变更|change\s*to|update\s*to|revise)/i.test(text)) {
150
+ const m = text.match(/(改成|修改|调整|change\s*to|update\s*to|revise)\s*[::]?\s*([^。.!?\n]{2,60})/i);
151
+ if (m) {
152
+ out.push({ type: 'requirement_changed', summary: `Δ ${m[2].trim()}`, detail: { target: m[2].trim(), raw: opts.text.slice(0, 200) } });
153
+ }
154
+ }
155
+ // task_completed: "完成" / "搞定" / "done" / "shipped"
156
+ if (/(完成了?|搞定了?|做完了?|done|finished|shipped|closed)/i.test(text)) {
157
+ out.push({ type: 'task_completed', summary: opts.text.slice(0, 80), detail: { raw: opts.text.slice(0, 200) } });
158
+ }
159
+ return out.slice(0, 3); // 一次最多 3 条 (避免 spam)
160
+ }
@@ -9,6 +9,7 @@ import * as os from 'os';
9
9
  import * as path from 'path';
10
10
  import { getCachedBolloonContext, clearBolloonContextCache } from './context-collector.js';
11
11
  import { formatContextForSystemPrompt } from './project-context.js';
12
+ import { loadPersonaDocs, formatPersonaForSystemPrompt } from './persona-loader.js';
12
13
  let lastSessionStartAt = 0;
13
14
  const MIN_INTERVAL_MS = 5000; // 同一进程 5s 内最多触发一次, 防止循环
14
15
  export async function onSessionStart(opts = {}) {
@@ -25,10 +26,23 @@ export async function onSessionStart(opts = {}) {
25
26
  if (opts.channelId) {
26
27
  systemAddition = `# 当前 channel: ${opts.channelId}\n\n` + systemAddition;
27
28
  }
29
+ // 在头部最前拼 persona 文档 (如果 agentId 存在)
30
+ if (opts.agentId) {
31
+ try {
32
+ const docs = await loadPersonaDocs(opts.agentId);
33
+ const personaText = formatPersonaForSystemPrompt(docs);
34
+ if (personaText) {
35
+ systemAddition = personaText + '\n\n' + systemAddition;
36
+ }
37
+ }
38
+ catch (err) {
39
+ console.warn('[lifecycle-hooks] loadPersonaDocs failed (silent):', err);
40
+ }
41
+ }
28
42
  return {
29
43
  systemAddition,
30
44
  collectMs: Date.now() - start,
31
- truncated: systemAddition.length > 0 && systemAddition.includes('截断模式'),
45
+ truncated: systemAddition.length > 0 && systemAddition.includes('截断'),
32
46
  };
33
47
  }
34
48
  catch (err) {
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Memory Compressor — session 消息 → LLM 摘要 → ~/.bolloon/memory/<agentId>/sessions/
3
+ *
4
+ * 触发: 每次 /message 处理后, saveSession 之后 (server.ts:2073 之后)
5
+ * 行为:
6
+ * - 读 session messages
7
+ * - 只压缩 cursor 之后的增量 (≥ 4 条新 messages 才压缩, 防频繁)
8
+ * - 调 LLM 生成中文摘要 (失败 fallback 到纯模板)
9
+ * - append 模式写入 <safe-channelId>__<safe-sessionId>.summary.md
10
+ * - 维护 cursor 文件 <safe-channelId>__<safe-sessionId>.cursor
11
+ *
12
+ * 失败静默: 调用方 try/catch 后 console.warn, 不阻塞主对话.
13
+ */
14
+ import * as fs from 'fs/promises';
15
+ import * as os from 'os';
16
+ import * as path from 'path';
17
+ export function sanitizeAgentId(id) {
18
+ return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
19
+ }
20
+ export function sanitizeKey(id) {
21
+ return id.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 128);
22
+ }
23
+ /**
24
+ * ~/.bolloon/memory/<sanitizedAgentId>/
25
+ */
26
+ export function getMemoryDir(agentId, home) {
27
+ return path.join(home || os.homedir(), '.bolloon', 'memory', sanitizeAgentId(agentId));
28
+ }
29
+ /**
30
+ * ~/.bolloon/memory/<agentId>/sessions/<safe-channelId>__<safe-sessionId>.summary.md
31
+ */
32
+ export function getSessionSummaryPath(agentId, channelId, sessionId, home) {
33
+ const key = `${sanitizeKey(channelId)}__${sanitizeKey(sessionId)}`;
34
+ return path.join(getMemoryDir(agentId, home), 'sessions', `${key}.summary.md`);
35
+ }
36
+ export function getSessionCursorPath(agentId, channelId, sessionId, home) {
37
+ const key = `${sanitizeKey(channelId)}__${sanitizeKey(sessionId)}`;
38
+ return path.join(getMemoryDir(agentId, home), 'sessions', `${key}.cursor`);
39
+ }
40
+ /**
41
+ * Session 缓存文件路径 (跟 server.ts:1806 sessionKey 规则一致)
42
+ */
43
+ function getSessionCacheFile(channelId, sessionId, home) {
44
+ const root = path.join(home || os.homedir(), '.bolloon', 'sessions', 'cache');
45
+ const safeChannel = sanitizeKey(channelId);
46
+ const safeSession = sanitizeKey(sessionId).replace(/:/g, '__');
47
+ return path.join(root, `${safeChannel}__${safeSession}.json`);
48
+ }
49
+ async function readCursor(cursorPath) {
50
+ try {
51
+ const raw = await fs.readFile(cursorPath, 'utf-8');
52
+ const n = parseInt(raw.trim(), 10);
53
+ return Number.isFinite(n) && n >= 0 ? n : 0;
54
+ }
55
+ catch {
56
+ return 0;
57
+ }
58
+ }
59
+ async function writeCursor(cursorPath, value) {
60
+ await fs.mkdir(path.dirname(cursorPath), { recursive: true });
61
+ await fs.writeFile(cursorPath, String(value), 'utf-8');
62
+ }
63
+ async function readSessionMessages(sessionCacheFile) {
64
+ try {
65
+ const raw = await fs.readFile(sessionCacheFile, 'utf-8');
66
+ const parsed = JSON.parse(raw);
67
+ if (Array.isArray(parsed))
68
+ return parsed;
69
+ if (Array.isArray(parsed.messages))
70
+ return parsed.messages;
71
+ return [];
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ /**
78
+ * 纯模板摘要 (LLM 失败 fallback, 不依赖外部调用)
79
+ */
80
+ function templateSummary(opts) {
81
+ const userMsgs = opts.newMessages.filter(m => m.type === 'user');
82
+ const aiMsgs = opts.newMessages.filter(m => m.type === 'ai');
83
+ const userSnippet = userMsgs.slice(0, 3).map(m => ` - ${m.content.slice(0, 80)}`).join('\n');
84
+ const aiSnippet = aiMsgs.slice(0, 3).map(m => ` - ${m.content.slice(0, 80)}`).join('\n');
85
+ return `# Session 摘要 — ${opts.channelId} / ${opts.sessionId}
86
+
87
+ 时间: ${opts.timestamp}
88
+ 新增消息数: ${opts.newMessages.length} (user=${userMsgs.length}, ai=${aiMsgs.length})
89
+
90
+ ## 用户关键提问
91
+ ${userSnippet || ' (无)'}
92
+
93
+ ## AI 关键回答
94
+ ${aiSnippet || ' (无)'}
95
+
96
+ ## 备注
97
+ - 此摘要由模板生成 (LLM 调用失败 fallback)
98
+ `;
99
+ }
100
+ /**
101
+ * 尝试调 LLM 生成更精炼的中文摘要. 失败 → 抛错, 由 caller fallback.
102
+ *
103
+ * 用动态 import + 失败静默 — 不引入 pi-ai 强依赖 (避免循环).
104
+ */
105
+ async function tryLlmSummary(systemPrompt, userPrompt) {
106
+ try {
107
+ const piAi = await import('../llm/pi-ai.js');
108
+ const generateText = piAi.generateText;
109
+ if (typeof generateText !== 'function')
110
+ throw new Error('generateText not exported');
111
+ const result = await generateText({
112
+ messages: [
113
+ { role: 'system', content: systemPrompt },
114
+ { role: 'user', content: userPrompt },
115
+ ],
116
+ temperature: 0.2,
117
+ maxTokens: 800,
118
+ });
119
+ const text = result?.reply || result?.text || '';
120
+ if (typeof text !== 'string' || text.length < 20)
121
+ throw new Error('LLM returned too-short text');
122
+ return text;
123
+ }
124
+ catch (e) {
125
+ throw new Error(`LLM call failed: ${e?.message || String(e)}`);
126
+ }
127
+ }
128
+ export async function compressSessionToMemory(opts) {
129
+ const agentId = sanitizeAgentId(opts.agentId);
130
+ const home = opts.home || os.homedir();
131
+ const summaryPath = getSessionSummaryPath(agentId, opts.channelId, opts.sessionId, home);
132
+ const cursorPath = getSessionCursorPath(agentId, opts.channelId, opts.sessionId, home);
133
+ const sessionCacheFile = getSessionCacheFile(opts.channelId, opts.sessionId, home);
134
+ const allMessages = await readSessionMessages(sessionCacheFile);
135
+ const cursor = await readCursor(cursorPath);
136
+ const newMessages = allMessages.slice(cursor);
137
+ const minNew = opts.minNewMessages ?? 4;
138
+ if (allMessages.length === 0) {
139
+ return { summaryPath, cursorPath, messagesCount: 0, bytesWritten: 0, skipped: 'no-new-messages' };
140
+ }
141
+ if (newMessages.length < minNew) {
142
+ return { summaryPath, cursorPath, messagesCount: newMessages.length, bytesWritten: 0, skipped: 'too-few-messages' };
143
+ }
144
+ const timestamp = new Date().toISOString();
145
+ let summaryBody;
146
+ try {
147
+ const sysPrompt = '你是 bolloon 记忆压缩助手. 输入是一段 session 消息历史 (用户问题 + AI 回答), 输出 200-400 字中文摘要, 包含 3-5 条关键发现和未完成事项. 不要寒暄, 不要复述已知. 格式: ## 关键发现 / ## 待办.';
148
+ const recentSnippet = newMessages.slice(-10).map(m => `[${m.type}] ${m.content}`).join('\n---\n').slice(0, 6000);
149
+ const userPrompt = `Channel: ${opts.channelId}\nSession: ${opts.sessionId}\n时间: ${timestamp}\n新增消息数: ${newMessages.length}\n\n最近消息:\n${recentSnippet}`;
150
+ summaryBody = await tryLlmSummary(sysPrompt, userPrompt);
151
+ }
152
+ catch (e) {
153
+ summaryBody = templateSummary({
154
+ channelId: opts.channelId,
155
+ sessionId: opts.sessionId,
156
+ newMessages,
157
+ timestamp,
158
+ });
159
+ }
160
+ const block = `\n\n---\n\n## 增量摘要 @ ${timestamp} (${newMessages.length} messages)\n\n${summaryBody.trim()}\n`;
161
+ await fs.mkdir(path.dirname(summaryPath), { recursive: true });
162
+ await fs.appendFile(summaryPath, block, 'utf-8');
163
+ await writeCursor(cursorPath, allMessages.length);
164
+ return {
165
+ summaryPath,
166
+ cursorPath,
167
+ messagesCount: newMessages.length,
168
+ bytesWritten: Buffer.byteLength(block, 'utf-8'),
169
+ };
170
+ }