@bolloon/bolloon-agent 0.4.4 → 0.4.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.
@@ -24,6 +24,138 @@ async function uiStore() {
24
24
  return getUICidStore();
25
25
  }
26
26
  const fmt = (o) => JSON.stringify(o, null, 2).slice(0, 3000);
27
+ /** 2026-08-12 (Task5): Kanban 看板工具 (Hermes kanban_db → OrbitDB). 见 kanban-store.ts. */
28
+ async function kanbanBoard(boardId) {
29
+ const { openKanbanBoard } = await import('./kanban-store.js');
30
+ return openKanbanBoard(boardId);
31
+ }
32
+ const fmtRows = (tasks) => tasks.length === 0 ? '📭 无任务' : tasks.map(t => ` [${t.status}] ${t.id} ${t.title}${t.assignee ? ` (${t.assignee})` : ''}`).join('\n');
33
+ export function registerKanbanTools(ctx) {
34
+ ctx.tools.set('kanban_create', {
35
+ name: 'kanban_create',
36
+ description: '在看板上创建任务 (OrbitDB 持久化, 跨设备同步). 状态从 triage 开始.',
37
+ parameters: { boardId: '看板 id (必填, 默认用 agentId)', title: '任务标题 (必填)', body: '任务详情 (可选)', parentIds: '父任务 id 数组 (可选, 全部 done 才可执行)', createdBy: '创建者 (可选)', dueAt: '计划时间戳 (可选)' },
38
+ execute: async (args) => {
39
+ try {
40
+ const boardId = String(args.boardId || args.agentId || '').trim() || 'default';
41
+ const title = String(args.title || '').trim();
42
+ if (!title)
43
+ return { success: false, error: 'title 必填' };
44
+ const parentIds = Array.isArray(args.parentIds) ? args.parentIds.map(String) : [];
45
+ const dueAt = typeof args.dueAt === 'number' ? args.dueAt : null;
46
+ const task = await (await kanbanBoard(boardId)).createTask({ boardId, title, body: String(args.body || '').trim(), parentIds, createdBy: String(args.createdBy || '').trim() || undefined, dueAt });
47
+ return { success: true, output: `📌 已创建任务 (${boardId}):\n id: ${task.id}\n 状态: ${task.status}\n 推进: kanban_claim(id="${task.id}") → kanban_complete(...)` };
48
+ }
49
+ catch (e) {
50
+ return { success: false, error: `kanban_create 失败: ${String(e.message || e).slice(0, 200)}` };
51
+ }
52
+ }
53
+ });
54
+ ctx.tools.set('kanban_list', {
55
+ name: 'kanban_list',
56
+ description: '列出看板任务 (可按状态过滤). 状态: triage/todo/scheduled/ready/running/blocked/review/done/archived.',
57
+ parameters: { boardId: '看板 id (必填)', status: '可选: 按状态过滤' },
58
+ execute: async (args) => {
59
+ try {
60
+ const boardId = String(args.boardId || '').trim() || 'default';
61
+ const status = String(args.status || '').trim() || undefined;
62
+ const tasks = await (await kanbanBoard(boardId)).listTasks(status);
63
+ return { success: true, output: `🗂 ${boardId} ${status ? `[${status}]` : '全部'} ${tasks.length} 个任务:\n${fmtRows(tasks)}` };
64
+ }
65
+ catch (e) {
66
+ return { success: false, error: `kanban_list 失败: ${String(e.message || e).slice(0, 200)}` };
67
+ }
68
+ }
69
+ });
70
+ ctx.tools.set('kanban_get', {
71
+ name: 'kanban_get',
72
+ description: '查看单个任务详情 (含 parentIds/认领人/失败计数/createdRefs).',
73
+ parameters: { boardId: '看板 id (必填)', id: '任务 id (必填)' },
74
+ execute: async (args) => {
75
+ try {
76
+ const boardId = String(args.boardId || '').trim() || 'default';
77
+ const id = String(args.id || '').trim();
78
+ if (!id)
79
+ return { success: false, error: 'id 必填' };
80
+ const t = await (await kanbanBoard(boardId)).getTask(id);
81
+ if (!t)
82
+ return { success: false, error: `任务不存在: ${id}` };
83
+ return { success: true, output: `📄 ${t.id} [${t.status}]\n 标题: ${t.title}\n 详情: ${t.body || '—'}\n 父任务: ${t.parentIds.length ? t.parentIds.join(', ') : '无'}\n 认领人: ${t.assignee || t.claimLock || '未认领'}\n 失败计数: ${t.consecutiveFailures}\n 引用: ${t.createdRefs.length ? t.createdRefs.join(', ') : '无'}\n 备注: ${t.reason || '—'}` };
84
+ }
85
+ catch (e) {
86
+ return { success: false, error: `kanban_get 失败: ${String(e.message || e).slice(0, 200)}` };
87
+ }
88
+ }
89
+ });
90
+ ctx.tools.set('kanban_claim', {
91
+ name: 'kanban_claim',
92
+ description: '原子认领一个 ready 任务 (CAS, 至多一个 worker 成功). 认领后状态 → running.',
93
+ parameters: { boardId: '看板 id (必填)', id: '任务 id (必填)', worker: '认领者标识 (默认当前 agentId)' },
94
+ execute: async (args) => {
95
+ try {
96
+ const boardId = String(args.boardId || '').trim() || 'default';
97
+ const id = String(args.id || '').trim();
98
+ const worker = String(args.worker || '').trim() || 'agent';
99
+ if (!id)
100
+ return { success: false, error: 'id 必填' };
101
+ const r = await (await kanbanBoard(boardId)).claimTask(id, worker);
102
+ if (!r.ok)
103
+ return { success: false, error: `认领失败: ${r.reason}` };
104
+ return { success: true, output: `✅ 已认领 ${id} (worker=${worker})\n 完成: kanban_complete(id="${id}", worker="${worker}")` };
105
+ }
106
+ catch (e) {
107
+ return { success: false, error: `kanban_claim 失败: ${String(e.message || e).slice(0, 200)}` };
108
+ }
109
+ }
110
+ });
111
+ ctx.tools.set('kanban_complete', {
112
+ name: 'kanban_complete',
113
+ description: '完成一个 running 任务 → done (自动释放认领锁, 父任务完成时自动解子任务). createdRefs 声称的引用会做防幻觉校验.',
114
+ parameters: { boardId: '看板 id (必填)', id: '任务 id (必填)', worker: '认领者 (默认当前 agentId)', createdRefs: '可选: 声称创建的任务/资源引用数组' },
115
+ execute: async (args) => {
116
+ try {
117
+ const boardId = String(args.boardId || '').trim() || 'default';
118
+ const id = String(args.id || '').trim();
119
+ const worker = String(args.worker || '').trim() || 'agent';
120
+ if (!id)
121
+ return { success: false, error: 'id 必填' };
122
+ const refs = Array.isArray(args.createdRefs) ? args.createdRefs.map(String) : [];
123
+ const t = await (await kanbanBoard(boardId)).completeTask(id, worker, refs);
124
+ if (!t)
125
+ return { success: false, error: `无法完成 ${id} (不存在 或 非本 worker 认领)` };
126
+ return { success: true, output: `🏁 已完成 ${id} [done]${t.reason ? `\n 提醒: ${t.reason}` : ''}` };
127
+ }
128
+ catch (e) {
129
+ return { success: false, error: `kanban_complete 失败: ${String(e.message || e).slice(0, 200)}` };
130
+ }
131
+ }
132
+ });
133
+ ctx.tools.set('kanban_status', {
134
+ name: 'kanban_status',
135
+ description: '手动推进任务状态 (review/done/blocked/archived/ready 等合法迁移). blocked 需带 reason; archived 为终态.',
136
+ parameters: { boardId: '看板 id (必填)', id: '任务 id (必填)', status: '目标状态 (必填)', reason: 'blocked/review 时必填备注' },
137
+ execute: async (args) => {
138
+ try {
139
+ const boardId = String(args.boardId || '').trim() || 'default';
140
+ const id = String(args.id || '').trim();
141
+ const status = String(args.status || '').trim();
142
+ if (!id || !status)
143
+ return { success: false, error: 'id 和 status 必填' };
144
+ let t = null;
145
+ if (status === 'review')
146
+ t = await (await kanbanBoard(boardId)).requestReview(id, String(args.reason || '').trim());
147
+ else
148
+ t = await (await kanbanBoard(boardId)).setStatus(id, status, String(args.reason || '').trim());
149
+ if (!t)
150
+ return { success: false, error: `状态迁移失败 (非法迁移或任务不存在): ${id} → ${status}` };
151
+ return { success: true, output: `↻ ${id} → [${t.status}]${t.reason ? ` (${t.reason})` : ''}` };
152
+ }
153
+ catch (e) {
154
+ return { success: false, error: `kanban_status 失败: ${String(e.message || e).slice(0, 200)}` };
155
+ }
156
+ }
157
+ });
158
+ }
27
159
  export function registerOrbitdbTools(ctx) {
28
160
  ctx.tools.set('cid_save', {
29
161
  name: 'cid_save',
@@ -222,4 +354,11 @@ export function registerOrbitdbTools(ctx) {
222
354
  }
223
355
  }
224
356
  });
357
+ // 2026-08-12 (Task5): Kanban 看板工具 (Hermes kanban_db → OrbitDB)
358
+ try {
359
+ registerKanbanTools(ctx);
360
+ }
361
+ catch (odbErr) {
362
+ console.warn('[registerTools] Kanban 工具注册失败 (非致命):', odbErr);
363
+ }
225
364
  }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * kanban-store.ts — Hermes kanban 的 OrbitDB 落地 (2026-08-12, Task5)
3
+ *
4
+ * 借鉴 hermes_cli/kanban_db.py 的 9 态任务看板 + 原子认领 (CAS), 存到 OrbitDB
5
+ * keyvalue store (去中心化, DID 复制可跨设备同步). 每个 board 一个 store:
6
+ * bolloon-kanban-<boardId>
7
+ *
8
+ * 任务 9 态 (VALID_STATUSES):
9
+ * triage → todo → (父全部 done) → ready → running → done / archived
10
+ * - scheduled: 定时任务 (本次简化为状态 + dueAt, 到时可 promote)
11
+ * - blocked: 主动 block (sticky, 需显式 unblock) 或父依赖未完成 (自动解)
12
+ * - review: request_review 挂起等人工审批, approve → done
13
+ * - archived: 终态归档
14
+ *
15
+ * 原子认领 (CAS, 至多一个 worker 认领成功): claim_task 只在
16
+ * status === 'ready' && claimLock 为空 时置 running + claimLock + claimExpires,
17
+ * 否则返回输家 (不重试, 无分布式锁).
18
+ *
19
+ * 安全:
20
+ * - 依赖的 KV store (put/get/all) 可注入 → 单测用内存 map, 生产用 OrbitDB store.
21
+ * - 任务字段全部有默认值 (dag-cbor 不支持 undefined).
22
+ */
23
+ export const KANBAN_STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
24
+ const now = () => Date.now();
25
+ function fresh(id, boardId, title, base = {}) {
26
+ return {
27
+ id, boardId, title,
28
+ body: base.body ?? '',
29
+ status: base.status ?? 'triage',
30
+ parentIds: base.parentIds ?? [],
31
+ createdBy: base.createdBy ?? '',
32
+ assignee: base.assignee ?? '',
33
+ dueAt: base.dueAt ?? null,
34
+ claimLock: base.claimLock ?? '',
35
+ claimExpires: base.claimExpires ?? 0,
36
+ consecutiveFailures: base.consecutiveFailures ?? 0,
37
+ createdRefs: base.createdRefs ?? [],
38
+ reason: base.reason ?? '',
39
+ review: base.review ?? '',
40
+ createdAt: base.createdAt ?? now(),
41
+ updatedAt: base.updatedAt ?? now(),
42
+ completedAt: base.completedAt ?? null,
43
+ archivedAt: base.archivedAt ?? null,
44
+ };
45
+ }
46
+ export function taskKey(id) {
47
+ return `task:${id}`;
48
+ }
49
+ /**
50
+ * KanbanBoard — 绑定一个 KV store 的看板.
51
+ * 所有写都经 readTask→改→put 的 CAS 语义; claim 在内核判赢家.
52
+ */
53
+ export class KanbanBoard {
54
+ kv;
55
+ constructor(kv) {
56
+ this.kv = kv;
57
+ }
58
+ st(s) {
59
+ return JSON.parse(JSON.stringify(s));
60
+ }
61
+ async createTask(input) {
62
+ const id = `t_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
63
+ const task = fresh(id, input.boardId, input.title, {
64
+ body: input.body ?? '',
65
+ parentIds: input.parentIds ?? [],
66
+ createdBy: input.createdBy ?? '',
67
+ dueAt: input.dueAt ?? null,
68
+ status: 'triage',
69
+ });
70
+ await this.kv.put(taskKey(id), JSON.parse(JSON.stringify(task)));
71
+ return this.st(task);
72
+ }
73
+ async getTask(id) {
74
+ const v = await this.kv.get(taskKey(id));
75
+ return v || null;
76
+ }
77
+ async listTasks(status) {
78
+ const all = await this.kv.all();
79
+ const tasks = [];
80
+ for (const e of all) {
81
+ const t = e.value;
82
+ if (t && typeof t === 'object' && t.title !== undefined) {
83
+ if (!status || t.status === status)
84
+ tasks.push(this.st(t));
85
+ }
86
+ }
87
+ return tasks.sort((a, b) => a.createdAt - b.createdAt);
88
+ }
89
+ /** 保存变更 (校验合法) */
90
+ async persist(task) {
91
+ task.updatedAt = now();
92
+ await this.kv.put(taskKey(task.id), JSON.parse(JSON.stringify(task)));
93
+ return this.st(task);
94
+ }
95
+ /**
96
+ * 状态推进 gate: 只允许合法迁移 (防乱跳). 返回 null 表示禁止.
97
+ */
98
+ trans(from, to) {
99
+ if (from === to)
100
+ return true;
101
+ if (from === 'done' || from === 'archived')
102
+ return false; // 终态不可再动
103
+ const allowed = {
104
+ triage: ['todo', 'scheduled', 'blocked', 'archived'],
105
+ todo: ['scheduled', 'ready', 'blocked', 'archived'],
106
+ scheduled: ['todo', 'ready', 'blocked', 'archived'],
107
+ ready: ['running', 'blocked', 'archived'],
108
+ running: ['review', 'done', 'blocked', 'ready'],
109
+ blocked: ['todo', 'ready', 'archived'],
110
+ review: ['done', 'blocked', 'todo'],
111
+ };
112
+ const ok = (allowed[from] || []).includes(to);
113
+ return ok;
114
+ }
115
+ /** checking parents status from KV */
116
+ async allParentsDone(task) {
117
+ if (!task.parentIds || task.parentIds.length === 0)
118
+ return true;
119
+ for (const pid of task.parentIds) {
120
+ const p = await this.getTask(pid);
121
+ if (p && p.status !== 'done' && p.status !== 'archived')
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /** 把 todo/scheduled/blocked(父依赖未完成) 任务晋升为 ready (父依赖满足时) — recompute Ready */
127
+ async promoteReady(taskId) {
128
+ const t = await this.getTask(taskId);
129
+ if (!t)
130
+ return null;
131
+ // 自动可清 blocked = 因父依赖未完成; sticky blocked (用户主动) 不自动解
132
+ const autoClearable = t.status === 'blocked' && t.reason === 'parents_not_done';
133
+ if (!autoClearable && t.status !== 'todo' && t.status !== 'scheduled')
134
+ return null;
135
+ if (!(await this.allParentsDone(t))) {
136
+ if (t.status === 'blocked')
137
+ return this.st(t);
138
+ return { ...this.st(t), status: 'blocked', reason: 'parents_not_done' };
139
+ }
140
+ return this.persist({ ...t, status: 'ready', reason: '' });
141
+ }
142
+ /** 认领 (CAS): 只有 ready 且未被认领的才能成功 → running + claimLock */
143
+ async claimTask(taskId, worker, ttlMs = 15 * 60 * 1000) {
144
+ const t = await this.getTask(taskId);
145
+ if (!t)
146
+ return { ok: false, reason: `任务不存在: ${taskId}` };
147
+ if (t.claimLock)
148
+ return { ok: false, reason: '已被其他 worker 认领' };
149
+ if (t.status !== 'ready') {
150
+ // 父依赖不变式: 认领点再强校验一次 (任何写路径把任务置 ready 都可能被这里纠正)
151
+ if (!(await this.allParentsDone(t))) {
152
+ await this.persist({ ...t, status: 'blocked', reason: 'parents_not_done' });
153
+ return { ok: false, reason: '父任务未完成 (降级 blocked)' };
154
+ }
155
+ return { ok: false, reason: `状态为 ${t.status}, 只有 ready 可认领` };
156
+ }
157
+ const run = fresh(t.id, t.boardId, t.title, { ...t, status: 'running', claimLock: worker, claimExpires: now() + ttlMs, consecutiveFailures: 0 });
158
+ await this.kv.put(taskKey(taskId), JSON.parse(JSON.stringify(run)));
159
+ return { ok: true, task: this.st(run) };
160
+ }
161
+ /** 认领 TTL 过期释放 — 防崩溃锁泄漏 (重启后旧 claimLock 残留) */
162
+ async releaseStaleClaims(nowTs = now()) {
163
+ const tasks = await this.listTasks('running');
164
+ let freed = 0;
165
+ for (const t of tasks) {
166
+ if (t.claimLock && t.claimExpires < nowTs) {
167
+ await this.persist({ ...t, status: 'ready', claimLock: '', claimExpires: 0 });
168
+ freed++;
169
+ }
170
+ }
171
+ return freed;
172
+ }
173
+ /** 记一次失败 (熔断): 连续失败 > limit → blocked */
174
+ async registerFailure(taskId, failureLimit = 3) {
175
+ const t = await this.getTask(taskId);
176
+ if (!t)
177
+ return null;
178
+ let next = t.claimLock ? { ...t, status: 'ready', claimLock: '', claimExpires: 0 } : { ...t };
179
+ next.consecutiveFailures = (next.consecutiveFailures || 0) + 1;
180
+ if (next.consecutiveFailures >= failureLimit) {
181
+ next.status = 'blocked';
182
+ next.reason = `circuit_break (连续失败 ${next.consecutiveFailures})`;
183
+ }
184
+ return this.persist(next);
185
+ }
186
+ /** 完成: 校验声称创建的引用 (防幻觉 — 幽灵引用 advisory), 父任务完成则解子依赖 */
187
+ async completeTask(taskId, worker, createdRefs = []) {
188
+ const t = await this.getTask(taskId);
189
+ if (!t)
190
+ return null;
191
+ if (t.claimLock && t.claimLock !== worker)
192
+ return null;
193
+ // 防幻觉 (advisory): 声称创建的引用逐一验证存在 → 不存在记为 advisory (不阻塞, 但可审计)
194
+ const missing = [];
195
+ for (const ref of createdRefs) {
196
+ const existing = await this.getTask(ref.replace(/^task:/, ''));
197
+ if (!existing)
198
+ missing.push(ref);
199
+ }
200
+ const task = fresh(t.id, t.boardId, t.title, {
201
+ ...t, status: 'done', claimLock: '', claimExpires: 0,
202
+ createdRefs, reason: missing.length ? `advisory_missing_refs: ${missing.join(',')}` : t.reason,
203
+ completedAt: now(),
204
+ });
205
+ await this.kv.put(taskKey(taskId), JSON.parse(JSON.stringify(task)));
206
+ // 子任务受父完成影响 → 尝试晋升 ready
207
+ const all = await this.listTasks();
208
+ for (const c of all) {
209
+ if (c.parentIds.includes(taskId) && (c.status === 'todo' || c.status === 'blocked')) {
210
+ await this.promoteReady(c.id);
211
+ }
212
+ }
213
+ return this.st(task);
214
+ }
215
+ async requestReview(taskId, note) {
216
+ const t = await this.getTask(taskId);
217
+ if (!t || t.status !== 'running')
218
+ return null;
219
+ return this.persist({ ...t, status: 'review', claimLock: '', claimExpires: 0, review: note });
220
+ }
221
+ async approveReview(taskId) {
222
+ const t = await this.getTask(taskId);
223
+ if (!t || t.status !== 'review')
224
+ return null;
225
+ return this.persist({ ...t, status: 'done', reason: 'review_approved', completedAt: now() });
226
+ }
227
+ async rejectReview(taskId, reason) {
228
+ const t = await this.getTask(taskId);
229
+ if (!t || t.status !== 'review')
230
+ return null;
231
+ return this.persist({ ...t, status: 'blocked', review: '', reason: `review_rejected: ${reason}` });
232
+ }
233
+ async setStatus(taskId, to, reason = '') {
234
+ const t = await this.getTask(taskId);
235
+ if (!t || !this.trans(t.status, to))
236
+ return null;
237
+ if (to === 'blocked' && !reason && !t.reason)
238
+ return null; // blocked 需带 reason
239
+ const next = { ...t, status: to, reason: to === 'blocked' ? (reason || t.reason) : reason || t.reason };
240
+ if (to === 'archived')
241
+ next.archivedAt = now();
242
+ if (to === 'ready') {
243
+ if (!(await this.allParentsDone(next)))
244
+ return this.persist({ ...next, status: 'blocked', reason: 'parents_not_done' });
245
+ }
246
+ return this.persist(next);
247
+ }
248
+ }
249
+ /** 打开/缓存指定 board 的 OrbitDB KV store, 返回绑定的 KanbanBoard. 失败抛错. */
250
+ const _boardCache = new Map();
251
+ export async function openKanbanBoard(boardId) {
252
+ if (_boardCache.has(boardId))
253
+ return _boardCache.get(boardId);
254
+ const { getCIDDatabase } = await import('./cid-database.js');
255
+ const store = await (await getCIDDatabase()).openStore(`bolloon-kanban-${boardId}`, 'keyvalue');
256
+ const board = new KanbanBoard(store);
257
+ _boardCache.set(boardId, board);
258
+ return board;
259
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * task-store.ts — Task 队列的 OrbitDB 主存储 (2026-08-12, Task6)
3
+ *
4
+ * 把 hermes kanban 用 OrbitDB 实现: 任务队列以 OrbitDB keyvalue store 为主存储,
5
+ * 支持跨设备/去中心化同步 (与 did-catalog-replication 同模式, 共享同一 helia 节点).
6
+ *
7
+ * 设计:
8
+ * - store 名: `bolloon-tasks-<did短名>` (确定性, 跨设备同名 → 同一复制流)
9
+ * - 存整个任务数组到单键 `tasks` (兼容 server-storage 现有 loadTaskQueue/saveTaskQueue)
10
+ * - 懒初始化 + 幂等: 首次 warm 时打开 store; 失败静默 → 回退本地文件 (增强层, 不阻塞)
11
+ * - 生产由 server 启动时 warmTaskOrbitStore() 预热; 测试环境 (临时 HOME) 不预热 → 走本地
12
+ *
13
+ * 失败静默: OrbitDB 不可用 (离线/首次/测试) 时任务照常走本地文件, 不阻塞主路径.
14
+ */
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ import { getCIDDatabase } from './cid-database.js';
18
+ const homeRoot = () => process.env.HOME || os.homedir() || '/tmp';
19
+ export const TASK_ORBIT_STORE_KEY = 'tasks';
20
+ /** store 名: 按 did 短名确定性命名 */
21
+ export function taskStoreName(did) {
22
+ const short = String(did || 'local').replace(/[^a-zA-Z0-9-]/g, '_').slice(0, 40) || 'local';
23
+ return `bolloon-tasks-${short}`;
24
+ }
25
+ /** 真实实现 (OrbitDB). 单例: 同一进程共享 CID 数据库单例. */
26
+ export class OrbitDBTaskStore {
27
+ did;
28
+ db;
29
+ _store = null;
30
+ ready = false;
31
+ storeName;
32
+ storeAddress = '';
33
+ constructor(did, db = getCIDDatabase()) {
34
+ this.did = did;
35
+ this.db = db;
36
+ this.storeName = taskStoreName(did);
37
+ }
38
+ async warm() {
39
+ if (this.ready && this._store)
40
+ return true;
41
+ try {
42
+ this._store = await this.db.openStore(this.storeName, 'keyvalue');
43
+ this.storeAddress = this._store.address;
44
+ this.ready = true;
45
+ return true;
46
+ }
47
+ catch (err) {
48
+ // OrbitDB 不可用 (离线/首次/测试环境) → 静默回退本地
49
+ try {
50
+ console.warn(`[task-store] OrbitDB 任务存储不可用, 回退本地: ${err?.message?.slice(0, 120)}`);
51
+ }
52
+ catch { /* 忽略 */ }
53
+ this.ready = false;
54
+ return false;
55
+ }
56
+ }
57
+ async loadTasks() {
58
+ if (!this.ready || !this._store)
59
+ return null;
60
+ try {
61
+ const v = await this._store.get(TASK_ORBIT_STORE_KEY);
62
+ if (v == null)
63
+ return null;
64
+ if (Array.isArray(v))
65
+ return v;
66
+ return null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ async saveTasks(tasks) {
73
+ if (!this.ready || !this._store)
74
+ return false;
75
+ try {
76
+ await this._store.put(TASK_ORBIT_STORE_KEY, tasks);
77
+ return true;
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ async close() {
84
+ this._store = null;
85
+ this.ready = false;
86
+ }
87
+ }
88
+ let _instance = null;
89
+ let _warmPromise = null;
90
+ /** 获取任务 OrbitDB 存储单例 (首次调用时 warm). did 为 '' 时用 'local'. */
91
+ export function getTaskOrbitStore(did = '') {
92
+ if (!_instance)
93
+ _instance = new OrbitDBTaskStore(did || 'local');
94
+ return _instance;
95
+ }
96
+ /** 预热任务 OrbitDB 存储 (server 启动时调用, 幂等). 返回是否就绪. */
97
+ export function warmTaskOrbitStore(did = '') {
98
+ if (!_warmPromise) {
99
+ const store = getTaskOrbitStore(did);
100
+ _warmPromise = store.warm().then((ok) => ok).catch(() => false);
101
+ }
102
+ return _warmPromise;
103
+ }
104
+ /** 重置单例 (测试用) */
105
+ export function resetTaskOrbitStore() {
106
+ _instance = null;
107
+ _warmPromise = null;
108
+ }
109
+ /** 用 did 短名构造 task store 名 (供 server 组装 store name 展示) */
110
+ export function taskStorePath(did, home = homeRoot()) {
111
+ return path.join(home, '.bolloon', 'orbitdb', 'stores', taskStoreName(did));
112
+ }
@@ -279,7 +279,7 @@ export async function findRecentSimilarDecisions(decision, withinMs, options) {
279
279
  for (const j of judgments) {
280
280
  if (statusFilter !== 'all' && (j.status ?? 'active') !== statusFilter)
281
281
  continue;
282
- if (new Date(j.timestamp).getTime() < cutoff)
282
+ if (new Date(j.timestamp).getTime() <= cutoff)
283
283
  continue;
284
284
  if (wantChannel) {
285
285
  const jChannel = j.context?.domain;