@bolloon/bolloon-agent 0.6.5 → 0.6.7

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 (188) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/ha-daemon.js +85 -0
  9. package/dist/agents/ha-supervisor.js +146 -0
  10. package/dist/agents/intent-classifier.js +1 -1
  11. package/dist/agents/iteration-budget.js +12 -0
  12. package/dist/agents/knowledge-organizer.js +0 -2
  13. package/dist/agents/lesson-to-skill.js +0 -1
  14. package/dist/agents/loop-review.js +2 -2
  15. package/dist/agents/memory-store.js +384 -0
  16. package/dist/agents/opportunity-match.js +15 -3
  17. package/dist/agents/opportunity-web-search.js +39 -0
  18. package/dist/agents/p2p-chat-tools.js +1 -2
  19. package/dist/agents/parse-tool-call.js +9 -22
  20. package/dist/agents/pi-harness.js +1 -2
  21. package/dist/agents/pi-sdk-session-manager.js +8 -0
  22. package/dist/agents/pi-sdk-tools.js +180 -5
  23. package/dist/agents/pi-sdk.js +210 -503
  24. package/dist/agents/process-runner.js +28 -9
  25. package/dist/agents/react-loop.js +9 -17
  26. package/dist/agents/runner-resolver.js +4 -3
  27. package/dist/agents/skill-loader.js +22 -15
  28. package/dist/agents/skills-manager.js +1 -25
  29. package/dist/agents/workflow-pivot-loop.js +152 -61
  30. package/dist/bootstrap/context-collector.js +67 -7
  31. package/dist/bootstrap/context-hierarchy.js +13 -1
  32. package/dist/bootstrap/persona-loader.js +26 -0
  33. package/dist/cli/ink-app.js +44 -17
  34. package/dist/cli/intent-command.js +22 -12
  35. package/dist/cli/loading-tui.js +8 -2
  36. package/dist/cli/log-gate.js +40 -8
  37. package/dist/cli/mention-data.js +1 -0
  38. package/dist/cli/reply-hygiene.js +22 -0
  39. package/dist/cli/trace-line.js +61 -2
  40. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  41. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  42. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  43. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  44. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  45. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  46. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  47. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  48. package/dist/electron-build/agents/contacts/tools.js +112 -0
  49. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  50. package/dist/electron-build/agents/experience-review.js +0 -3
  51. package/dist/electron-build/agents/experience-review.js.map +1 -1
  52. package/dist/electron-build/agents/external-events.js.map +1 -1
  53. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  55. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  56. package/dist/electron-build/agents/goal-resume.js +0 -4
  57. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  58. package/dist/electron-build/agents/intent-classifier.js +1 -1
  59. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  60. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  61. package/dist/electron-build/agents/loop-review.js +2 -2
  62. package/dist/electron-build/agents/loop-review.js.map +1 -1
  63. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  64. package/dist/electron-build/agents/memory-store.js +430 -0
  65. package/dist/electron-build/agents/memory-store.js.map +1 -0
  66. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  67. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  68. package/dist/electron-build/agents/pi-harness.js +1 -2
  69. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  71. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  72. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  73. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  74. package/dist/electron-build/agents/pi-sdk.js +208 -501
  75. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  76. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  77. package/dist/electron-build/agents/process-runner.js +28 -9
  78. package/dist/electron-build/agents/process-runner.js.map +1 -1
  79. package/dist/electron-build/agents/runner-resolver.js +4 -3
  80. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  81. package/dist/electron-build/agents/skill-loader.js +22 -16
  82. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  83. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  84. package/dist/electron-build/agents/skills-manager.js +0 -24
  85. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  86. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  87. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  88. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  89. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  90. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  91. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  92. package/dist/electron-build/bootstrap/context-collector.js +68 -7
  93. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  94. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  95. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  96. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  97. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  98. package/dist/electron-build/electron-preload.js.map +1 -1
  99. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  100. package/dist/electron-build/kernel/channel-actor.js +31 -2
  101. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  102. package/dist/electron-build/kernel/model-runtime.js +8 -1
  103. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  104. package/dist/electron-build/llm/config-store.js +20 -0
  105. package/dist/electron-build/llm/config-store.js.map +1 -1
  106. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  107. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  108. package/dist/electron-build/llm/pi-ai.js +112 -38
  109. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  110. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  111. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  112. package/dist/electron-build/network/agent-network.js.map +1 -1
  113. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  114. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  115. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  116. package/dist/electron-build/security/tool-gate.js +2 -0
  117. package/dist/electron-build/security/tool-gate.js.map +1 -1
  118. package/dist/electron-build/setup/setup-store.js +34 -6
  119. package/dist/electron-build/setup/setup-store.js.map +1 -1
  120. package/dist/electron-build/social/heartbeat.js +18 -5
  121. package/dist/electron-build/social/heartbeat.js.map +1 -1
  122. package/dist/electron-build/utils/update-manager.js.map +1 -1
  123. package/dist/index.js +250 -12
  124. package/dist/ios/bolloon-web.json +3 -3
  125. package/dist/ios/client.js +74 -35
  126. package/dist/ios/icons/icon-1400x1400.png +0 -0
  127. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  128. package/dist/ios/index.html +122 -108
  129. package/dist/ios/mobile-agent.js +572 -45
  130. package/dist/ios/mobile-contacts.js +0 -4
  131. package/dist/ios/mobile-core.js +8079 -4590
  132. package/dist/ios/mobile-harness.js +1004 -0
  133. package/dist/ios/mobile-memory.js +317 -0
  134. package/dist/ios/mobile-orbitdb.js +194 -0
  135. package/dist/ios/mobile-persona.js +80 -0
  136. package/dist/ios/mobile-privacy.js +2 -3
  137. package/dist/ios/mobile.css +286 -21
  138. package/dist/ios/mobile.html +122 -108
  139. package/dist/ios/mobile.js +434 -152
  140. package/dist/ios/routes-contacts.js +94 -1
  141. package/dist/ios/routes-world.js +13 -2
  142. package/dist/ios/server.js +54 -41
  143. package/dist/ios/style.css +13 -5
  144. package/dist/ios/sw.js +1 -1
  145. package/dist/ios/ui/message-renderer.js +4 -18
  146. package/dist/ios/ui/step-timeline.js +14 -1
  147. package/dist/kernel/channel-actor.js +30 -2
  148. package/dist/kernel/gate-scan.js +2 -2
  149. package/dist/kernel/ha/ha-arbiter.js +177 -0
  150. package/dist/kernel/ha/ha-guard.js +151 -0
  151. package/dist/kernel/index.js +36 -0
  152. package/dist/kernel/model-runtime.js +8 -1
  153. package/dist/kernel/plan.js +5 -5
  154. package/dist/kernel/roster.js +3 -3
  155. package/dist/llm/config-store.js +20 -0
  156. package/dist/llm/embedding-adapter.js +69 -0
  157. package/dist/llm/pi-ai.js +110 -37
  158. package/dist/llm/system-prompt/registry.js +8 -6
  159. package/dist/orbitdb/memory-sync.js +70 -0
  160. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  161. package/dist/security/tool-gate.js +2 -0
  162. package/dist/setup/setup-store.js +34 -6
  163. package/dist/social/heartbeat.js +18 -5
  164. package/dist/web/bolloon-web.json +3 -3
  165. package/dist/web/client.js +74 -35
  166. package/dist/web/icons/icon-1400x1400.png +0 -0
  167. package/dist/web/icons/icon-1400x1400.webp +0 -0
  168. package/dist/web/index.html +37 -25
  169. package/dist/web/mobile-agent.js +572 -45
  170. package/dist/web/mobile-contacts.js +0 -4
  171. package/dist/web/mobile-core.js +8079 -4590
  172. package/dist/web/mobile-harness.js +1004 -0
  173. package/dist/web/mobile-memory.js +317 -0
  174. package/dist/web/mobile-orbitdb.js +194 -0
  175. package/dist/web/mobile-persona.js +80 -0
  176. package/dist/web/mobile-privacy.js +2 -3
  177. package/dist/web/mobile.css +286 -21
  178. package/dist/web/mobile.html +122 -108
  179. package/dist/web/mobile.js +434 -152
  180. package/dist/web/routes-contacts.js +94 -1
  181. package/dist/web/routes-world.js +13 -2
  182. package/dist/web/server.js +54 -41
  183. package/dist/web/style.css +13 -5
  184. package/dist/web/sw.js +1 -1
  185. package/dist/web/ui/message-renderer.js +4 -18
  186. package/dist/web/ui/step-timeline.js +14 -1
  187. package/package.json +3 -2
  188. package/scripts/build-web.ts +16 -1
package/dist/llm/pi-ai.js CHANGED
@@ -9,6 +9,42 @@ export const LLAMACPP_DEFAULT_BASE_URL = 'http://localhost:8080/v1';
9
9
  export const LLAMACPP_DEFAULT_MODEL = 'local';
10
10
  /** 唯一的重路径用途 */
11
11
  export const MAIN_AGENT_PURPOSE = 'main-agent';
12
+ // ---------------------------------------------------------------------------
13
+ // 模型调用并发闸 (2026-10-15, leo: 「[AI 服务调用失败] ... 429 并发请求已达上限, 有错误也要修复」)
14
+ // ---------------------------------------------------------------------------
15
+ // 根因: CLI/Web/心跳/会话总结等多路会同时打同一个 provider, 上游按 API key 限并发
16
+ // (实测 zai/glm: `当前 API Key 的并发请求已达到上限` 429 rate_limit_error)。
17
+ // 修复: 在**全仓唯一模型调用咽喉** generateText 前按 `providerId|baseUrl` 分桶限流 —
18
+ // 同 key 同时最多 LLM_MAX_CONCURRENCY 个在途请求, 其余排队 (轮询, 可响应 abort)。
19
+ const llmConcurrencyBuckets = new Map();
20
+ export const LLM_MAX_CONCURRENCY = (() => {
21
+ const n = Number(process.env.BOLLOON_LLM_MAX_CONCURRENCY ?? '');
22
+ return Number.isFinite(n) && n >= 1 ? Math.min(n, 8) : 2;
23
+ })();
24
+ /** 包一个模型调用进并发闸: 同 key 超限则排队, abort 时尽快退出 (轮询 120ms, 最坏等一拍) */
25
+ export async function withLlmConcurrency(key, signal, fn) {
26
+ for (;;) {
27
+ if (signal?.aborted)
28
+ throw new Error(String(signal.reason || 'AbortError'));
29
+ const b = llmConcurrencyBuckets.get(key) ?? { active: 0, waiting: 0 };
30
+ if (b.active < LLM_MAX_CONCURRENCY) {
31
+ b.active++;
32
+ llmConcurrencyBuckets.set(key, b);
33
+ try {
34
+ return await fn();
35
+ }
36
+ finally {
37
+ b.active--;
38
+ if (b.active <= 0 && b.waiting <= 0)
39
+ llmConcurrencyBuckets.delete(key);
40
+ }
41
+ }
42
+ b.waiting++;
43
+ llmConcurrencyBuckets.set(key, b);
44
+ await new Promise((r) => setTimeout(r, 120));
45
+ b.waiting--;
46
+ }
47
+ }
12
48
  /** 轻量前缀的 system 上限 (字符). 只对**自动装配部分 + 调用方自带短提示**做兜底截断, 正常调用远低于它 */
13
49
  export const LIGHTWEIGHT_SYSTEM_MAX_CHARS = 2000;
14
50
  /**
@@ -577,7 +613,9 @@ export class PiAIModel {
577
613
  error = retryErr; // 记录最后一次错误, 循环继续重试 (backoff 递增)
578
614
  }
579
615
  }
580
- console.error('PiAI chat error:', error);
616
+ // 2026-10-15: 单行标签形 (`[pi-ai]` 是 log-gate INTERNAL_CHATTER_TAG, 只落盘不上屏;
617
+ // `分类` 在 for 循环块级作用域内, errMsg 在下面才解析 —— 这里自包含一行, 不引块内变量)
618
+ console.error(`[pi-ai] chat 失败: ${String(error?.message ?? error).replace(/\n/g, ' ').slice(0, 300)}`);
581
619
  // 2026-06-15: 真实 error 信息 + 明确告诉 LLM "这是 API 错, 不要 retry"
582
620
  // 旧版: "抱歉,AI服务暂时不可用。" → LLM 看到 isTooShort=false(< 50 但 > 0),
583
621
  // needsMoreWork 不会触发, 但 hasError 模式 (含 "error" / "失败") 会判定要继续修
@@ -655,29 +693,36 @@ export class PiAIModel {
655
693
  finalMessages = [{ role: 'system', content: `可用工具:\n${toolDescriptions}` }, ...messages];
656
694
  }
657
695
  }
658
- switch (this.provider) {
659
- case 'openai':
660
- case 'minimax':
661
- case 'deepseek':
662
- case 'kimi':
663
- case 'glm':
664
- case 'qwen':
665
- case 'mimo':
666
- case 'grok':
667
- return this.callOpenAI(finalMessages, temperature, maxTokens, signal, openaiTools, onToken, purpose, source);
668
- case 'anthropic':
669
- return this.callAnthropic(finalMessages, temperature, maxTokens, signal);
670
- case 'ollama':
671
- return this.callOllama(finalMessages, temperature, signal);
672
- case 'openrouter':
673
- return this.callOpenRouter(finalMessages, temperature, maxTokens, signal);
674
- case 'gemini':
675
- return this.callGemini(finalMessages, temperature, maxTokens, signal);
676
- case 'local':
677
- return this.callLocal(finalMessages, temperature, signal);
678
- default:
679
- throw new Error(`Unsupported provider: ${this.provider}`);
680
- }
696
+ // 2026-10-15 (leo: 429 并发已达上限, 有错误也要修复): 全仓唯一模型调用咽喉 —
697
+ // 所有入口 (CLI/Web/MCP/技能/子 Agent/长期任务) 都经 generateText 出网,
698
+ // 在这里按 `providerId|baseUrl` 分桶限流, 同 key 同时最多 LLM_MAX_CONCURRENCY 个在途,
699
+ // 排队等待 (轮询 120ms, abort 时尽快退出) — 不再把同一 API key 的并发打爆。
700
+ const bucketKey = `${this.config.providerId ?? this.provider}|${this.getBaseUrl()}`;
701
+ return withLlmConcurrency(bucketKey, signal, async () => {
702
+ switch (this.provider) {
703
+ case 'openai':
704
+ case 'minimax':
705
+ case 'deepseek':
706
+ case 'kimi':
707
+ case 'glm':
708
+ case 'qwen':
709
+ case 'mimo':
710
+ case 'grok':
711
+ return this.callOpenAI(finalMessages, temperature, maxTokens, signal, openaiTools, onToken, purpose, source);
712
+ case 'anthropic':
713
+ return this.callAnthropic(finalMessages, temperature, maxTokens, signal);
714
+ case 'ollama':
715
+ return this.callOllama(finalMessages, temperature, signal);
716
+ case 'openrouter':
717
+ return this.callOpenRouter(finalMessages, temperature, maxTokens, signal);
718
+ case 'gemini':
719
+ return this.callGemini(finalMessages, temperature, maxTokens, signal);
720
+ case 'local':
721
+ return this.callLocal(finalMessages, temperature, signal);
722
+ default:
723
+ throw new Error(`Unsupported provider: ${this.provider}`);
724
+ }
725
+ });
681
726
  }
682
727
  getApiKey() {
683
728
  // 2026-09-28: llama.cpp (本地 OpenAI 兼容服务) —— 声明 id 单独接 key, key 可空
@@ -1138,7 +1183,7 @@ export class PiAIModel {
1138
1183
  }
1139
1184
  // 注: callLocal 直接代理 ollama. 工具清单由 generateText 在外层拼到 messages, 这里 messages 已是 finalMessages.
1140
1185
  /**
1141
- * 2026-09-28: 装配系统提示词并拆成「稳定段 / 动态段」, 按 working dir 缓存 (TTL 10 分钟)。
1186
+ * 2026-09-28: 装配系统提示词并拆成「稳定段 / 动态段」, 按 working dir + **文档指纹** 缓存 (TTL 10 分钟)。
1142
1187
  *
1143
1188
  * 返回: `{ stableText, dynamicText, cached }`
1144
1189
  * - `stableText` 进 system (逐字节稳定 → 服务端前缀 KV 可复用);
@@ -1147,14 +1192,27 @@ export class PiAIModel {
1147
1192
  * 为什么缓存: 旧版每轮都跑 25 次 fs.readFile + 装配; 缓存后同一次会话内只装一次,
1148
1193
  * 且装配结果逐字节相同 (缓存本身也是"前缀稳定"的必要条件 —— 每次重装都可能因动态层
1149
1194
  * 内容变化而抖动)。
1195
+ *
1196
+ * 2026-10-15 (leo: 「文档改了要下一轮生效, 对齐 Hermes invalidate_system_prompt」):
1197
+ * 缓存 key 并入 `contextFingerprint` —— project Bolloon.md / AGENTS.md / CLAUDE.md /
1198
+ * hierarchy 4 层 / 当前 agent persona 任一文档的 mtime+size 变了, key 就变 ⇒ 缓存 miss
1199
+ * ⇒ 下一轮重装配。此前只有 working dir 作 key: 文档改了要等 10 分钟 TTL 才生效。
1150
1200
  */
1151
1201
  async buildSystemPromptParts(context) {
1152
- const key = context || process.cwd();
1153
1202
  const now = Date.now();
1203
+ const workingDir = context || process.cwd();
1204
+ let fp = '';
1205
+ try {
1206
+ const { contextFingerprint } = await import('../bootstrap/context-collector.js');
1207
+ fp = await contextFingerprint(workingDir);
1208
+ }
1209
+ catch { /* 指纹不可得 → 退回无指纹 key (文档失效钩子缺席, 但装配不阻塞) */ }
1210
+ // 缓存 key = workingDir + 指纹; 装配时 workingDir 保持原值 (不把指纹拼进 prompt 文本)
1211
+ const key = `${workingDir}\u0000${fp}`;
1154
1212
  if (_systemPromptCache && _systemPromptCache.key === key && now - _systemPromptCache.at < SYSTEM_PROMPT_CACHE_TTL_MS) {
1155
1213
  return { stableText: _systemPromptCache.stableText, dynamicText: _systemPromptCache.dynamicText, cached: true };
1156
1214
  }
1157
- const parts = await this.assembleSystemPromptPartsText(key);
1215
+ const parts = await this.assembleSystemPromptPartsText(workingDir);
1158
1216
  _systemPromptCache = { key, at: now, ...parts };
1159
1217
  return { ...parts, cached: false };
1160
1218
  }
@@ -1390,9 +1448,8 @@ export function initPiAI(config = {}) {
1390
1448
  const isLlamacpp = declared === LLAMACPP_PROVIDER_ID;
1391
1449
  const provider = isLlamacpp ? 'openai' : declared;
1392
1450
  const providerId = config.providerId || (isLlamacpp ? LLAMACPP_PROVIDER_ID : undefined);
1393
- const model = config.model
1394
- || (isLlamacpp ? (process.env.LLAMACPP_MODEL || LLAMACPP_DEFAULT_MODEL) : detectModel(provider));
1395
- let baseUrl = config.baseUrl || (isLlamacpp ? (process.env.LLAMACPP_BASE_URL || LLAMACPP_DEFAULT_BASE_URL) : undefined);
1451
+ let model = config.model || undefined;
1452
+ let baseUrl = config.baseUrl || undefined;
1396
1453
  console.log('[PiAIModel] Initializing with provider:', provider, 'model:', model);
1397
1454
  // 如果没有提供 apiKey,从配置文件读取
1398
1455
  let apiKey = config.apiKey;
@@ -1400,26 +1457,42 @@ export function initPiAI(config = {}) {
1400
1457
  // 否则"换 key"这种真变化不会重建实例, 指纹就失去意义)
1401
1458
  if (!apiKey && isLlamacpp)
1402
1459
  apiKey = process.env.LLAMACPP_API_KEY || undefined;
1403
- if (!apiKey) {
1460
+ // ── 2026-10-06 技术债重构: 配置文件对所有 provider 生效 (不只 llamacpp) ──
1461
+ // 用户是 200+ 模型 / 自适应 URL 的重度用户: 在配置里自定义 baseUrl/model 必须被尊重。
1462
+ // 之前只有 llamacpp 走这条, 其他 provider 一律被 getBaseUrl()/mapModel() 的
1463
+ // 内置默认 / env 覆盖掉 ⇒ 配置了自定义网关 URL 却打官方端点 (401 真实事故)。
1464
+ // 优先级: 显式 config > 配置文件 > env > 内置默认 (getBaseUrl/mapModel 天然如此,
1465
+ // 这里只把配置值塞进去, 让"配置文件"这一层对所有 provider 生效)。
1466
+ {
1404
1467
  try {
1405
1468
  const configPath = resolveConfigPath();
1406
1469
  if (configPath) {
1407
1470
  const configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1408
- const providerConfig = configData.providers[declared];
1409
- if (providerConfig?.apiKey) {
1471
+ const providerConfig = configData.providers?.[declared];
1472
+ if (!apiKey && providerConfig?.apiKey) {
1410
1473
  apiKey = providerConfig.apiKey;
1411
1474
  console.log('[PiAIModel] Loaded apiKey from config for', declared);
1412
1475
  }
1413
- // 2026-09-28: 本地服务 (llamacpp) 的 baseUrl 常写在配置里 → 只对这一家多读一个字段
1414
- // (别人保持"只读 apiKey"的老行为: 让 model/baseUrl 的优先级变动会牵连 llm-config 那条线)
1415
- if (isLlamacpp && !baseUrl && providerConfig?.baseUrl)
1476
+ // 2026-10-06: baseUrl / model 不再只给 llamacpp —— 所有 provider 都尊重配置文件
1477
+ // (显式 config 优先; 配置里的值只作为"未显式指定"时的兜底)
1478
+ if (!baseUrl && providerConfig?.baseUrl)
1416
1479
  baseUrl = providerConfig.baseUrl;
1480
+ if (!model && providerConfig?.model)
1481
+ model = providerConfig.model;
1482
+ if (providerConfig?.baseUrl || providerConfig?.model) {
1483
+ console.log('[PiAIModel] Loaded baseUrl/model from config for', declared, baseUrl ? 'baseUrl=' + baseUrl : '', model ? 'model=' + model : '');
1484
+ }
1417
1485
  }
1418
1486
  }
1419
1487
  catch (e) {
1420
- console.log('[PiAIModel] Error reading apiKey from config:', e);
1488
+ console.log('[PiAIModel] Error reading config:', e);
1421
1489
  }
1422
1490
  }
1491
+ // 兜底: 配置文件也没给 → 回落 llamacpp env / detectModel 内置默认 (老行为不变)
1492
+ if (!model)
1493
+ model = isLlamacpp ? (process.env.LLAMACPP_MODEL || LLAMACPP_DEFAULT_MODEL) : detectModel(provider);
1494
+ if (!baseUrl)
1495
+ baseUrl = isLlamacpp ? (process.env.LLAMACPP_BASE_URL || LLAMACPP_DEFAULT_BASE_URL) : undefined;
1423
1496
  // ── 2026-09-28: 按指纹幂等 ──────────────────────────────────────────────
1424
1497
  // 同一份配置重复 init (CLI 每轮 / web 每次请求 / 脚本多入口) **不再重建实例、不再清装配缓存**.
1425
1498
  // 之前每次 init 都清缓存 → 每轮重装 system → 前缀抖动 → 服务端前缀 KV 永远用不上.
@@ -87,13 +87,14 @@ const DEFAULT_META = (ttlDays) => ({
87
87
  const STATIC_LAYERS = [
88
88
  // ── core/ ──
89
89
  { id: 'core.identity', version: '1.0.0', priority: 50, appliesTo: ['all'], source: 'static-md', maxChars: 800, meta: DEFAULT_META(TTL_KNOWLEDGE) },
90
- { id: 'core.knowledge', version: '1.0.0', priority: 60, appliesTo: ['all'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_KNOWLEDGE) },
91
- { id: 'core.tools.thin', version: '1.0.0', priority: 70, appliesTo: ['all'], source: 'static-md', maxChars: 400, meta: DEFAULT_META(TTL_SAFETY) },
90
+ // 2026-10-16: 停用过时的通用知识/Computer Use 提示;实际工具与知识边界由运行时提供。
91
+ { id: 'core.knowledge', version: '1.0.0', priority: 60, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
92
+ { id: 'core.tools.thin', version: '1.0.0', priority: 70, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_SAFETY) },
92
93
  { id: 'core.hibs_reminders', version: '1.0.0', priority: 80, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_SAFETY) },
93
94
  { id: 'core.refusal', version: '1.0.0', priority: 100, appliesTo: ['all'], source: 'static-md', maxChars: 800, meta: DEFAULT_META(TTL_SAFETY) },
94
- { id: 'core.tone', version: '1.0.0', priority: 110, appliesTo: ['all'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_KNOWLEDGE) },
95
- { id: 'core.wellbeing', version: '1.0.0', priority: 120, appliesTo: ['all'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_KNOWLEDGE) },
96
- { id: 'core.evenhandedness', version: '1.0.0', priority: 130, appliesTo: ['all'], source: 'static-md', maxChars: 300, meta: DEFAULT_META(TTL_KNOWLEDGE) },
95
+ { id: 'core.tone', version: '1.0.0', priority: 110, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
96
+ { id: 'core.wellbeing', version: '1.0.0', priority: 120, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
97
+ { id: 'core.evenhandedness', version: '1.0.0', priority: 130, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
97
98
  { id: 'core.artifacts_storage', version: '1.0.0', priority: 145, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
98
99
  { id: 'core.network_filesystem', version: '1.0.0', priority: 148, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
99
100
  // ── role/ ── 阶段 0 只用 expert, 其他 3 个停用 (节省 + 阶段 0 不分 role)
@@ -111,7 +112,8 @@ const STATIC_LAYERS = [
111
112
  { id: 'channel.human-async', version: '1.0.0', priority: 150, appliesTo: ['local'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_CHANNEL) },
112
113
  { id: 'channel.session-handoff', version: '1.0.0', priority: 150, appliesTo: ['local', 'p2p-visitor', 'p2p-agent'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_CHANNEL) },
113
114
  // ── core/ ──
114
- { id: 'core.memory_system', version: '1.0.0', priority: 140, appliesTo: ['all'], source: 'static-md', maxChars: 200, meta: DEFAULT_META(TTL_KNOWLEDGE) },
115
+ // 2026-10-16: 停用 memory_system — 静态"无记忆"描述与真实记忆系统不符, 记忆状态由 buildStableMemoryBlock 动态注入。
116
+ { id: 'core.memory_system', version: '1.0.0', priority: 140, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
115
117
  // 2026-07-10 双栖 agent 网络新增 (按 plan 阶段 4):
116
118
  // priority 90 (在 channel 150 之后) — 保证 channel 装完后再装 core, 避免 channel 预算被 core 吃掉
117
119
  // maxChars 700 → 400: 7 个新 layer 总预算 3900 chars, 必须让出空间给 tool.p2p_request (700) + tool.goal_handoff (600)
@@ -0,0 +1,70 @@
1
+ /**
2
+ * memory-sync.ts — 记忆条目级 OrbitDB 复制 (P3, decentralized-memory-design §2.6)
3
+ *
4
+ * 在现有 CID 快照(整快照)之上加「条目级同步」:
5
+ * - 保存: MemoryEntry.id (sha256) 作为经内容寻址的 key
6
+ * - 存储: CIDDatabase type='memory', metadata 带 entryId + entryUpdatedAt
7
+ * - 复制: OrbitDB pubsub 自动 (既有机制)
8
+ * - 冲突: 同 entryId 冲突 → updatedAt 大者胜 (LWW), 平手比 content hash
9
+ *
10
+ * 诚实边界 (写死):
11
+ * - 条目级复制依赖 OrbitDB 网络就绪 (getCIDDatabase 初始化); 网络不可用 /
12
+ * 初始化失败 → 静默跳过 (本地记忆仍是权威, 复制是增强不是阻塞)
13
+ * - 本模块不建新 store, 复用既有 bolloon-cid-store (type='memory')
14
+ * - 跨机器真同步需双节点 (设计文档 §5 已记「跨机读群未验证」, P3 只保证
15
+ * 条目可被 CID 化 + 可恢复; 真双节点验证是 P3b)
16
+ */
17
+ import { getCIDDatabase } from './cid-database.js';
18
+ /** 条目 → CID 记录的 metadata 常量键 */
19
+ const META_ENTRY_ID = 'entryId';
20
+ const META_ENTRY_UPDATED = 'entryUpdatedAt';
21
+ /** 从 CID 记录还原 MemoryEntry (metadata 无 entryId 的旧记录 → null) */
22
+ export function entryFromRecord(rec) {
23
+ if (!rec)
24
+ return null;
25
+ const meta = rec.metadata || {};
26
+ if (!meta[META_ENTRY_ID])
27
+ return null;
28
+ const e = rec.content;
29
+ if (!e || typeof e !== 'object' || !e.id || !e.agentId)
30
+ return null;
31
+ return e;
32
+ }
33
+ /**
34
+ * 同步入口: 把一条 MemoryEntry 写入 OrbitDB (条目级)。
35
+ * 失败静默 (本地记忆不受影响), 返回 CID 或 null。
36
+ */
37
+ export async function syncMemoryEntryToOrbit(entry, db = getCIDDatabase()) {
38
+ try {
39
+ const rec = await db.save({
40
+ agentId: entry.agentId,
41
+ type: 'memory',
42
+ content: entry,
43
+ metadata: {
44
+ [META_ENTRY_ID]: entry.id,
45
+ [META_ENTRY_UPDATED]: entry.updatedAt,
46
+ kind: 'memory-entry',
47
+ },
48
+ });
49
+ return rec.id;
50
+ }
51
+ catch {
52
+ return null; // 网络/初始化失败静默 — 本地记忆仍权威
53
+ }
54
+ }
55
+ /**
56
+ * 拉取远端条目: 列出该 agent 的全部 memory type 记录, 还原成 MemoryEntry。
57
+ * 仅返回「本地没有 或 远端 updatedAt 更新」的条目 (LWW)。
58
+ * 不写本地 — 由调用方决定如何合并。
59
+ */
60
+ export async function fetchRemoteEntries(agentId, db = getCIDDatabase()) {
61
+ try {
62
+ const recs = await db.list({ agentId, type: 'memory' });
63
+ return recs
64
+ .map((r) => entryFromRecord(r))
65
+ .filter((e) => e !== null);
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
@@ -18,7 +18,7 @@
18
18
  import * as fs from 'fs/promises';
19
19
  import * as os from 'os';
20
20
  import * as path from 'path';
21
- import { loadAllJudgments } from './human-value-store.js';
21
+ import { loadAllJudgments, } from './human-value-store.js';
22
22
  // 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
23
23
  // 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
24
24
  // 跨平台: 优先 process.env.HOME (测试可控), 然后 os.homedir() (Windows 上读 USERPROFILE).
@@ -70,6 +70,8 @@ const TOOL_WHITELIST = new Set([
70
70
  'ui_save_component', 'ui_load_component',
71
71
  // 2026-08-12: Kanban 看板工具 (Hermes kanban_db → OrbitDB, src/orbitdb/kanban-store.ts)
72
72
  'kanban_create', 'kanban_list', 'kanban_get', 'kanban_claim', 'kanban_complete', 'kanban_status',
73
+ // 2026-10-14: 结构化记忆工具 (src/agents/memory-store.ts, P0)
74
+ 'memory_save', 'memory_recall',
73
75
  // 2026-08-07: Bolloon 自身配置读写 (bolloon-config.json, agent 有修改自身配置权限)
74
76
  'bolloon_config_get', 'bolloon_config_set',
75
77
  // 2026-09-13: 下一代工具集 (人机问答 / 精确补丁 / 代码执行 / 桌面操作 / 技能分享 / 浏览器)
@@ -257,18 +257,36 @@ export async function evaluateSetup(opts = {}) {
257
257
  checks.credentialPresent = hasKey;
258
258
  checks.providerUsable = providerSelected && credentialOk;
259
259
  checks.modelPresent = modelOk;
260
- // 3) 连通性 (24h 有效期)
260
+ // 3) 连通性 (2026-10-13 重构: 结论有效期 = 「测过 + 配置没变」, 不再只看墙钟 24h)
261
+ // - 旧逻辑: 24h 墙钟一到就过期, 即使 provider/key/baseUrl/model 一个没动 → 用户隔天启动必见
262
+ // 「未就绪」, 被迫重测 (体验债: 配置没变重测无意义, 且评估会把 connectivityOk 覆盖成 false 落盘,
263
+ // 于是「测过」这个事实被销毁, 更修不回来)。
264
+ // - 新语义: connectivityAt 存在(测过) 且 配置指纹一致(配置没变) ⇒ 结论仍有效; 只有配置真变了才重测。
261
265
  const connAt = checks.connectivityAt ? Date.parse(checks.connectivityAt) : 0;
262
- const connFresh = !!connAt && now - connAt < ttl;
266
+ const hadConnTest = !!connAt;
267
+ // 配置指纹: 当前配置 vs 上次通过时的配置 —— 相同则连通性结论仍有效 (provider/key/baseUrl/model 都没变)
268
+ let configUnchanged = false;
269
+ try {
270
+ const { hash: curHash } = await hashConfigFiles(bolloonHome);
271
+ configUnchanged = !!prev.configHash && prev.configHash === curHash;
272
+ }
273
+ catch {
274
+ configUnchanged = false;
275
+ }
263
276
  let connectivityOk = false;
264
- if (checks.connectivityOk && connFresh)
277
+ if (hadConnTest && configUnchanged) {
278
+ // 测过 + 配置没变 ⇒ 结论有效 (即使墙钟过了 24h)
265
279
  connectivityOk = true;
266
- else if (checks.connectivityOk && !connFresh) {
267
- reasons.push('连通性结果已过期 (>24h) → 需要重新测试');
280
+ checks.connectivityOk = true; // 救回被旧逻辑覆盖的 false
281
+ }
282
+ else if (hadConnTest && !configUnchanged) {
283
+ // 测过但配置变了 (或 configHash 缺失) ⇒ 需要重测
284
+ connectivityOk = false;
268
285
  checks.connectivityOk = false;
286
+ reasons.push('LLM 配置已变化 → 需要重新测试连通性');
269
287
  }
270
288
  if (!connectivityOk)
271
- why.basic.push(connAt && !connFresh ? '连通性结果过期 (需重测)' : '还没做过连通性测试');
289
+ why.basic.push(hadConnTest && !configUnchanged ? '配置变化, 连通性需重测' : '还没做过连通性测试');
272
290
  // 4) 运行时 (真实: initMinimax + session; 不是"singleton 存在")
273
291
  let runtimeOk = checks.runtimeInitialized === true;
274
292
  if (!opts.light) {
@@ -545,6 +563,16 @@ export async function recordSetupStage(stage, patch = {}, bolloonHome = resolveB
545
563
  lastError: undefined,
546
564
  updatedAt: new Date().toISOString(),
547
565
  };
566
+ // 2026-10-13 重构: 补算并落盘配置指纹 —— 与 evaluateSetup (413-414) 同一逻辑。
567
+ // 之前只有 evaluateSetup 落 configHash; 直写路径 (onboard 连通性成功 / 测试模拟) 缺 hash ⇒
568
+ // 连通性判定 (「测过且配置没变 → 结论有效」) 在非 evaluateSetup 路径下永远 configUnchanged=false。
569
+ // 行为不变 (configHash 仍是同一文件的 sha256), 只是所有写 state 的路径都带上指纹。
570
+ try {
571
+ const fp = await hashConfigFiles(bolloonHome);
572
+ state.configHash = fp.hash;
573
+ state.configVersion = fp.version;
574
+ }
575
+ catch { /* hash 计算失败不阻塞记录 */ }
548
576
  await writeSetupState(state, bolloonHome);
549
577
  return state;
550
578
  }
@@ -260,6 +260,9 @@ export class SocialHeartbeat {
260
260
  currentPeerDids = new Set();
261
261
  greetingMessage;
262
262
  ownIpfsCid;
263
+ // 2026-10-12: IPFS/IPNS 发布失败只提示一次 (本地没跑节点时每次心跳刷 warn)
264
+ _ipfsPublishWarned = false;
265
+ _ipnsPublishWarned = false;
263
266
  // 蚁群组件
264
267
  pheromoneEngine = null;
265
268
  adaptiveHeartbeat = null;
@@ -393,7 +396,7 @@ export class SocialHeartbeat {
393
396
  return;
394
397
  try {
395
398
  const { IpfsClient } = await import('@diap/sdk');
396
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
399
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
397
400
  console.log(`[Heartbeat] 从 IPNS 加载 persona: ${this.config.ownIpnsName}`);
398
401
  const cid = await ipfs.resolveIpns(this.config.ownIpnsName);
399
402
  if (cid) {
@@ -524,7 +527,7 @@ export class SocialHeartbeat {
524
527
  return;
525
528
  try {
526
529
  const { IpfsClient } = await import('@diap/sdk');
527
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
530
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
528
531
  const doc = {
529
532
  name: persona.name,
530
533
  description: persona.description,
@@ -541,6 +544,9 @@ export class SocialHeartbeat {
541
544
  const content = JSON.stringify(doc);
542
545
  const result = await ipfs.upload(content);
543
546
  const cid = typeof result === 'string' ? result : result?.cid;
547
+ // 2026-10-12 (leo: 「IPFS 发布失败刷屏 27 行」): 本地没跑 IPFS 节点时每次心跳都 warn。
548
+ // 成功一次就重置提示标志 —— 节点恢复后重新可提示。
549
+ this._ipfsPublishWarned = false;
544
550
  if (cid && cid !== this.ownIpfsCid) {
545
551
  this.ownIpfsCid = cid;
546
552
  console.log(`[Heartbeat] Persona 发布到 IPFS: ${cid}`);
@@ -550,13 +556,20 @@ export class SocialHeartbeat {
550
556
  console.log(`[Heartbeat] Persona 发布到 IPNS: ${this.config.ownIpnsName}`);
551
557
  }
552
558
  catch (err) {
553
- console.warn(`[Heartbeat] IPNS 发布失败:`, err);
559
+ if (!this._ipnsPublishWarned) {
560
+ this._ipnsPublishWarned = true;
561
+ console.warn('[Heartbeat] IPNS 发布失败:', err);
562
+ }
554
563
  }
555
564
  }
556
565
  }
557
566
  }
558
567
  catch (err) {
559
- console.warn('[Heartbeat] IPFS 发布失败:', err);
568
+ // 只提示第一次 (配置缺失 / 节点未跑是常态, 不该每个心跳刷一条)
569
+ if (!this._ipfsPublishWarned) {
570
+ this._ipfsPublishWarned = true;
571
+ console.warn('[Heartbeat] IPFS 发布失败:', err);
572
+ }
560
573
  }
561
574
  }
562
575
  async discoverPeers() {
@@ -652,7 +665,7 @@ export class SocialHeartbeat {
652
665
  return;
653
666
  try {
654
667
  const { IpfsClient } = await import('@diap/sdk');
655
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
668
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
656
669
  const cid = await ipfs.resolveIpns(agent.ipnsName);
657
670
  if (cid) {
658
671
  agent.cid = cid;
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema": "bolloon-mobile-web/1",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "channel": "stable",
5
5
  "build": "stable",
6
- "commit": "0d876c1b48ea86d5a390bd21b62468c8d2487fe0",
7
- "builtAt": "2026-10-06T07:04:38.761Z",
6
+ "commit": "94ac12e0c01b2136d9034a38ee1545efb21b23c1",
7
+ "builtAt": "2026-10-16T19:05:31.069Z",
8
8
  "layer": "web",
9
9
  "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
10
  }