@bolloon/bolloon-agent 0.6.4 → 0.6.6

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 (173) 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/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-inbox.js +147 -0
  15. package/dist/agents/opportunity-match.js +48 -7
  16. package/dist/agents/opportunity-web-search.js +222 -0
  17. package/dist/agents/p2p-chat-tools.js +1 -2
  18. package/dist/agents/parse-tool-call.js +9 -22
  19. package/dist/agents/pi-harness.js +1 -2
  20. package/dist/agents/pi-sdk-tools.js +180 -5
  21. package/dist/agents/pi-sdk.js +143 -9
  22. package/dist/agents/process-runner.js +28 -9
  23. package/dist/agents/react-loop.js +9 -17
  24. package/dist/agents/runner-resolver.js +4 -3
  25. package/dist/agents/skill-loader.js +22 -15
  26. package/dist/agents/skills-manager.js +1 -25
  27. package/dist/agents/workflow-pivot-loop.js +152 -61
  28. package/dist/agents/world-watcher.js +30 -1
  29. package/dist/bootstrap/context-collector.js +2 -4
  30. package/dist/bootstrap/context-hierarchy.js +13 -1
  31. package/dist/cli/ink-app.js +44 -17
  32. package/dist/cli/intent-command.js +22 -12
  33. package/dist/cli/loading-tui.js +8 -2
  34. package/dist/cli/log-gate.js +40 -8
  35. package/dist/cli/mention-data.js +1 -0
  36. package/dist/cli/reply-hygiene.js +22 -0
  37. package/dist/cli/trace-line.js +61 -2
  38. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  39. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  40. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  41. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  42. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  44. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  45. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  46. package/dist/electron-build/agents/contacts/tools.js +112 -0
  47. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  48. package/dist/electron-build/agents/experience-review.js +0 -3
  49. package/dist/electron-build/agents/experience-review.js.map +1 -1
  50. package/dist/electron-build/agents/external-events.js.map +1 -1
  51. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  52. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  53. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  54. package/dist/electron-build/agents/goal-resume.js +0 -4
  55. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  56. package/dist/electron-build/agents/intent-classifier.js +1 -1
  57. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  58. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  59. package/dist/electron-build/agents/loop-review.js +2 -2
  60. package/dist/electron-build/agents/loop-review.js.map +1 -1
  61. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  62. package/dist/electron-build/agents/memory-store.js +404 -0
  63. package/dist/electron-build/agents/memory-store.js.map +1 -0
  64. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  65. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  66. package/dist/electron-build/agents/pi-harness.js +1 -2
  67. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  68. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  69. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk.js +143 -9
  71. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  72. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  73. package/dist/electron-build/agents/process-runner.js +28 -9
  74. package/dist/electron-build/agents/process-runner.js.map +1 -1
  75. package/dist/electron-build/agents/runner-resolver.js +4 -3
  76. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  77. package/dist/electron-build/agents/skill-loader.js +22 -16
  78. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  79. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  80. package/dist/electron-build/agents/skills-manager.js +0 -24
  81. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  82. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  83. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  84. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  85. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  86. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  87. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  88. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  89. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  90. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  91. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  92. package/dist/electron-build/electron-preload.js.map +1 -1
  93. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  94. package/dist/electron-build/kernel/channel-actor.js +31 -2
  95. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  96. package/dist/electron-build/kernel/model-runtime.js +8 -1
  97. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  98. package/dist/electron-build/llm/config-store.js +20 -0
  99. package/dist/electron-build/llm/config-store.js.map +1 -1
  100. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  101. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  102. package/dist/electron-build/llm/pi-ai.js +96 -35
  103. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  104. package/dist/electron-build/network/agent-network.js.map +1 -1
  105. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  106. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  107. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  108. package/dist/electron-build/security/tool-gate.js +2 -0
  109. package/dist/electron-build/security/tool-gate.js.map +1 -1
  110. package/dist/electron-build/setup/setup-store.js +34 -6
  111. package/dist/electron-build/setup/setup-store.js.map +1 -1
  112. package/dist/electron-build/social/heartbeat.js +18 -5
  113. package/dist/electron-build/social/heartbeat.js.map +1 -1
  114. package/dist/electron-build/utils/update-manager.js.map +1 -1
  115. package/dist/index.js +250 -12
  116. package/dist/ios/bolloon-web.json +3 -3
  117. package/dist/ios/client.js +150 -113
  118. package/dist/ios/icons/icon-1400x1400.png +0 -0
  119. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  120. package/dist/ios/index.html +106 -107
  121. package/dist/ios/mobile-agent.js +346 -45
  122. package/dist/ios/mobile-core.js +7303 -4590
  123. package/dist/ios/mobile-harness.js +1004 -0
  124. package/dist/ios/mobile-orbitdb.js +194 -0
  125. package/dist/ios/mobile-persona.js +80 -0
  126. package/dist/ios/mobile-privacy.js +2 -3
  127. package/dist/ios/mobile.css +286 -21
  128. package/dist/ios/mobile.html +106 -107
  129. package/dist/ios/mobile.js +300 -132
  130. package/dist/ios/routes-world.js +58 -1
  131. package/dist/ios/server.js +3 -1
  132. package/dist/ios/style.css +49 -2
  133. package/dist/ios/sw.js +1 -1
  134. package/dist/ios/ui/message-renderer.js +4 -18
  135. package/dist/ios/ui/step-timeline.js +14 -1
  136. package/dist/kernel/channel-actor.js +30 -2
  137. package/dist/kernel/gate-scan.js +1 -1
  138. package/dist/kernel/index.js +33 -0
  139. package/dist/kernel/model-runtime.js +8 -1
  140. package/dist/kernel/plan.js +3 -3
  141. package/dist/kernel/roster.js +3 -3
  142. package/dist/llm/config-store.js +20 -0
  143. package/dist/llm/embedding-adapter.js +69 -0
  144. package/dist/llm/pi-ai.js +94 -34
  145. package/dist/orbitdb/memory-sync.js +70 -0
  146. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  147. package/dist/security/tool-gate.js +2 -0
  148. package/dist/setup/setup-store.js +34 -6
  149. package/dist/social/heartbeat.js +18 -5
  150. package/dist/web/bolloon-web.json +3 -3
  151. package/dist/web/client.js +150 -113
  152. package/dist/web/icons/icon-1400x1400.png +0 -0
  153. package/dist/web/icons/icon-1400x1400.webp +0 -0
  154. package/dist/web/index.html +52 -59
  155. package/dist/web/mobile-agent.js +351 -45
  156. package/dist/web/mobile-contacts.js +0 -4
  157. package/dist/web/mobile-core.js +7430 -4590
  158. package/dist/web/mobile-harness.js +1004 -0
  159. package/dist/web/mobile-orbitdb.js +194 -0
  160. package/dist/web/mobile-persona.js +80 -0
  161. package/dist/web/mobile-privacy.js +2 -3
  162. package/dist/web/mobile.css +286 -21
  163. package/dist/web/mobile.html +121 -107
  164. package/dist/web/mobile.js +409 -136
  165. package/dist/web/routes-contacts.js +94 -1
  166. package/dist/web/routes-world.js +58 -1
  167. package/dist/web/server.js +4 -2
  168. package/dist/web/style.css +49 -2
  169. package/dist/web/sw.js +1 -1
  170. package/dist/web/ui/message-renderer.js +4 -18
  171. package/dist/web/ui/step-timeline.js +14 -1
  172. package/package.json +3 -2
  173. package/scripts/build-web.ts +16 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * memory-store.ts — 结构化记忆条目 (P0, 2026-10-14, decentralized-memory-design §2)
3
+ *
4
+ * 与 memory-recall.ts (会话摘要召回) 互补: 摘要文件是「整段会话的压缩」,
5
+ * 这里是「结构化单条记忆」—— kind / importance / refs 证据链, 可单独召回/删除/固化。
6
+ *
7
+ * P0 范围 (小步, 零新依赖):
8
+ * - MemoryEntry 模型 + 原子 JSONL 持久化 (~/.bolloon/memory/<agentId>/entries.jsonl)
9
+ * - save (自动 importance 启发式打分) / recall (关键词 + importance 加权) / remove
10
+ * - 无 embedding (P1 接), 无 OrbitDB 复制 (P3 接)
11
+ *
12
+ * 借鉴 Hermes (decentralized-memory-design §1.5):
13
+ * - 写入落盘, prompt 注入走「每轮召回」快照, 不实时重建 (冻结快照 ≠ 实时写)
14
+ * - 条目摘要进上下文, 全文按需拉取 (progressive disclosure)
15
+ */
16
+ import * as fs from 'fs/promises';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+ import { createHash } from 'node:crypto';
20
+ import { cosineSimilarity } from '../llm/embedding-adapter.js';
21
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
22
+ export function memoryEntriesPath(agentId, homeDir) {
23
+ const base = homeDir || home();
24
+ return path.join(base, '.bolloon', 'memory', agentId, 'entries.jsonl');
25
+ }
26
+ /** 空内容/纯空白 → 拒绝存 (与 evaluateQuality 同口径: 空串不是记忆) */
27
+ function validContent(content) {
28
+ return String(content || '').trim().length >= 2;
29
+ }
30
+ /** importance 启发式: 长度 + 特定信号词 (P0 无 LLM 打分, 不引依赖) */
31
+ export function heuristicImportance(content, kind = 'fact', manual) {
32
+ if (typeof manual === 'number')
33
+ return Math.min(1, Math.max(0, manual));
34
+ const t = String(content || '');
35
+ let score = 0.3; // 基线
36
+ if (t.length > 100)
37
+ score += 0.2; // 长记忆 = 更可能是重要细节
38
+ if (/喜欢|偏好|讨厌|禁止|必须|永远|从不|重要|注意|记住|约定/.test(t))
39
+ score += 0.25; // 用户强语义
40
+ if (kind === 'preference' || kind === 'relationship')
41
+ score += 0.2;
42
+ if (kind === 'skill-feedback')
43
+ score += 0.15;
44
+ return Math.min(1, score);
45
+ }
46
+ /**
47
+ * P2: 价值衰减 (decay) — 长期未被召回的条目降权标记, 不删除。
48
+ * 规则:
49
+ * - 固化条目 (crystallized) 豁免 (用户确认过/多次命中, 不淡忘)
50
+ * - 从 lastRecalledAt (未召回过 = createdAt) 起算, 超过 days 天 → decayed=true + importance×factor
51
+ * - 被召回 → decayed 撤销 (recall 里已处理)
52
+ * 纯函数: 不改 entry, 返回更新后的副本 (无变化返回原引用)。
53
+ */
54
+ export function applyDecay(entry, nowIso, opts = {}) {
55
+ if (entry.crystallized)
56
+ return entry; // 固化豁免
57
+ const days = opts.days ?? 30;
58
+ const factor = opts.factor ?? 0.5;
59
+ const base = entry.lastRecalledAt || entry.createdAt;
60
+ const ms = Date.parse(nowIso) - Date.parse(base);
61
+ if (!Number.isFinite(ms) || ms < 0)
62
+ return entry;
63
+ const elapsedDays = ms / 86_400_000;
64
+ if (elapsedDays < days)
65
+ return entry;
66
+ // 幂等: 已淡化过就不再重复降 (decayed 标记本身是终态)
67
+ if (entry.decayed)
68
+ return entry;
69
+ return {
70
+ ...entry,
71
+ importance: Math.max(0.05, entry.importance * factor),
72
+ decayed: true,
73
+ updatedAt: nowIso,
74
+ };
75
+ }
76
+ /** P2: 全库跑一次 decay, 返回被淡化条数 (未调用过 save/recall 的旧条目也会被标) */
77
+ export function decayAll(entries, nowIso, opts = {}) {
78
+ let n = 0;
79
+ for (const [id, e] of entries) {
80
+ const after = applyDecay(e, nowIso, opts);
81
+ if (after !== e) {
82
+ entries.set(id, after);
83
+ n++;
84
+ }
85
+ }
86
+ return n;
87
+ }
88
+ /** 关键词提取 (与 memory-recall.tokenizeQuery 同口径: 中文 2-gram + 英文分词) */
89
+ function tokenize(text) {
90
+ const clean = String(text || '').toLowerCase().trim();
91
+ if (!clean)
92
+ return [];
93
+ const STOP = new Set(['的', '了', '是', '我', '你', '他', '她', '它', '我们', '你们', '在', '和', '与', '吗', '呢', '吧', '这', '那', '个', '请', '帮', '一下', '一个', '怎么', '如何', 'the', 'a', 'an', 'is', 'are', 'to', 'of', 'for', 'and']);
94
+ const out = [];
95
+ // 英文词
96
+ for (const w of clean.match(/[a-z][a-z0-9_]+/g) || []) {
97
+ if (!STOP.has(w))
98
+ out.push(w);
99
+ }
100
+ // 中文 2-gram
101
+ const cjk = (clean.match(/[\u4e00-\u9fff]+/g) || []).join('');
102
+ for (let i = 0; i < cjk.length - 1; i++) {
103
+ const g = cjk.slice(i, i + 2);
104
+ if (!STOP.has(g))
105
+ out.push(g);
106
+ }
107
+ return out;
108
+ }
109
+ /** 词 → 分数 (词面命中 1 + 干净长词加权, 不引 TF-IDF, P1 换向量) */
110
+ function scoreText(text, tokens) {
111
+ const t = String(text || '').toLowerCase();
112
+ let score = 0;
113
+ for (const tok of tokens) {
114
+ if (t.includes(tok))
115
+ score += tok.length >= 4 ? 1.2 : 1;
116
+ }
117
+ return score;
118
+ }
119
+ /**
120
+ * MemoryStore — 单 agent 的结构化记忆 (每实例对应一个 agentId, 不跨 agent)
121
+ *
122
+ * 文件锁: fcntl 不可用 (Windows) 时退化为无锁 (与 Hermes 同思路, 原子 rename 兜底)。
123
+ * 防并发写坏: 写入 → tmp 文件 → rename 原子替换。
124
+ */
125
+ export class MemoryStore {
126
+ agentId;
127
+ file;
128
+ entries = new Map();
129
+ constructor(agentId, homeDir) {
130
+ this.agentId = agentId;
131
+ this.file = memoryEntriesPath(agentId, homeDir);
132
+ }
133
+ async load() {
134
+ try {
135
+ const raw = await fs.readFile(this.file, 'utf8');
136
+ this.entries.clear();
137
+ for (const line of raw.split('\n')) {
138
+ if (!line.trim())
139
+ continue;
140
+ try {
141
+ const e = JSON.parse(line);
142
+ if (e?.id)
143
+ this.entries.set(e.id, e);
144
+ }
145
+ catch { /* 单行损坏跳过 (append-only, 不整文件作废) */ }
146
+ }
147
+ }
148
+ catch { /* 文件不存在 = 空记忆 */ }
149
+ }
150
+ async persist() {
151
+ const dir = path.dirname(this.file);
152
+ await fs.mkdir(dir, { recursive: true });
153
+ const tmp = `${this.file}.tmp-${process.pid}`;
154
+ const lines = [...this.entries.values()].map((e) => JSON.stringify(e));
155
+ await fs.writeFile(tmp, lines.join('\n') + (lines.length ? '\n' : ''));
156
+ await fs.rename(tmp, this.file); // 原子替换, 并发读不碰半截文件
157
+ }
158
+ async save(input, adapter) {
159
+ const content = String(input.content || '').trim();
160
+ if (!validContent(content))
161
+ throw new Error('记忆内容太短 (至少 2 个字符)');
162
+ const now = new Date().toISOString();
163
+ const id = createHash('sha256').update(`${this.agentId}|${content}`).digest('hex').slice(0, 32);
164
+ const existing = this.entries.get(id);
165
+ let vec = existing?.vec;
166
+ if (adapter && adapter.provider !== 'none') {
167
+ try {
168
+ vec = await adapter.embed(content); // 内容变 → 重算; 内容同 → 复用旧向量
169
+ }
170
+ catch {
171
+ // 向量化失败不阻塞保存 (记忆仍存, 召回走关键词)
172
+ }
173
+ }
174
+ const entry = {
175
+ id,
176
+ agentId: this.agentId,
177
+ content,
178
+ kind: input.kind || existing?.kind || 'fact',
179
+ importance: heuristicImportance(content, input.kind || existing?.kind, input.importance),
180
+ createdAt: existing?.createdAt || now,
181
+ updatedAt: now,
182
+ source: input.source || existing?.source || 'manual',
183
+ refs: input.refs || existing?.refs || [],
184
+ crystallized: existing?.crystallized || false,
185
+ recallCount: existing?.recallCount || 0,
186
+ lastRecalledAt: existing?.lastRecalledAt,
187
+ decayed: existing?.decayed,
188
+ vec,
189
+ };
190
+ this.entries.set(id, entry);
191
+ await this.persist();
192
+ return entry;
193
+ }
194
+ /**
195
+ * 召回:
196
+ * - queryVec 提供 且 条目有 vec → cosine 相似度 (语义召回), 按 importance 加权
197
+ * - 否则 → 关键词命中 (P0 行为), 完全兼容旧调用
198
+ * 命中后 recallCount++ 并落盘 (动态升权)
199
+ */
200
+ async recall(opts) {
201
+ if (this.entries.size === 0)
202
+ return [];
203
+ const limit = opts.limit || 3;
204
+ const hits = [];
205
+ const qv = opts.queryVec;
206
+ const useVec = !!qv && qv.length > 0;
207
+ if (useVec) {
208
+ const minCosine = opts.minCosine ?? 0;
209
+ for (const e of this.entries.values()) {
210
+ if (!e.vec || e.vec.length !== qv.length)
211
+ continue; // 无向量的旧条目跳过
212
+ const c = cosineSimilarity(qv, e.vec);
213
+ if (c < minCosine)
214
+ continue;
215
+ const weighted = c * (0.4 + 0.6 * e.importance); // importance 加权
216
+ if (weighted > 0)
217
+ hits.push({ entry: e, score: weighted });
218
+ }
219
+ }
220
+ else {
221
+ const tokens = tokenize(opts.query);
222
+ if (tokens.length === 0)
223
+ return [];
224
+ const minScore = opts.minScore ?? 0;
225
+ for (const e of this.entries.values()) {
226
+ const s = scoreText(e.content, tokens);
227
+ if (s <= minScore)
228
+ continue;
229
+ const weighted = Math.min(5, s) * (0.4 + 0.6 * e.importance); // importance 加权
230
+ hits.push({ entry: e, score: weighted });
231
+ }
232
+ }
233
+ hits.sort((a, b) => b.score - a.score);
234
+ const top = hits.slice(0, limit);
235
+ // 命中 → recallCount++ + lastRecalledAt 刷新 (动态升权 / decay 重置), 每 10 次 importance +0.05
236
+ if (top.length > 0) {
237
+ const nowIso = new Date().toISOString();
238
+ let changed = false;
239
+ for (const h of top) {
240
+ h.entry.recallCount++;
241
+ h.entry.lastRecalledAt = nowIso;
242
+ if (h.entry.decayed)
243
+ h.entry.decayed = false; // 被召回 → 淡化撤销
244
+ if (h.entry.recallCount % 10 === 0 && h.entry.importance < 1) {
245
+ h.entry.importance = Math.min(1, h.entry.importance + 0.05);
246
+ }
247
+ changed = true;
248
+ }
249
+ if (changed)
250
+ await this.persist().catch(() => { });
251
+ }
252
+ return top;
253
+ }
254
+ /** P2: 固化 — 多次召回或用户确认后调用; crystallized=true (进 08-Insights 由调用方负责) */
255
+ async crystallize(id) {
256
+ const e = this.entries.get(id);
257
+ if (!e)
258
+ return null;
259
+ const changed = !e.crystallized;
260
+ e.crystallized = true;
261
+ e.importance = Math.min(1, e.importance + 0.1); // 固化 → importance 加成
262
+ e.updatedAt = new Date().toISOString();
263
+ if (changed)
264
+ await this.persist();
265
+ return e;
266
+ }
267
+ /** P2: 自动固化阈值 — 召回 ≥ 5 次且 importance ≥ 0.6 的条目 (recall 后可调用, 返回新固化条数) */
268
+ async autoCrystallize() {
269
+ let n = 0;
270
+ for (const e of this.entries.values()) {
271
+ if (!e.crystallized && e.recallCount >= 5 && e.importance >= 0.6) {
272
+ e.crystallized = true;
273
+ e.importance = Math.min(1, e.importance + 0.1);
274
+ e.updatedAt = new Date().toISOString();
275
+ n++;
276
+ }
277
+ }
278
+ if (n > 0)
279
+ await this.persist();
280
+ return n;
281
+ }
282
+ /** P2: 统计 (decay 标定 / 审计用) */
283
+ stats() {
284
+ let crystallized = 0;
285
+ let decayed = 0;
286
+ for (const e of this.entries.values()) {
287
+ if (e.crystallized)
288
+ crystallized++;
289
+ if (e.decayed)
290
+ decayed++;
291
+ }
292
+ return { total: this.entries.size, crystallized, decayed };
293
+ }
294
+ /**
295
+ * P3: 合并远端条目 (OrbitDB 拉回的) 到本地, LWW (updatedAt 大者胜)。
296
+ * 返回被采纳的远端条数; 有采纳则落盘。同步前本地必须先 load()。
297
+ */
298
+ async mergeRemote(remote) {
299
+ let n = 0;
300
+ for (const r of remote) {
301
+ if (!r || !r.id || r.agentId !== this.agentId)
302
+ continue; // 归属校验: 只合并本 agent
303
+ const l = this.entries.get(r.id);
304
+ if (!l) {
305
+ this.entries.set(r.id, r);
306
+ n++;
307
+ }
308
+ else if (r.updatedAt > l.updatedAt) {
309
+ this.entries.set(r.id, r);
310
+ n++;
311
+ }
312
+ // 平手 / 本地更新 → 本地胜 (LWW)
313
+ }
314
+ if (n > 0)
315
+ await this.persist();
316
+ return n;
317
+ }
318
+ /** P3: 导出一份「可在其他节点重建」的条目快照 (不含运行时计数, 只含持久事实) */
319
+ exportEntries() {
320
+ return [...this.entries.values()].map((e) => ({ ...e, vec: undefined })); // vec 不跨节点 (维度依赖 adapter)
321
+ }
322
+ async remove(id) {
323
+ const existed = this.entries.delete(id);
324
+ if (existed)
325
+ await this.persist();
326
+ return existed;
327
+ }
328
+ async list(limit = 50) {
329
+ return [...this.entries.values()]
330
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
331
+ .slice(0, limit);
332
+ }
333
+ }
334
+ /** 单例 (per-agent 分桶, 与 getContextStore 同模式); 测试可传 homeDir */
335
+ const stores = new Map();
336
+ export function getMemoryStore(agentId, homeDir) {
337
+ const key = `${homeDir || home()}|${agentId}`;
338
+ let s = stores.get(key);
339
+ if (!s) {
340
+ s = new MemoryStore(agentId, homeDir);
341
+ stores.set(key, s);
342
+ }
343
+ return s;
344
+ }
345
+ /** 召回 → 拼注入文本 (progressive disclosure: 只带摘要级内容 + id, 全文按需 load) */
346
+ export function formatRecallBlock(hits) {
347
+ if (!hits.length)
348
+ return '';
349
+ const lines = hits.map((h, i) => {
350
+ const e = h.entry;
351
+ const tag = `${e.kind}${e.crystallized ? '★' : ''}`;
352
+ return `[${i + 1}] (${tag}, 权重 ${e.importance.toFixed(2)}) ${e.content.slice(0, 200)}`;
353
+ });
354
+ return `<memory-context>\n${lines.join('\n')}\n</memory-context>`;
355
+ }
356
+ /** 测试辅助: 清空分桶 (隔离 HOME 时用) */
357
+ export function resetMemoryStores() {
358
+ stores.clear();
359
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * opportunity-inbox.ts — 机会信箱 (2026-10-05, leo: 别人主动投来的机会)
3
+ *
4
+ * 设计: 外部 agent 用 DIAP Ed25519 身份签名投递机会声明到本机信箱,
5
+ * 验签通过才入库 (~/.bolloon/world/inbox/<id>.json) 并进入世界流。
6
+ * 来源可验: 每条机会带 provider DID + 验签状态, 不再是「本机自产」的虚拟感。
7
+ *
8
+ * 协议: bolloon-opportunity-inbox/1
9
+ * POST payload = { title, summary, refs[], provider: {did}, issuedAt }
10
+ * signature = ed25519Sign(privateKey, canonicalize(payload))
11
+ * 验证 = ed25519Verify(providerPublicKey, canonicalize(payload), signature)
12
+ *
13
+ * 未来: 群复制 (OrbitDB 公共群 announce) 是信箱的另一条投递通道, 需要第二节点 (诚实边界)。
14
+ */
15
+ import * as fs from 'fs/promises';
16
+ import * as path from 'path';
17
+ import * as crypto from 'node:crypto';
18
+ import { worldDir } from './intent-store.js';
19
+ import { canonicalize, ed25519Verify } from './x402/paid-info-protocol.js';
20
+ export const INBOX_PROTOCOL = 'bolloon-opportunity-inbox/1';
21
+ const inboxDir = () => path.join(worldDir(), 'inbox');
22
+ function safeId(s) {
23
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
24
+ }
25
+ /** 把投递载荷按签名标准序列化 (必须与签名时完全一致) */
26
+ export function canonicalPayload(p) {
27
+ return canonicalize({
28
+ protocol: p.protocol,
29
+ title: p.title,
30
+ summary: p.summary,
31
+ refs: p.refs,
32
+ provider: p.provider,
33
+ issuedAt: p.issuedAt,
34
+ });
35
+ }
36
+ /** 投递方 DID 的 Ed25519 公钥解析 (did:key 格式 → 32 字节 hex) */
37
+ export async function resolveDidPublicKey(did) {
38
+ try {
39
+ // did:key:z6Mk... — multibase base58btc 编码: [multicodec 0xed01(2B) + 32B 公钥] = 34 字节
40
+ const m = String(did || '').match(/^did:key:z([1-9A-HJ-NP-Za-km-z]+)$/);
41
+ if (!m)
42
+ return null;
43
+ const decoded = Buffer.from(importB58(m[1]));
44
+ if (decoded.length === 34 && decoded[0] === 0xed && decoded[1] === 0x01) {
45
+ return decoded.subarray(2).toString('hex'); // 剥 multicodec, 取 32 字节公钥
46
+ }
47
+ if (decoded.length === 32)
48
+ return decoded.toString('hex'); // 裸 32 字节兜底
49
+ return null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /** base58btc 解码 (multibase 'z' 前缀 = base58btc) — 只依赖 Node crypto 的 b58 简易实现 */
56
+ function importB58(s) {
57
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
58
+ const base = BigInt(58);
59
+ let num = BigInt(0);
60
+ for (const ch of s) {
61
+ const idx = ALPHABET.indexOf(ch);
62
+ if (idx < 0)
63
+ throw new Error('bad base58 char');
64
+ num = num * base + BigInt(idx);
65
+ }
66
+ const bytes = [];
67
+ while (num > 0n) {
68
+ bytes.unshift(Number(num & 0xffn));
69
+ num >>= 8n;
70
+ }
71
+ // 前导 '1' → 前导 0x00
72
+ let leading = 0;
73
+ for (const ch of s) {
74
+ if (ch === '1')
75
+ leading++;
76
+ else
77
+ break;
78
+ }
79
+ return Uint8Array.from([...new Array(leading).fill(0), ...bytes]);
80
+ }
81
+ /** 投递机会到信箱 (验签通过才入库) */
82
+ export async function deliverOpportunity(payload, signatureB64) {
83
+ if (payload.protocol !== INBOX_PROTOCOL)
84
+ return { ok: false, error: `协议不匹配: ${payload.protocol}` };
85
+ const title = String(payload.title || '').trim();
86
+ if (!title)
87
+ return { ok: false, error: '缺 title' };
88
+ const did = String(payload.provider?.did || '').trim();
89
+ if (!did)
90
+ return { ok: false, error: '缺 provider.did' };
91
+ // 1. 验签 (能解析公钥才可能 verified)
92
+ const pub = await resolveDidPublicKey(did);
93
+ let verification;
94
+ if (!pub) {
95
+ verification = 'unverified'; // 公钥解析不了 → 不入库 (不可信来源不污染世界流)
96
+ return { ok: false, error: '无法解析 provider DID 公钥 (拒绝不可信来源)' };
97
+ }
98
+ const okSig = await ed25519Verify(pub, canonicalPayload(payload), signatureB64);
99
+ if (!okSig) {
100
+ verification = 'signature-invalid';
101
+ return { ok: false, error: '签名无效 (拒绝伪造投递)' };
102
+ }
103
+ verification = 'verified';
104
+ // 2. 入库 (一机会一文件)
105
+ const id = `inbox_${safeId(crypto.createHash('sha256').update(canonicalPayload(payload)).digest('hex').slice(0, 16))}`;
106
+ const opp = {
107
+ protocol: payload.protocol,
108
+ id,
109
+ title,
110
+ summary: String(payload.summary || '').trim(),
111
+ refs: Array.isArray(payload.refs) ? payload.refs.map(String).filter(Boolean) : [],
112
+ provider: { did, name: payload.provider?.name },
113
+ issuedAt: payload.issuedAt,
114
+ verification,
115
+ receivedAt: Date.now(),
116
+ };
117
+ try {
118
+ await fs.mkdir(inboxDir(), { recursive: true });
119
+ await fs.writeFile(path.join(inboxDir(), `${id}.json`), JSON.stringify(opp, null, 2) + '\n', { mode: 0o600 });
120
+ }
121
+ catch (err) {
122
+ return { ok: false, error: `入库失败: ${err instanceof Error ? err.message : err}` };
123
+ }
124
+ return { ok: true, opportunity: opp };
125
+ }
126
+ /** 列出信箱机会 (verified 优先, 最近在前) */
127
+ export async function listInboxOpportunities() {
128
+ try {
129
+ const files = await fs.readdir(inboxDir());
130
+ const out = [];
131
+ for (const f of files) {
132
+ if (!f.endsWith('.json'))
133
+ continue;
134
+ try {
135
+ const raw = await fs.readFile(path.join(inboxDir(), f), 'utf-8');
136
+ const p = JSON.parse(raw);
137
+ if (p && p.id)
138
+ out.push(p);
139
+ }
140
+ catch { /* 坏文件跳过 */ }
141
+ }
142
+ return out.sort((a, b) => b.receivedAt - a.receivedAt);
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }
@@ -134,7 +134,52 @@ function candidateOf(entry, intentId, score, reason, matchTags) {
134
134
  * · 无意图: 流入最近 open 公告 (新鲜度优先) —— 打开即有内容
135
135
  * · 有意图: 匹配(`match`)优先浮上来, 其余世界变化按新鲜度垫底 (世界不停)
136
136
  * · 已忽略的同源公告不流入 (随交互越来越准)
137
+ * · 机会信箱 (外部 agent 投递, 验签通过) 恒优先 —— 真实可信来源
137
138
  */
139
+ async function mergeInboxOnTop(all) {
140
+ const { listInboxOpportunities } = await import('./opportunity-inbox.js');
141
+ const inboxOpps = await listInboxOpportunities();
142
+ const inboxCards = inboxOpps
143
+ .filter((o) => o.verification === 'verified') // 只放行验签通过的
144
+ .map((o) => ({
145
+ id: o.id,
146
+ intentId: null,
147
+ source: 'inbox',
148
+ sourceId: o.id,
149
+ score: 0.9,
150
+ reason: 'inbox',
151
+ title: o.title,
152
+ summary: o.summary,
153
+ budget: null,
154
+ matchTags: [],
155
+ createdAt: o.receivedAt,
156
+ inbox: { providerDid: o.provider.did, providerName: o.provider.name, verification: o.verification, refs: o.refs },
157
+ }));
158
+ for (const c of inboxCards.slice(0, 10)) {
159
+ void noteObservation({ ts: Date.now(), kind: 'opportunity-seen', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'inbox' });
160
+ }
161
+ // 2026-10-05: AI 主动上网搜到的机会 (真 URL, 可点开核实) — 排信箱后、本地源前
162
+ const { listSearchOpportunities } = await import('./opportunity-web-search.js');
163
+ const searchOpps = await listSearchOpportunities();
164
+ const searchCards = searchOpps.map((o) => ({
165
+ id: o.id,
166
+ intentId: null,
167
+ source: 'search',
168
+ sourceId: o.id,
169
+ score: 0.8,
170
+ reason: 'search',
171
+ title: o.title,
172
+ summary: o.summary,
173
+ budget: null,
174
+ matchTags: [],
175
+ createdAt: o.searchedAt,
176
+ search: { url: o.url, source: o.source, query: o.query },
177
+ }));
178
+ for (const c of searchCards.slice(0, 10)) {
179
+ void noteObservation({ ts: Date.now(), kind: 'opportunity-seen', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'search' });
180
+ }
181
+ return [...inboxCards, ...searchCards, ...all]; // 信箱 > 搜索 > 本地
182
+ }
138
183
  export async function scanOpportunities(opts = {}) {
139
184
  const limit = opts.limit ?? 50;
140
185
  const minScore = opts.minScore ?? 0.2;
@@ -184,7 +229,7 @@ export async function scanOpportunities(opts = {}) {
184
229
  for (const c of all.filter((x) => x.reason === 'match').slice(0, 10)) {
185
230
  void noteObservation({ ts: Date.now(), kind: 'opportunity-seen', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'match' });
186
231
  }
187
- return { ok: true, results: all.slice(0, limit) };
232
+ return { ok: true, results: (await mergeInboxOnTop(all)).slice(0, limit) };
188
233
  }
189
234
  // 默认世界流: 新鲜度优先
190
235
  const sorted = [...openEntries].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
@@ -198,7 +243,7 @@ export async function scanOpportunities(opts = {}) {
198
243
  for (const c of out.slice(0, 10)) {
199
244
  void noteObservation({ ts: Date.now(), kind: 'world-change', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'world' });
200
245
  }
201
- return { ok: true, results: out.slice(0, limit) };
246
+ return { ok: true, results: (await mergeInboxOnTop(out)).slice(0, limit) };
202
247
  }
203
248
  // 有意图: 匹配打分, match 优先; 世界变化不消失
204
249
  const matches = [];
@@ -229,11 +274,7 @@ export async function scanOpportunities(opts = {}) {
229
274
  return rank(a.reason) - rank(b.reason);
230
275
  return b.score - a.score;
231
276
  });
232
- // 世界观察: 高分匹配 = AI 主动看到的机会 (自动记 memory)
233
- for (const c of all.filter((x) => x.reason === 'match').slice(0, 10)) {
234
- void noteObservation({ ts: Date.now(), kind: 'opportunity-seen', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'match' });
235
- }
236
- return { ok: true, results: all.slice(0, limit) };
277
+ return { ok: true, results: (await mergeInboxOnTop(all)).slice(0, limit) };
237
278
  }
238
279
  /** 对单个意图即时匹配 (list --intent 用; 不走反馈过滤) */
239
280
  export async function matchOneIntentText(text, tags, budget, opts = {}) {