@bolloon/bolloon-agent 0.6.6 → 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 (52) hide show
  1. package/dist/agents/ha-daemon.js +85 -0
  2. package/dist/agents/ha-supervisor.js +146 -0
  3. package/dist/agents/memory-store.js +25 -0
  4. package/dist/agents/opportunity-match.js +15 -3
  5. package/dist/agents/pi-sdk-session-manager.js +8 -0
  6. package/dist/agents/pi-sdk.js +67 -494
  7. package/dist/bootstrap/context-collector.js +65 -3
  8. package/dist/bootstrap/persona-loader.js +26 -0
  9. package/dist/electron-build/agents/memory-store.js +26 -0
  10. package/dist/electron-build/agents/memory-store.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  12. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  13. package/dist/electron-build/agents/pi-sdk.js +65 -492
  14. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  15. package/dist/electron-build/bootstrap/context-collector.js +66 -3
  16. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  17. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  18. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  19. package/dist/electron-build/llm/pi-ai.js +16 -3
  20. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  21. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  22. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  23. package/dist/ios/bolloon-web.json +3 -3
  24. package/dist/ios/index.html +19 -4
  25. package/dist/ios/mobile-agent.js +267 -41
  26. package/dist/ios/mobile-contacts.js +0 -4
  27. package/dist/ios/mobile-core.js +806 -30
  28. package/dist/ios/mobile-memory.js +317 -0
  29. package/dist/ios/mobile.html +19 -4
  30. package/dist/ios/mobile.js +144 -30
  31. package/dist/ios/routes-contacts.js +94 -1
  32. package/dist/ios/routes-world.js +9 -1
  33. package/dist/ios/server.js +51 -40
  34. package/dist/ios/sw.js +1 -1
  35. package/dist/kernel/gate-scan.js +1 -1
  36. package/dist/kernel/ha/ha-arbiter.js +177 -0
  37. package/dist/kernel/ha/ha-guard.js +151 -0
  38. package/dist/kernel/index.js +3 -0
  39. package/dist/kernel/plan.js +5 -5
  40. package/dist/kernel/roster.js +3 -3
  41. package/dist/llm/pi-ai.js +16 -3
  42. package/dist/llm/system-prompt/registry.js +8 -6
  43. package/dist/web/bolloon-web.json +3 -3
  44. package/dist/web/mobile-agent.js +265 -44
  45. package/dist/web/mobile-core.js +681 -32
  46. package/dist/web/mobile-memory.js +317 -0
  47. package/dist/web/mobile.html +4 -4
  48. package/dist/web/mobile.js +36 -27
  49. package/dist/web/routes-world.js +9 -1
  50. package/dist/web/server.js +50 -39
  51. package/dist/web/sw.js +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ha-daemon.ts — 独立 kernel 守护进程入口 (业务侧宿主, 2026-10-16, leo: 双内核冗余 — 拆独立 kernel 守护进程)
3
+ *
4
+ * 由主进程 spawn (见 ha-supervisor.ts 的 startHaDaemon): `node dist/agents/ha-daemon.js --role standby --home <HOME>`
5
+ * 职责: 以 **standby** 角色运行 HA 守护循环 —— 探测主进程心跳, 主死则接管其 orphan run。
6
+ *
7
+ * 位置说明: **不放在 src/kernel/** — kernel 目录有 K1 边界门 (禁 import 业务); 本文件要注入 run-store
8
+ * 的 reconcileOrphans/prepareResume, 是**业务侧宿主**, 放 src/agents/。kernel/ha/ 只留纯机制
9
+ * (ha-arbiter 心跳格式/锁/决策 + ha-guard 守护循环, 端口注入), 零业务依赖。
10
+ *
11
+ * 环境变量:
12
+ * BOLLOON_HOME 数据目录 (缺省 ~/.bolloon)
13
+ * BOLLOON_HA_BEAT_MS 心跳间隔 (缺省 10s)
14
+ * BOLLOON_HA_STALE_MS 心跳过期阈值 (缺省 30s)
15
+ *
16
+ * 退出: SIGTERM/SIGINT → 停心跳 → exit(0); 父进程退出 → 管道关闭 → 退出 (不孤儿存活)。
17
+ */
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ import * as fs from 'fs/promises';
21
+ import { startHaGuard } from '../kernel/ha/ha-guard.js';
22
+ import { runsDir } from './run-store.js';
23
+ const home = process.env.BOLLOON_HOME || os.homedir();
24
+ const beatMs = Number(process.env.BOLLOON_HA_BEAT_MS) || 10_000;
25
+ const staleMs = Number(process.env.BOLLOON_HA_STALE_MS) || 30_000;
26
+ // 2026-10-16: run-store 的 runsDir() 用 os.homedir() (POSIX 读 $HOME)。
27
+ // 设置了 BOLLOON_HOME = daemon 操作隔离数据目录 → 无条件把 $HOME 指过去
28
+ // (daemon 是独立 spawn 的守护进程, 改自己的 HOME 不影响主进程; 这里不设的话
29
+ // reconcileOrphans/prepareResume 会读真实 ~/.bolloon/runs, 接不到隔离 runs 的孤儿)。
30
+ if (process.env.BOLLOON_HOME) {
31
+ process.env.HOME = process.env.BOLLOON_HOME;
32
+ }
33
+ // 业务侧端口注入: run-store 实现 (守护循环不自己 import 业务, 由宿主注入)
34
+ const ports = {
35
+ reconcileOrphans: async () => {
36
+ const { reconcileOrphans } = await import('./run-store.js');
37
+ return reconcileOrphans();
38
+ },
39
+ prepareResume: async (runId) => {
40
+ const { prepareResume } = await import('./run-store.js');
41
+ const r = await prepareResume(runId);
42
+ return r.ok ? { ok: true } : { ok: false, reason: r.reason };
43
+ },
44
+ collectActiveRunIds: async () => {
45
+ // 2026-10-16: BOLLOON_HOME 优先 (隔离 HOME 的 daemon 读隔离 runs), 否则 fallback 真实 runsDir()
46
+ const home = process.env.BOLLOON_HOME || '';
47
+ const dir = home ? path.join(home, '.bolloon', 'runs') : runsDir();
48
+ const files = await fs.readdir(dir).catch(() => []);
49
+ const active = [];
50
+ for (const n of files) {
51
+ if (!n.endsWith('.json') || n.endsWith('.bak'))
52
+ continue;
53
+ const id = n.replace(/\.json$/, '');
54
+ if (/^(run|goal)_/.test(id))
55
+ active.push(id); // 保守: 只认 run/goal 前缀
56
+ }
57
+ return active;
58
+ },
59
+ log: (level, message) => {
60
+ process.stdout.write(`[ha-daemon:${level}] ${message}\n`);
61
+ },
62
+ };
63
+ const guard = startHaGuard(ports, {
64
+ role: 'standby',
65
+ home,
66
+ kernelVersion: process.env.npm_package_version || '',
67
+ beatMs,
68
+ staleMs,
69
+ onTakeover: (info) => {
70
+ process.stdout.write(`[ha-daemon] 接管主进程: adopted=${info.adoptedRunIds.length} runs (${info.reason})\n`);
71
+ },
72
+ });
73
+ process.stdout.write(`[ha-daemon] 启动 role=standby pid=${process.pid} home=${home} beat=${beatMs}ms stale=${staleMs}ms\n`);
74
+ let shuttingDown = false;
75
+ function shutdown(sig) {
76
+ if (shuttingDown)
77
+ return;
78
+ shuttingDown = true;
79
+ process.stdout.write(`[ha-daemon] 收到 ${sig}, 停止心跳退出\n`);
80
+ guard.stop();
81
+ process.exit(0);
82
+ }
83
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
84
+ process.on('SIGINT', () => shutdown('SIGINT'));
85
+ export { guard };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ha-supervisor.ts — 主进程侧 HA 守护管理器 (2026-10-16, leo: 双内核冗余 — 独立 kernel 守护进程)
3
+ *
4
+ * 主进程 (createWebServer / CLI) 侧:
5
+ * ① 自己以 **primary** 角色写心跳 (含当前活跃 runIds)
6
+ * ② spawn 独立 kernel 守护进程 (src/agents/ha-daemon.ts, 以 standby 角色跑)
7
+ * ③ 监控 daemon 存活: 心跳过期 / 进程退出 → **自动重启 + 接管** (重启后它自己会 reconcileOrphans + prepareResume)
8
+ * ④ daemon 启动即探测主进程: 主死 → daemon 接管 (反向故障转移)
9
+ *
10
+ * 依赖注入 (业务侧端口):
11
+ * - primary 心跳的 runIds 来源: collectActiveRunIds (runs 目录扫描)
12
+ * - 全端口注入 → 可单测 (注入假 DAEMON_TS 路径 / 假 child_process)
13
+ */
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ import { spawn } from 'node:child_process';
17
+ import { startHaGuard } from '../kernel/ha/ha-guard.js';
18
+ import { collectActiveRunIds, STALE_MS, BEAT_MS } from '../kernel/ha/ha-arbiter.js';
19
+ import * as fs from 'fs';
20
+ const noop = () => { };
21
+ function defaultSpawn(cmd, args, opts) {
22
+ return spawn(cmd, args, opts);
23
+ }
24
+ export class HaSupervisor {
25
+ opts;
26
+ guard = null;
27
+ daemon = null;
28
+ restartTimer = null;
29
+ state = {
30
+ primaryStarted: false,
31
+ daemonPid: null,
32
+ daemonAlive: false,
33
+ restarts: 0,
34
+ daemonExitCode: null,
35
+ };
36
+ stopped = false;
37
+ constructor(opts = {}) {
38
+ this.opts = opts;
39
+ }
40
+ getState() { return { ...this.state }; }
41
+ /** 启动: primary 心跳 + spawn daemon + 监控重启 */
42
+ async start() {
43
+ const home = this.opts.home || os.homedir();
44
+ const beatMs = this.opts.beatMs ?? BEAT_MS;
45
+ const staleMs = this.opts.staleMs ?? STALE_MS;
46
+ const log = this.opts.log || noop;
47
+ // ① primary 心跳 (主进程自己; runIds 从 runs 目录收集)
48
+ const ports = {
49
+ reconcileOrphans: async () => ({ interrupted: [], stillRunning: [], failed: [] }),
50
+ prepareResume: async () => ({ ok: false, reason: 'primary 不接管 (它是主)' }),
51
+ collectActiveRunIds: async () => collectActiveRunIds(path.join(home, '.bolloon', 'runs')),
52
+ log: (l, m) => log(l, `[ha:primary] ${m}`),
53
+ };
54
+ this.guard = startHaGuard(ports, {
55
+ role: 'primary',
56
+ home,
57
+ kernelVersion: process.env.npm_package_version || '',
58
+ beatMs,
59
+ staleMs,
60
+ });
61
+ this.state.primaryStarted = true;
62
+ log('info', `[ha] primary 心跳已启动 (home=${home}, beat=${beatMs}ms, stale=${staleMs}ms)`);
63
+ // ② spawn 独立 kernel 守护进程 (standby)
64
+ this.spawnDaemon();
65
+ // ③ 监控 daemon: 退出 → 自动重启 (kernel 死则重启 + 它接管)
66
+ // 监控交给 child 'exit' 事件 (已在 spawnDaemon 里挂), 无需额外轮询。
67
+ }
68
+ spawnDaemon() {
69
+ if (this.stopped)
70
+ return;
71
+ const log = this.opts.log || noop;
72
+ const home = this.opts.home || os.homedir();
73
+ const nodeBin = this.opts.nodeBin || process.execPath;
74
+ const entry = this.opts.daemonEntry || path.join(__dirname, 'ha-daemon.js');
75
+ const spawnImpl = this.opts.spawnImpl || defaultSpawn;
76
+ log('info', `[ha] spawn kernel 守护进程: ${nodeBin} ${entry} (role=standby)`);
77
+ try {
78
+ const child = spawnImpl(nodeBin, [entry], {
79
+ env: {
80
+ ...process.env,
81
+ BOLLOON_HOME: home,
82
+ BOLLOON_HA_BEAT_MS: String(this.opts.beatMs ?? BEAT_MS),
83
+ BOLLOON_HA_STALE_MS: String(this.opts.staleMs ?? STALE_MS),
84
+ },
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ detached: false,
87
+ });
88
+ this.daemon = child;
89
+ this.state.daemonPid = child.pid ?? null;
90
+ this.state.daemonAlive = true;
91
+ child.stdout?.on('data', (d) => {
92
+ const s = String(d).trim();
93
+ if (s)
94
+ log('info', `[ha:daemon] ${s}`);
95
+ });
96
+ child.stderr?.on('data', (d) => {
97
+ const s = String(d).trim();
98
+ if (s)
99
+ log('warn', `[ha:daemon:err] ${s}`);
100
+ });
101
+ child.on('exit', (code, signal) => {
102
+ this.state.daemonAlive = false;
103
+ this.state.daemonExitCode = code;
104
+ log('warn', `[ha] kernel 守护进程退出 code=${code} signal=${signal || ''}`);
105
+ if (!this.stopped && this.opts.autoRestart !== false) {
106
+ log('info', `[ha] 守护进程退出, 自动重启 (第 ${this.state.restarts + 1} 次)`);
107
+ this.restartTimer = setTimeout(() => {
108
+ if (!this.stopped) {
109
+ this.state.restarts++;
110
+ this.spawnDaemon();
111
+ }
112
+ }, 1000);
113
+ }
114
+ });
115
+ }
116
+ catch (e) {
117
+ log('error', `[ha] spawn 守护进程失败: ${String(e?.message || e).slice(0, 160)}`);
118
+ }
119
+ }
120
+ /** 停止: 停 primary 心跳 + 杀 daemon (幂等) */
121
+ stop() {
122
+ this.stopped = true;
123
+ this.guard?.stop();
124
+ this.guard = null;
125
+ if (this.restartTimer) {
126
+ clearTimeout(this.restartTimer);
127
+ this.restartTimer = null;
128
+ }
129
+ if (this.daemon && !this.daemon.killed) {
130
+ try {
131
+ this.daemon.kill('SIGTERM');
132
+ }
133
+ catch { /* 忽略 */ }
134
+ }
135
+ // best-effort 清 primary 心跳
136
+ try {
137
+ const f = path.join(this.opts.home || os.homedir(), '.bolloon', 'kernel-ha', 'primary.heartbeat');
138
+ fs.unlinkSync(f);
139
+ }
140
+ catch { /* 忽略 */ }
141
+ }
142
+ }
143
+ /** 便捷工厂 */
144
+ export function createHaSupervisor(opts = {}) {
145
+ return new HaSupervisor(opts);
146
+ }
@@ -353,6 +353,31 @@ export function formatRecallBlock(hits) {
353
353
  });
354
354
  return `<memory-context>\n${lines.join('\n')}\n</memory-context>`;
355
355
  }
356
+ /**
357
+ * 稳态记忆块 (2026-10-15, 对齐 Hermes `_memory_parts` 的整块注入):
358
+ * 每次 build system prompt 时把「重要且稳定」的记忆整块给出 — 不依赖当轮 query 召回。
359
+ * 选材: ① crystallized (固化 = 用户确认过/多次命中) ② 未淡化 (decayed=false)
360
+ * ③ importance 降序; 上限 maxEntries (默认 6), 单条 120 字符摘要。
361
+ * 与 recall 互补: recall 是 query 驱动的即时相关, 这块是「始终可见的稳态事实」。
362
+ */
363
+ export function formatMemorySystemBlock(entries, maxEntries = 6) {
364
+ const picked = entries
365
+ .filter((e) => !e.decayed) // 淡化中的不算稳态
366
+ .sort((a, b) => {
367
+ // 固化优先, 然后 importance 降序
368
+ if (a.crystallized !== b.crystallized)
369
+ return a.crystallized ? -1 : 1;
370
+ return b.importance - a.importance;
371
+ })
372
+ .slice(0, maxEntries);
373
+ if (!picked.length)
374
+ return '';
375
+ const lines = picked.map((e) => {
376
+ const tag = `${e.kind}${e.crystallized ? '★' : ''}`;
377
+ return `- (${tag}, ${e.importance.toFixed(2)}) ${e.content.slice(0, 120)}`;
378
+ });
379
+ return `<stable-memory>\n${lines.join('\n')}\n</stable-memory>`;
380
+ }
356
381
  /** 测试辅助: 清空分桶 (隔离 HOME 时用) */
357
382
  export function resetMemoryStores() {
358
383
  stores.clear();
@@ -114,6 +114,18 @@ export async function recordFeedback(action, sourceId) {
114
114
  await writeFeedback(fb);
115
115
  return { ok: true };
116
116
  }
117
+ /** 按 provider DID 过滤 (通讯录卡片): 只留信箱投递人/搜索来源命中指定 DID 的; 本机 board 源无 DID → 过滤掉 */
118
+ function filterByProviderDids(all, providerDids) {
119
+ if (!providerDids || !providerDids.length)
120
+ return all;
121
+ const set = new Set(providerDids.map((d) => String(d || '').trim()).filter(Boolean));
122
+ if (!set.size)
123
+ return all;
124
+ return all.filter((c) => {
125
+ const did = c.inbox?.providerDid || '';
126
+ return !!did && set.has(did);
127
+ });
128
+ }
117
129
  function candidateOf(entry, intentId, score, reason, matchTags) {
118
130
  return {
119
131
  id: `opp_${entry.announcementId.slice(0, 12)}`,
@@ -229,7 +241,7 @@ export async function scanOpportunities(opts = {}) {
229
241
  for (const c of all.filter((x) => x.reason === 'match').slice(0, 10)) {
230
242
  void noteObservation({ ts: Date.now(), kind: 'opportunity-seen', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'match' });
231
243
  }
232
- return { ok: true, results: (await mergeInboxOnTop(all)).slice(0, limit) };
244
+ return { ok: true, results: filterByProviderDids(await mergeInboxOnTop(all), opts.providerDids ?? []).slice(0, limit) };
233
245
  }
234
246
  // 默认世界流: 新鲜度优先
235
247
  const sorted = [...openEntries].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
@@ -243,7 +255,7 @@ export async function scanOpportunities(opts = {}) {
243
255
  for (const c of out.slice(0, 10)) {
244
256
  void noteObservation({ ts: Date.now(), kind: 'world-change', sourceId: c.sourceId, title: c.title, score: c.score, reason: 'world' });
245
257
  }
246
- return { ok: true, results: (await mergeInboxOnTop(out)).slice(0, limit) };
258
+ return { ok: true, results: filterByProviderDids(await mergeInboxOnTop(out), opts.providerDids ?? []).slice(0, limit) };
247
259
  }
248
260
  // 有意图: 匹配打分, match 优先; 世界变化不消失
249
261
  const matches = [];
@@ -274,7 +286,7 @@ export async function scanOpportunities(opts = {}) {
274
286
  return rank(a.reason) - rank(b.reason);
275
287
  return b.score - a.score;
276
288
  });
277
- return { ok: true, results: (await mergeInboxOnTop(all)).slice(0, limit) };
289
+ return { ok: true, results: filterByProviderDids(await mergeInboxOnTop(all), opts.providerDids ?? []).slice(0, limit) };
278
290
  }
279
291
  /** 对单个意图即时匹配 (list --intent 用; 不走反馈过滤) */
280
292
  export async function matchOneIntentText(text, tags, budget, opts = {}) {
@@ -219,6 +219,14 @@ export class PiSessionManager {
219
219
  getPersona() {
220
220
  return this.persona;
221
221
  }
222
+ /**
223
+ * 2026-10-15 (对齐 Hermes invalidate_system_prompt): 重读 persona 源并刷新快照。
224
+ * persona.json / persona/<agentId>/*.md 在外被改时, 由上层 (pi-sdk) 检测指纹变化后调用,
225
+ * 让本轮 systemPrompt 就带上新 persona — 不再等 agent 重建/切 channel。
226
+ */
227
+ async reloadPersona() {
228
+ this.persona = await this.loadPersona();
229
+ }
222
230
  getState() {
223
231
  return { ...this.state, lastActive: new Date().toISOString() };
224
232
  }