@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.
- package/dist/agents/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- package/dist/agents/memory-store.js +25 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/pi-sdk-session-manager.js +8 -0
- package/dist/agents/pi-sdk.js +67 -494
- package/dist/bootstrap/context-collector.js +65 -3
- package/dist/bootstrap/persona-loader.js +26 -0
- package/dist/electron-build/agents/memory-store.js +26 -0
- package/dist/electron-build/agents/memory-store.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +65 -492
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +66 -3
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-loader.js +27 -0
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +16 -3
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/system-prompt/registry.js +8 -6
- package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/index.html +19 -4
- package/dist/ios/mobile-agent.js +267 -41
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +806 -30
- package/dist/ios/mobile-memory.js +317 -0
- package/dist/ios/mobile.html +19 -4
- package/dist/ios/mobile.js +144 -30
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +9 -1
- package/dist/ios/server.js +51 -40
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/gate-scan.js +1 -1
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/plan.js +5 -5
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/pi-ai.js +16 -3
- package/dist/llm/system-prompt/registry.js +8 -6
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile-agent.js +265 -44
- package/dist/web/mobile-core.js +681 -32
- package/dist/web/mobile-memory.js +317 -0
- package/dist/web/mobile.html +4 -4
- package/dist/web/mobile.js +36 -27
- package/dist/web/routes-world.js +9 -1
- package/dist/web/server.js +50 -39
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
package/dist/kernel/gate-scan.js
CHANGED
|
@@ -238,7 +238,7 @@ export function validateDeletionRecord(rec) {
|
|
|
238
238
|
return bad;
|
|
239
239
|
}
|
|
240
240
|
/** 第一批删除候选 = 产品码里 **0 入边引用** 且非入口形态 (机械派生, 不手写) */
|
|
241
|
-
const ENTRY_SHAPE_RE = /(index\.ts$|cli-entry\.ts$|electron\.ts$|\.d\.ts$|server\.ts$|main\.ts$|route|command|register|setup|bootstrap|migration|types\.ts$|constants?\.ts$|config)/;
|
|
241
|
+
const ENTRY_SHAPE_RE = /(index\.ts$|cli-entry\.ts$|electron\.ts$|\.d\.ts$|server\.ts$|main\.ts$|daemon\.ts$|route|command|register|setup|bootstrap|migration|types\.ts$|constants?\.ts$|config)/;
|
|
242
242
|
/**
|
|
243
243
|
* **豁免规则 (2026-10-02)**: 冻结面自身的文件 (KERNEL_FILES 成员 —— `kernel/roster.ts` / `kernel/plan*.ts`)
|
|
244
244
|
* **不算删除候选**。它们是台账/名册, 在"0 入边"口径下天然是孤岛, 每加一个台账就会让候选集变一次
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ha-arbiter.ts — Kernel 双活仲裁器 (2026-10-16, leo: 双内核冗余第一刀)
|
|
3
|
+
*
|
|
4
|
+
* 职责: 同机两个 bolloon 进程各自加载 kernel, 通过**心跳文件**互相探测,
|
|
5
|
+
* 主进程死亡时备进程接管其 orphan run (复用 reconcileOrphans + prepareResume)。
|
|
6
|
+
*
|
|
7
|
+
* 设计 (对齐 tick-lock 跨进程先例):
|
|
8
|
+
* - 心跳文件 `~/.bolloon/kernel-ha/<role>.heartbeat` = JSON {pid, host, role, startedAt, lastBeat, runIds[], kernelVersion}
|
|
9
|
+
* - 主进程每 BEAT_MS (10s) 写自己的心跳 (含当前 runIds); 备进程每 BEAT_MS 探测主心跳
|
|
10
|
+
* - 角色仲裁: 抢 `~/.bolloon/kernel-ha/.primary.lock` (O_EXCL 原子创建) — 防双主
|
|
11
|
+
* - 主心跳过期 (lastBeat 超过 STALE_MS=30s 或 pid 已死) → 备**接管**:
|
|
12
|
+
* ① 标记本机 role=primary (抢锁)
|
|
13
|
+
* ② reconcileOrphans() 找主进程留下的 orphan run
|
|
14
|
+
* ③ 逐个 prepareResume(runId) 确认可恢复 → 记入本机心跳 runIds
|
|
15
|
+
* ④ 心跳文件切 role=primary
|
|
16
|
+
* - 纯函数可单测: parseHeartbeat / isHeartbeatStale / decideTakeover / serializeHeartbeat
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
export const BEAT_MS = 10_000; // 心跳间隔
|
|
22
|
+
export const STALE_MS = 30_000; // 心跳过期阈值 (3 倍间隔)
|
|
23
|
+
export const PRIMARY_LOCK_STALE_MS = 60_000;
|
|
24
|
+
/** HA 数据目录 ~/.bolloon/kernel-ha */
|
|
25
|
+
export function haDir(home = os.homedir()) {
|
|
26
|
+
return path.join(home, '.bolloon', 'kernel-ha');
|
|
27
|
+
}
|
|
28
|
+
export function heartbeatPath(role, home = os.homedir()) {
|
|
29
|
+
return path.join(haDir(home), `${role}.heartbeat`);
|
|
30
|
+
}
|
|
31
|
+
export function primaryLockPath(home = os.homedir()) {
|
|
32
|
+
return path.join(haDir(home), '.primary.lock');
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------- 纯函数
|
|
35
|
+
/** 序列化心跳 (原子写用) */
|
|
36
|
+
export function serializeHeartbeat(rec) {
|
|
37
|
+
return JSON.stringify(rec, null, 2);
|
|
38
|
+
}
|
|
39
|
+
/** 解析心跳文件内容 — 坏行/畸形一律 ok:false (fail-closed) */
|
|
40
|
+
export function parseHeartbeat(raw) {
|
|
41
|
+
try {
|
|
42
|
+
const p = JSON.parse(raw);
|
|
43
|
+
if (!p || typeof p !== 'object')
|
|
44
|
+
return { ok: false, error: 'not-object' };
|
|
45
|
+
if (typeof p.pid !== 'number' || typeof p.host !== 'string' || typeof p.role !== 'string') {
|
|
46
|
+
return { ok: false, error: 'missing-fields' };
|
|
47
|
+
}
|
|
48
|
+
if (p.role !== 'primary' && p.role !== 'standby')
|
|
49
|
+
return { ok: false, error: 'bad-role' };
|
|
50
|
+
const record = {
|
|
51
|
+
pid: p.pid,
|
|
52
|
+
host: p.host,
|
|
53
|
+
role: p.role,
|
|
54
|
+
startedAt: typeof p.startedAt === 'string' ? p.startedAt : new Date().toISOString(),
|
|
55
|
+
lastBeat: typeof p.lastBeat === 'string' ? p.lastBeat : new Date().toISOString(),
|
|
56
|
+
runIds: Array.isArray(p.runIds) ? p.runIds.filter((x) => typeof x === 'string') : [],
|
|
57
|
+
kernelVersion: typeof p.kernelVersion === 'string' ? p.kernelVersion : '',
|
|
58
|
+
};
|
|
59
|
+
return { ok: true, record };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return { ok: false, error: String(e?.message || e).slice(0, 80) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** 心跳是否过期: lastBeat 距今 > staleMs, 或 pid 已不存在 */
|
|
66
|
+
export function isHeartbeatStale(rec, now = Date.now(), staleMs = STALE_MS) {
|
|
67
|
+
const last = Date.parse(rec.lastBeat);
|
|
68
|
+
if (!Number.isFinite(last))
|
|
69
|
+
return true; // 坏时间 → 判过期 (fail-closed)
|
|
70
|
+
if (now - last > staleMs)
|
|
71
|
+
return true;
|
|
72
|
+
// pid 不存在 = 进程已死 → 判过期
|
|
73
|
+
try {
|
|
74
|
+
process.kill(rec.pid, 0);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 是否接管: 主心跳缺失 / 解析失败 / 过期 → true。
|
|
83
|
+
* 纯函数 (不含抢锁副作用), 调用方拿到 true 再走 acquirePrimaryLock。
|
|
84
|
+
*/
|
|
85
|
+
export function decideTakeover(primaryBeat, now = Date.now(), staleMs = STALE_MS) {
|
|
86
|
+
if (!primaryBeat || !primaryBeat.ok)
|
|
87
|
+
return { takeover: true, reason: 'primary-heartbeat-missing' };
|
|
88
|
+
const rec = primaryBeat.record;
|
|
89
|
+
if (isHeartbeatStale(rec, now, staleMs))
|
|
90
|
+
return { takeover: true, reason: 'primary-heartbeat-stale' };
|
|
91
|
+
return { takeover: false, reason: 'primary-alive' };
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------- 副作用 (原子写 / 锁)
|
|
94
|
+
/** 原子写心跳文件 (tmp + rename) */
|
|
95
|
+
export async function writeHeartbeat(rec, home = os.homedir()) {
|
|
96
|
+
await fs.mkdir(haDir(home), { recursive: true });
|
|
97
|
+
const target = heartbeatPath(rec.role, home);
|
|
98
|
+
const tmp = target + '.tmp';
|
|
99
|
+
await fs.writeFile(tmp, serializeHeartbeat(rec), { mode: 0o600 });
|
|
100
|
+
await fs.rename(tmp, target);
|
|
101
|
+
}
|
|
102
|
+
/** 抢主锁 (O_EXCL 原子创建; 已存在且持锁者存活 → 未抢到; 陈旧 → 回收重试一次) */
|
|
103
|
+
export async function acquirePrimaryLock(pid, host, home = os.homedir(), staleMs = PRIMARY_LOCK_STALE_MS) {
|
|
104
|
+
const lockPath = primaryLockPath(home);
|
|
105
|
+
await fs.mkdir(haDir(home), { recursive: true });
|
|
106
|
+
const content = JSON.stringify({ pid, host, at: new Date().toISOString() });
|
|
107
|
+
try {
|
|
108
|
+
await fs.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
|
|
109
|
+
return { acquired: true, reason: 'created' };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (e?.code !== 'EEXIST')
|
|
113
|
+
return { acquired: false, reason: String(e?.message || e).slice(0, 80) };
|
|
114
|
+
// 已存在 → 判陈旧
|
|
115
|
+
let holder = {};
|
|
116
|
+
try {
|
|
117
|
+
holder = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
|
|
118
|
+
}
|
|
119
|
+
catch { /* 坏锁文件 → 视为陈旧 */ }
|
|
120
|
+
const holderAlive = holder && typeof holder.pid === 'number' && (() => { try {
|
|
121
|
+
process.kill(holder.pid, 0);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
} })();
|
|
127
|
+
const fresh = holderAlive;
|
|
128
|
+
if (fresh)
|
|
129
|
+
return { acquired: false, reason: 'held-by-alive-primary' };
|
|
130
|
+
// 陈旧 → 回收重试一次
|
|
131
|
+
try {
|
|
132
|
+
await fs.unlink(lockPath);
|
|
133
|
+
}
|
|
134
|
+
catch { /* 忽略 */ }
|
|
135
|
+
try {
|
|
136
|
+
await fs.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
|
|
137
|
+
return { acquired: true, reason: 'reclaimed-stale' };
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { acquired: false, reason: 'reclaim-race-lost' };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** 读主心跳 (不存在 → null; 坏内容 → ok:false) */
|
|
145
|
+
export async function readPrimaryHeartbeat(home = os.homedir()) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(heartbeatPath('primary', home), 'utf-8');
|
|
148
|
+
return parseHeartbeat(raw);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (e?.code === 'ENOENT')
|
|
152
|
+
return null; // 主心跳还没写过 = 缺失
|
|
153
|
+
return { ok: false, error: String(e?.message || e).slice(0, 80) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** 当前活跃 runIds: 扫 runs 目录里 status ∈ running/recovering/interrupted 的 run */
|
|
157
|
+
export async function collectActiveRunIds(runsDir, now = Date.now()) {
|
|
158
|
+
try {
|
|
159
|
+
const names = await fs.readdir(runsDir);
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const n of names) {
|
|
162
|
+
if (!n.endsWith('.json') || n.endsWith('.bak'))
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const rec = JSON.parse(await fs.readFile(path.join(runsDir, n), 'utf-8'));
|
|
166
|
+
if (rec && typeof rec.status === 'string' && ['running', 'recovering', 'interrupted'].includes(rec.status)) {
|
|
167
|
+
out.push(n.replace(/\.json$/, ''));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* 坏 run 文件跳过 */ }
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ha-guard.ts — Kernel 双活守护循环 (2026-10-16, leo: 双内核冗余 — 独立 kernel 守护进程)
|
|
3
|
+
*
|
|
4
|
+
* 职责: 心跳循环 + 对等探测 + 接管 orphan run。
|
|
5
|
+
*
|
|
6
|
+
* 边界 (K1 目录边界: kernel 只做机制, 不碰业务):
|
|
7
|
+
* - 本文件**不 import** 任何业务模块 (run-store / supervisor 等);
|
|
8
|
+
* - reconcileOrphans / prepareResume / collectActiveRunIds 全部**端口注入**,
|
|
9
|
+
* 由调用方 (主进程) 把业务实现传进来 —— 守护循环只负责「什么时候探测/什么时候接管/接管几步」。
|
|
10
|
+
*
|
|
11
|
+
* 循环语义:
|
|
12
|
+
* - 每 BEAT_MS 写自己的心跳 (含当前活跃 runIds, 由注入的 collectActiveRunIds 提供)
|
|
13
|
+
* - 每 BEAT_MS 读对端 (primary) 心跳 → decideTakeover
|
|
14
|
+
* - 判定接管 → acquirePrimaryLock (防双主) → 接管步骤 (注入的 takeoverRuns):
|
|
15
|
+
* ① reconcileOrphans() 找主进程留下的 orphan run
|
|
16
|
+
* ② 逐个 prepareResume(runId) 确认可恢复
|
|
17
|
+
* ③ 记入自己的心跳 runIds
|
|
18
|
+
* ④ 自己的心跳 role 切 primary
|
|
19
|
+
* - 一次接管只做一轮; 下轮心跳若 primary 复活 (心跳新鲜) → 备进程让位 (不主动降级, 由仲裁决定)
|
|
20
|
+
*
|
|
21
|
+
* 退出: stop() 幂等; 进程退出时 best-effort 清心跳 (防止陈旧心跳误导对端判接管)。
|
|
22
|
+
*/
|
|
23
|
+
import { BEAT_MS, STALE_MS, writeHeartbeat, readPrimaryHeartbeat, decideTakeover, acquirePrimaryLock, } from './ha-arbiter.js';
|
|
24
|
+
import * as fsSync from 'fs';
|
|
25
|
+
import * as os from 'os';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
const noop = () => { };
|
|
28
|
+
/** 构造一条心跳记录 */
|
|
29
|
+
export function makeHeartbeat(role, runIds, kernelVersion = '') {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
return {
|
|
32
|
+
pid: process.pid,
|
|
33
|
+
host: os.hostname(),
|
|
34
|
+
role,
|
|
35
|
+
startedAt: now,
|
|
36
|
+
lastBeat: now,
|
|
37
|
+
runIds: runIds || [],
|
|
38
|
+
kernelVersion: String(kernelVersion || '').slice(0, 40),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 一次探测+可能的接管 (纯编排, 可单测)。
|
|
43
|
+
* 返回 { action: 'beat' | 'takeover' | 'noop' } — noop = 本进程是 primary, 只写心跳。
|
|
44
|
+
*/
|
|
45
|
+
export async function guardTick(ports, opts, state) {
|
|
46
|
+
const log = opts.log || ((_l, _m) => { });
|
|
47
|
+
const home = opts.home || os.homedir();
|
|
48
|
+
const beatMs = opts.beatMs ?? BEAT_MS;
|
|
49
|
+
const staleMs = opts.staleMs ?? STALE_MS;
|
|
50
|
+
const collect = ports.collectActiveRunIds || (async () => []);
|
|
51
|
+
// ① 收集当前活跃 runIds (本机)
|
|
52
|
+
const runIds = await collect();
|
|
53
|
+
if (opts.role === 'primary') {
|
|
54
|
+
// 主进程: 写心跳, 什么都不用探测 (备进程会探测我)
|
|
55
|
+
const rec = makeHeartbeat('primary', runIds, opts.kernelVersion);
|
|
56
|
+
await writeHeartbeat(rec, home);
|
|
57
|
+
state.lastBeatAt = rec.lastBeat;
|
|
58
|
+
state.beats++;
|
|
59
|
+
if (opts.onBeat)
|
|
60
|
+
opts.onBeat(rec);
|
|
61
|
+
return { action: 'noop' };
|
|
62
|
+
}
|
|
63
|
+
// ② 备进程: 探测主进程心跳
|
|
64
|
+
const primaryBeat = await readPrimaryHeartbeat(home);
|
|
65
|
+
const decision = decideTakeover(primaryBeat, Date.now(), staleMs);
|
|
66
|
+
if (!decision.takeover) {
|
|
67
|
+
// 主还活着 → 备待命 (写自己的 standby 心跳, 供观察; 不接管)
|
|
68
|
+
const rec = makeHeartbeat('standby', runIds, opts.kernelVersion);
|
|
69
|
+
await writeHeartbeat(rec, home);
|
|
70
|
+
state.lastBeatAt = rec.lastBeat;
|
|
71
|
+
state.beats++;
|
|
72
|
+
if (opts.onBeat)
|
|
73
|
+
opts.onBeat(rec);
|
|
74
|
+
// 2026-10-16: 待命 tick 打一条 (verify/诊断用; 生产可忽略)
|
|
75
|
+
if (process.env.BOLLOON_HA_DEBUG === '1')
|
|
76
|
+
log('info', `standby 待命 beat#${state.beats} (primary alive)`);
|
|
77
|
+
return { action: 'beat' };
|
|
78
|
+
}
|
|
79
|
+
// ③ 判定接管: 抢主锁 (防双主), 抢到才接
|
|
80
|
+
const lock = await acquirePrimaryLock(process.pid, os.hostname(), home);
|
|
81
|
+
if (!lock.acquired) {
|
|
82
|
+
log('warn', `接管被拒: ${lock.reason} (另一进程已抢到主锁)`);
|
|
83
|
+
return { action: 'noop' };
|
|
84
|
+
}
|
|
85
|
+
// ④ 接管步骤: reconcileOrphans → prepareResume 逐个 → 记入心跳 runIds
|
|
86
|
+
let adopted = [];
|
|
87
|
+
try {
|
|
88
|
+
const orphan = await ports.reconcileOrphans();
|
|
89
|
+
adopted = [...orphan.interrupted];
|
|
90
|
+
const resumed = [];
|
|
91
|
+
for (const runId of orphan.interrupted) {
|
|
92
|
+
const r = await ports.prepareResume(runId);
|
|
93
|
+
if (r.ok)
|
|
94
|
+
resumed.push(runId);
|
|
95
|
+
else
|
|
96
|
+
log('warn', `接管 ${runId} 未恢复: ${r.reason || ''}`);
|
|
97
|
+
}
|
|
98
|
+
adopted = resumed;
|
|
99
|
+
// 接管后本机心跳提升为 primary (包含接管的 run)
|
|
100
|
+
const rec = makeHeartbeat('primary', [...runIds, ...resumed], opts.kernelVersion);
|
|
101
|
+
await writeHeartbeat(rec, home);
|
|
102
|
+
state.lastBeatAt = rec.lastBeat;
|
|
103
|
+
state.adoptedRunIds = resumed;
|
|
104
|
+
state.role = 'primary';
|
|
105
|
+
state.takeovers++;
|
|
106
|
+
if (opts.onTakeover)
|
|
107
|
+
opts.onTakeover({ adoptedRunIds: resumed, reason: decision.reason });
|
|
108
|
+
return { action: 'takeover', adoptedRunIds: resumed, reason: decision.reason };
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
log('error', `接管失败: ${String(e?.message || e).slice(0, 200)}`);
|
|
112
|
+
return { action: 'noop' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** 持续守护循环 (每 beatMs 一次 guardTick); stop() 幂等停止 */
|
|
116
|
+
export function startHaGuard(ports, opts) {
|
|
117
|
+
const state = { running: true, role: opts.role, beats: 0, takeovers: 0, lastBeatAt: null, adoptedRunIds: [] };
|
|
118
|
+
const beatMs = opts.beatMs ?? BEAT_MS;
|
|
119
|
+
let timer = null;
|
|
120
|
+
const tick = async () => {
|
|
121
|
+
if (!state.running)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
await guardTick(ports, opts, state);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
(opts.log || noop)('error', `guardTick 异常: ${String(e?.message || e).slice(0, 160)}`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
// 首跳立即执行 (不等一个 beatMs)
|
|
131
|
+
void tick();
|
|
132
|
+
timer = setInterval(() => { void tick(); }, beatMs);
|
|
133
|
+
// 2026-10-16 修复: **不 unref** — daemon 是 spawn 出来的独立守护进程, 必须持续跑。
|
|
134
|
+
// 旧代码 unref() 导致事件循环只剩 interval 时 (stdio pipe 不算活跃句柄),
|
|
135
|
+
// daemon 首跳写完心跳后进程直接退出 → 永远等不到接管 (真跑验证撞出的硬坑)。
|
|
136
|
+
// 只有「不拖住宿主退出」的场景才该 unref (如 CLI 内嵌的心跳), daemon 不属于那种。
|
|
137
|
+
const stop = () => {
|
|
138
|
+
state.running = false;
|
|
139
|
+
if (timer) {
|
|
140
|
+
clearInterval(timer);
|
|
141
|
+
timer = null;
|
|
142
|
+
}
|
|
143
|
+
// best-effort 清自己的心跳 (防陈旧误导对端判接管) — 静态 fsSync (打包面禁 require)
|
|
144
|
+
try {
|
|
145
|
+
const f = path.join(opts.home || os.homedir(), '.bolloon', 'kernel-ha', `${opts.role}.heartbeat`);
|
|
146
|
+
fsSync.unlinkSync(f);
|
|
147
|
+
}
|
|
148
|
+
catch { /* 清理失败不影响退出 */ }
|
|
149
|
+
};
|
|
150
|
+
return { stop, state: () => ({ ...state }) };
|
|
151
|
+
}
|
package/dist/kernel/index.js
CHANGED
|
@@ -31,3 +31,6 @@ export * from './run-resume.js';
|
|
|
31
31
|
export * from './turn-selfcheck.js';
|
|
32
32
|
export * from './code-write-gate.js';
|
|
33
33
|
export * from './transport.js';
|
|
34
|
+
// 2026-10-16 (leo: 双内核冗余): Kernel 双活 — 心跳仲裁 (纯机制) + 守护循环 (端口注入)
|
|
35
|
+
export * from './ha/ha-arbiter.js';
|
|
36
|
+
export * from './ha/ha-guard.js';
|
package/dist/kernel/plan.js
CHANGED
|
@@ -79,7 +79,7 @@ export const ENTRY_GRAPH = [
|
|
|
79
79
|
{ file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K4', note: "runExecution 的流式派发 (K5 步骤⑦ 新增)" },
|
|
80
80
|
{ file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptWithPivotLoop', count: 2, path: 'direct', targetPhase: 'K4', note: "适配器内部 pivot 分派 (不是外部入口)" },
|
|
81
81
|
{ file: 'cli/interface.ts', kind: 'readline-tui', method: 'prompt', count: 3, path: 'queued', targetPhase: 'K3', note: "readline 输入提示, 非 agent 入口" },
|
|
82
|
-
{ file: 'index.ts', kind: 'agent-entry', method: 'prompt', count:
|
|
82
|
+
{ file: 'index.ts', kind: 'agent-entry', method: 'prompt', count: 6, path: 'direct', targetPhase: 'K3', note: "CLI 装配点 (2026-10-16 并行线: prompt 调用点 7→6)" },
|
|
83
83
|
{ file: 'index.ts', kind: 'agent-entry', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K3', note: "CLI 装配点" },
|
|
84
84
|
];
|
|
85
85
|
/**
|
|
@@ -87,7 +87,7 @@ export const ENTRY_GRAPH = [
|
|
|
87
87
|
* ⚠️ 24 → **25** (2026-10-02, K5 步骤⑦): `runExecution` 内部新增 `prompt`/`promptStream` 派发各 1 处 (+2),
|
|
88
88
|
* web/server.ts 用户消息路径改成请求式 (-1)。**这是"形态变化"不是"旁路复活"** —— 见 ENTRY_GRAPH 的两行 adapter-internal。
|
|
89
89
|
*/
|
|
90
|
-
export const ENTRY_DIRECT_CALLS_FROZEN_AT =
|
|
90
|
+
export const ENTRY_DIRECT_CALLS_FROZEN_AT = 12; // 2026-10-16: 13 → 12 (并行线 src/index.ts prompt 调用点 7→6) // 2026-10-02: 20 → 16 → 13 (K8 第四/五步: server.ts 7 处全迁; 只剩 CLI 侧 index.ts 的 8 处) // 2026-10-02: 25 → 20 (K8: 5 处改走 runExecution; 棘轮只许降)
|
|
91
91
|
export const DELETION_LEDGER = [
|
|
92
92
|
{
|
|
93
93
|
target: 'src/constraint-runtime/src/migrations/',
|
|
@@ -144,6 +144,6 @@ export const DELETION_LEDGER = [
|
|
|
144
144
|
},
|
|
145
145
|
];
|
|
146
146
|
/** 第一批删除候选 = 产品码里 **0 入边引用** 且非入口形态 (机械派生, 不手写) */
|
|
147
|
-
export const DELETION_CANDIDATE_COUNT =
|
|
148
|
-
export const DELETION_CANDIDATE_SHA256 = '
|
|
149
|
-
export const DELETION_CANDIDATE_DIRS = { "agents":
|
|
147
|
+
export const DELETION_CANDIDATE_COUNT = 100;
|
|
148
|
+
export const DELETION_CANDIDATE_SHA256 = '8f70bd1370cdb1c37aabdb19feb9d46d55fb51b8072b0ecf4f77f51b618a9478';
|
|
149
|
+
export const DELETION_CANDIDATE_DIRS = { "agents": 10, "bollharness-integration": 1, "bollharness": 61, "cli": 1, "(根)": 1, "judgeness": 3, "llm": 1, "network": 3, "orbitdb": 2, "scripts": 1, "social": 2, "utils": 3, "web": 11 };
|
package/dist/kernel/roster.js
CHANGED
|
@@ -100,7 +100,7 @@ export const KERNEL_ALLOWED_IMPORT_PREFIXES = ['kernel/'];
|
|
|
100
100
|
* 判据是**双向相等**: 盘上多一个未登记文件 ⇒ 红; 名册有而盘上没有 ⇒ 红。
|
|
101
101
|
* (先例: SEAM_ROSTER 的「名册外无人越界」)
|
|
102
102
|
*/
|
|
103
|
-
export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/run-lifecycle.ts', 'kernel/transport.ts', 'kernel/session-lifecycle.ts', 'kernel/code-write-gate.ts', 'kernel/turn-selfcheck.ts', 'kernel/run-resume.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/index.ts', 'kernel/plan-harness.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan-communication.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
|
|
103
|
+
export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/run-lifecycle.ts', 'kernel/transport.ts', 'kernel/session-lifecycle.ts', 'kernel/code-write-gate.ts', 'kernel/turn-selfcheck.ts', 'kernel/run-resume.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/index.ts', 'kernel/ha/ha-arbiter.ts', 'kernel/ha/ha-guard.ts', 'kernel/plan-harness.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan-communication.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
|
|
104
104
|
/** Goal 事实的写入口 (goal-store.ts 的导出中带副作用的那些) */
|
|
105
105
|
export const GOAL_WRITES = [
|
|
106
106
|
'createGoal',
|
|
@@ -230,9 +230,9 @@ export const AUTHORITY_DEBT_FROZEN_AT = 0; // 3 → 0 (K4 用内核控制面还
|
|
|
230
230
|
* 目的只有一个: **不许所有逻辑回流到 kernel.ts**。要加就得显式抬这个数字, 留下痕迹。
|
|
231
231
|
* 数值 = 当前 kernel 目录真实行数, 不留余量。
|
|
232
232
|
*/
|
|
233
|
-
export const KERNEL_LINE_BUDGET =
|
|
233
|
+
export const KERNEL_LINE_BUDGET = 4302; // 2026-10-16: **显式抬档 +379** (重构: 删重复 re-export + fsSync 泄漏, 4304→4302) (Kernel 双活: 新增 kernel/ha/ha-arbiter.ts 心跳仲裁 + kernel/ha/ha-guard.ts 守护循环, 纯机制零业务 import; 守护进程宿主在 src/agents/ha-daemon.ts 不占 kernel 预算) // 2026-10-16: **显式抬档 +38** (10-13/14 两笔 loop 机制: model-runtime 超时预算按上下文长度自适应 +8 / channel-actor Ctrl+C 取消信号 ALS 注入 +33, 均无可删的真实功能) // 2026-10-12: **显式抬档 +33** (kernel SDK 独立 npm 包: 新增 `kernel/index.ts` 出口, 零业务 import 纯 `export *`) // 2026-10-02: 收尾自检规则 `kernel/code-write-gate.ts` 从 agents 侧整条搬入 (+49, 减法: agent 侧少一个模块) // 2026-10-02: 会话生命周期端口 `kernel/session-lifecycle.ts` (278 行) + `gate-scan` 播种锚点跟形状 (+4) —— **显式抬档 +282** (K10 余项; pi-sdk 本批 -18 行); // 2026-10-02 **显式抬档 +22** (K8 收尾: channel-actor 加 `abortActorsOfChannel` + 台账/门同步) // 2026-10-02 **显式抬档 +16** (K10 ④ 后半: transport.ts 加同步读入口 `transportPeersSync` + `peersSync` 端口) // 2026-10-02 **显式抬档 +145** (K10 ④: 新增内核模块 kernel/transport.ts —— 通信入口端口) // 2026-10-02 **显式抬档 +184** (K10 ①: 新增内核模块 kernel/run-lifecycle.ts —— 运行生命周期写口, 与 control.ts 的控制面分开; 预算 == 真实值) // 2026-10-02: +1 (K4-B: gate-scan 复位阈值注释) // 2026-10-02: +5 (K8 正刀: gate-scan 加"收口字段数 3→1"+ queue 反回归) // 2026-10-02: +54 (K8: gate-scan 加 K8 判据) // 2026-10-02: +2 (STAGE_STATUS 拆 K4-A/K4-B + K6/K7 如实修正)
|
|
234
234
|
/** 预算冻结值 (棘轮: 只许减; 想抬预算必须同时改上面那个数字 ⇒ 一次显式动作, diff 里看得见) */
|
|
235
|
-
export const KERNEL_LINE_BUDGET_FROZEN_AT =
|
|
235
|
+
export const KERNEL_LINE_BUDGET_FROZEN_AT = 4302; // 2026-10-16: 随抬档同步 (Kernel 双活 +376) // 2026-10-16: 随抬档同步 (10-13/14 loop 机制增量)
|
|
236
236
|
/**
|
|
237
237
|
* K3b —— **台账数据**单独一档预算 (`src/kernel/plan.ts`)。
|
|
238
238
|
*
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -1183,7 +1183,7 @@ export class PiAIModel {
|
|
|
1183
1183
|
}
|
|
1184
1184
|
// 注: callLocal 直接代理 ollama. 工具清单由 generateText 在外层拼到 messages, 这里 messages 已是 finalMessages.
|
|
1185
1185
|
/**
|
|
1186
|
-
* 2026-09-28: 装配系统提示词并拆成「稳定段 / 动态段」, 按 working dir 缓存 (TTL 10 分钟)。
|
|
1186
|
+
* 2026-09-28: 装配系统提示词并拆成「稳定段 / 动态段」, 按 working dir + **文档指纹** 缓存 (TTL 10 分钟)。
|
|
1187
1187
|
*
|
|
1188
1188
|
* 返回: `{ stableText, dynamicText, cached }`
|
|
1189
1189
|
* - `stableText` 进 system (逐字节稳定 → 服务端前缀 KV 可复用);
|
|
@@ -1192,14 +1192,27 @@ export class PiAIModel {
|
|
|
1192
1192
|
* 为什么缓存: 旧版每轮都跑 25 次 fs.readFile + 装配; 缓存后同一次会话内只装一次,
|
|
1193
1193
|
* 且装配结果逐字节相同 (缓存本身也是"前缀稳定"的必要条件 —— 每次重装都可能因动态层
|
|
1194
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 才生效。
|
|
1195
1200
|
*/
|
|
1196
1201
|
async buildSystemPromptParts(context) {
|
|
1197
|
-
const key = context || process.cwd();
|
|
1198
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}`;
|
|
1199
1212
|
if (_systemPromptCache && _systemPromptCache.key === key && now - _systemPromptCache.at < SYSTEM_PROMPT_CACHE_TTL_MS) {
|
|
1200
1213
|
return { stableText: _systemPromptCache.stableText, dynamicText: _systemPromptCache.dynamicText, cached: true };
|
|
1201
1214
|
}
|
|
1202
|
-
const parts = await this.assembleSystemPromptPartsText(
|
|
1215
|
+
const parts = await this.assembleSystemPromptPartsText(workingDir);
|
|
1203
1216
|
_systemPromptCache = { key, at: now, ...parts };
|
|
1204
1217
|
return { ...parts, cached: false };
|
|
1205
1218
|
}
|
|
@@ -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
|
-
|
|
91
|
-
{ id: 'core.
|
|
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: ['
|
|
95
|
-
{ id: 'core.wellbeing', version: '1.0.0', priority: 120, appliesTo: ['
|
|
96
|
-
{ id: 'core.evenhandedness', version: '1.0.0', priority: 130, appliesTo: ['
|
|
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
|
-
|
|
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)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "bolloon-mobile-web/1",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"channel": "stable",
|
|
5
5
|
"build": "stable",
|
|
6
|
-
"commit": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
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
|
}
|