@bolloon/bolloon-agent 0.5.5 → 0.6.1

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 (158) hide show
  1. package/dist/agents/execution-entry.js +10 -0
  2. package/dist/agents/goal-flywheel/wiring/seams.js +5 -1
  3. package/dist/agents/parse-tool-call.js +6 -1
  4. package/dist/agents/pi-sdk-session-factory.js +36 -3
  5. package/dist/agents/pi-sdk-tools.js +18 -8
  6. package/dist/agents/pi-sdk.js +749 -1390
  7. package/dist/agents/run-context.js +20 -0
  8. package/dist/agents/runner-resolver.js +3 -1
  9. package/dist/agents/tool-capability/index.js +51 -0
  10. package/dist/agents/workflow-pivot-loop.js +31 -10
  11. package/dist/agents/write-verify.js +60 -0
  12. package/dist/bollharness-integration/integration.js +8 -0
  13. package/dist/bollharness-integration/skill-adapter.js +32 -3
  14. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +5 -1
  15. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -1
  16. package/dist/electron-build/agents/parse-tool-call.js +6 -1
  17. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  18. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  19. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  20. package/dist/electron-build/agents/pi-sdk-tools.js +21 -11
  21. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  22. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  23. package/dist/electron-build/agents/pi-sdk.js +750 -1389
  24. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  25. package/dist/electron-build/agents/run-context.js +24 -0
  26. package/dist/electron-build/agents/run-context.js.map +1 -0
  27. package/dist/electron-build/agents/runner-resolver.js +3 -1
  28. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  29. package/dist/electron-build/agents/tool-capability/index.js +90 -0
  30. package/dist/electron-build/agents/tool-capability/index.js.map +1 -0
  31. package/dist/electron-build/agents/workflow-pivot-loop.js +31 -10
  32. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  33. package/dist/electron-build/agents/write-verify.js +99 -0
  34. package/dist/electron-build/agents/write-verify.js.map +1 -0
  35. package/dist/electron-build/bollharness-integration/integration.js +8 -0
  36. package/dist/electron-build/bollharness-integration/integration.js.map +1 -1
  37. package/dist/electron-build/bollharness-integration/skill-adapter.js +32 -3
  38. package/dist/electron-build/bollharness-integration/skill-adapter.js.map +1 -1
  39. package/dist/electron-build/kernel/channel-actor.js +339 -0
  40. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  41. package/dist/electron-build/kernel/control.js +102 -0
  42. package/dist/electron-build/kernel/control.js.map +1 -0
  43. package/dist/electron-build/kernel/model-runtime.js +508 -0
  44. package/dist/electron-build/kernel/model-runtime.js.map +1 -0
  45. package/dist/electron-build/kernel/run-lifecycle.js +146 -0
  46. package/dist/electron-build/kernel/run-lifecycle.js.map +1 -0
  47. package/dist/electron-build/kernel/transport.js +119 -0
  48. package/dist/electron-build/kernel/transport.js.map +1 -0
  49. package/dist/electron-build/llm/native-adapter.js +122 -0
  50. package/dist/electron-build/llm/native-adapter.js.map +1 -0
  51. package/dist/index.js +43 -19
  52. package/dist/ios/bolloon-web.json +3 -3
  53. package/dist/ios/client.js +13 -7
  54. package/dist/ios/mobile-core.js +7762 -6289
  55. package/dist/ios/mobile.js +64 -0
  56. package/dist/ios/routes-mobile-tasks.js +2 -1
  57. package/dist/ios/routes-tasks.js +16 -1
  58. package/dist/ios/routes-x402-info.js +28 -1
  59. package/dist/ios/server-storage.js +26 -5
  60. package/dist/ios/server.js +840 -862
  61. package/dist/ios/sw.js +1 -1
  62. package/dist/ios/ui/message-renderer.js +473 -29
  63. package/dist/ios/ui/step-timeline.js +13 -12
  64. package/dist/kernel/channel-actor.js +321 -0
  65. package/dist/kernel/control.js +95 -0
  66. package/dist/kernel/gate-scan.js +1350 -0
  67. package/dist/kernel/model-runtime.js +494 -0
  68. package/dist/kernel/plan-channel-actor.js +305 -0
  69. package/dist/kernel/plan-communication.js +163 -0
  70. package/dist/kernel/plan-constraint.js +152 -0
  71. package/dist/kernel/plan-deletion.js +32 -0
  72. package/dist/kernel/plan-harness.js +78 -0
  73. package/dist/kernel/plan-modelruntime.js +60 -0
  74. package/dist/kernel/plan-runcontext.js +163 -0
  75. package/dist/kernel/plan.js +149 -0
  76. package/dist/kernel/roster.js +244 -0
  77. package/dist/kernel/run-lifecycle.js +138 -0
  78. package/dist/kernel/transport.js +111 -0
  79. package/dist/llm/native-adapter.js +117 -0
  80. package/dist/web/bolloon-web.json +3 -3
  81. package/dist/web/client.js +12 -7
  82. package/dist/web/routes-tasks.js +16 -1
  83. package/dist/web/server.js +838 -862
  84. package/dist/web/sw.js +1 -1
  85. package/dist/web/ui/message-renderer.js +473 -29
  86. package/dist/web/ui/step-timeline.js +13 -12
  87. package/package.json +1 -1
  88. package/scripts/build-web.ts +74 -1
  89. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  90. package/dist/constraint-runtime/_archive_helper.js +0 -9
  91. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  92. package/dist/constraint-runtime/assistant/index.js +0 -12
  93. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  94. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  95. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  96. package/dist/constraint-runtime/bridge/index.js +0 -12
  97. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  98. package/dist/constraint-runtime/buddy/index.js +0 -12
  99. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  100. package/dist/constraint-runtime/cli/index.js +0 -12
  101. package/dist/constraint-runtime/components/index.d.ts +0 -11
  102. package/dist/constraint-runtime/components/index.js +0 -12
  103. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  104. package/dist/constraint-runtime/constants/index.js +0 -12
  105. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  106. package/dist/constraint-runtime/coordinator/index.js +0 -12
  107. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  108. package/dist/constraint-runtime/cost_hook.js +0 -4
  109. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  110. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  111. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  112. package/dist/constraint-runtime/execution_registry.js +0 -44
  113. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  114. package/dist/constraint-runtime/hooks/index.js +0 -12
  115. package/dist/constraint-runtime/ink.d.ts +0 -1
  116. package/dist/constraint-runtime/ink.js +0 -4
  117. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  118. package/dist/constraint-runtime/keybindings/index.js +0 -12
  119. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  120. package/dist/constraint-runtime/memdir/index.js +0 -12
  121. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  122. package/dist/constraint-runtime/migrations/index.js +0 -12
  123. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  124. package/dist/constraint-runtime/moreright/index.js +0 -12
  125. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  126. package/dist/constraint-runtime/native_ts/index.js +0 -12
  127. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  128. package/dist/constraint-runtime/output_styles/index.js +0 -12
  129. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  130. package/dist/constraint-runtime/plugins/index.js +0 -12
  131. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  132. package/dist/constraint-runtime/port_manifest.js +0 -11
  133. package/dist/constraint-runtime/query.d.ts +0 -6
  134. package/dist/constraint-runtime/query.js +0 -1
  135. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  136. package/dist/constraint-runtime/remote/index.js +0 -12
  137. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  138. package/dist/constraint-runtime/schemas/index.js +0 -12
  139. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  140. package/dist/constraint-runtime/screens/index.js +0 -12
  141. package/dist/constraint-runtime/server/index.d.ts +0 -11
  142. package/dist/constraint-runtime/server/index.js +0 -12
  143. package/dist/constraint-runtime/services/index.d.ts +0 -11
  144. package/dist/constraint-runtime/services/index.js +0 -12
  145. package/dist/constraint-runtime/state/index.d.ts +0 -11
  146. package/dist/constraint-runtime/state/index.js +0 -12
  147. package/dist/constraint-runtime/system_init.d.ts +0 -1
  148. package/dist/constraint-runtime/system_init.js +0 -20
  149. package/dist/constraint-runtime/types/index.d.ts +0 -11
  150. package/dist/constraint-runtime/types/index.js +0 -12
  151. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  152. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  153. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  154. package/dist/constraint-runtime/utils/index.js +0 -12
  155. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  156. package/dist/constraint-runtime/vim/index.js +0 -12
  157. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  158. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -225,6 +225,68 @@
225
225
  }
226
226
  }
227
227
 
228
+ // ── Efficode 条目 (内置参考实现: 仓内 src/efficode/**, 不靠"从电脑端同步") ──────
229
+ // 纪律: 这段**不用 innerHTML 拼任何数据** (纯 createElement + textContent) ·
230
+ // 双语走 data-zh/data-en (切语言时外面那段 applyLang 会一起改文字节点) ·
231
+ // role=button + tabindex + aria-label + Enter/Space 可达 · 无外部依赖。
232
+ const EFFICODE_SPEC_PATH = 'docs/wiki/efficode.md';
233
+ const EFFICODE_IMPL_PATH = 'src/efficode/** · src/agents/agent-lang.ts';
234
+ const EFFICODE_GATE = 'scripts/verify-efficode.ts (55/0)';
235
+ function effiLangNow() {
236
+ try { return localStorage.getItem('bolloon_lang') === 'en' ? 'en' : 'zh'; } catch (e) { return 'zh'; }
237
+ }
238
+ function buildEfficodeEntry() {
239
+ const el = document.createElement('div');
240
+ el.className = 'conv-item';
241
+ el.id = 'item-efficode';
242
+ el.setAttribute('role', 'button');
243
+ el.setAttribute('tabindex', '0');
244
+ el.setAttribute('aria-label', 'Efficode 规范页 (中文): ' + EFFICODE_SPEC_PATH);
245
+ const avatar = document.createElement('div');
246
+ avatar.className = 'conv-avatar';
247
+ avatar.textContent = 'E';
248
+ const body = document.createElement('div');
249
+ body.className = 'conv-body';
250
+ const name = document.createElement('div');
251
+ name.className = 'conv-name';
252
+ name.textContent = '/efficode';
253
+ const desc = document.createElement('div');
254
+ desc.className = 'conv-preview';
255
+ desc.setAttribute('data-zh', '智能体间的符号化交流语言 · 参考实现 (签名/声波未实现) — 点开看规范页 docs/wiki/efficode.md');
256
+ desc.setAttribute('data-en', 'Symbolic agent-to-agent language · reference impl (signing/audio not implemented) — tap for docs/wiki/efficode.md');
257
+ desc.textContent = effiLangNow() === 'en' ? desc.getAttribute('data-en') : desc.getAttribute('data-zh');
258
+ body.appendChild(name);
259
+ body.appendChild(desc);
260
+ el.appendChild(avatar);
261
+ el.appendChild(body);
262
+ const open = () => alert(
263
+ '/efficode\n\n' +
264
+ (effiLangNow() === 'en'
265
+ ? 'Symbolic binary language between agents (Efficient + Code). Reference implementation in this repo; negotiation falls back to natural language unless both sides declare efficode. Signing / ECC session keys / sound-wave mode are NOT implemented.\n\n'
266
+ : '智能体之间的符号化二进制交流语言 (Efficient + Code)。本仓有参考实现; 只有双方都声明 efficode 才用, 否则明确回落自然语言。签名验证 / ECC 临时会话密钥 / 声波模式尚未实现。\n\n') +
267
+ '规范页: ' + EFFICODE_SPEC_PATH + '\n' +
268
+ '实现: ' + EFFICODE_IMPL_PATH + '\n' +
269
+ '验收门: ' + EFFICODE_GATE
270
+ );
271
+ el.addEventListener('click', open);
272
+ el.addEventListener('keydown', (ev) => {
273
+ if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') { ev.preventDefault(); open(); }
274
+ });
275
+ return el;
276
+ }
277
+
278
+ /** 把内置 Efficode 条目追加到 Skills 页 (空态 / 有列表都显示 —— 它是仓内参考实现) */
279
+ function appendEfficodeBlock(body) {
280
+ if (!body || body.querySelector('#item-efficode')) return;
281
+ const cap = document.createElement('div');
282
+ cap.style.cssText = 'font-size:11px;color:var(--text-muted);margin:14px 0 6px';
283
+ cap.setAttribute('data-zh', '内置 (随客户端发布, 不来自电脑端同步)');
284
+ cap.setAttribute('data-en', 'Built in (ships with the client, not synced from desktop)');
285
+ cap.textContent = effiLangNow() === 'en' ? cap.getAttribute('data-en') : cap.getAttribute('data-zh');
286
+ body.appendChild(cap);
287
+ body.appendChild(buildEfficodeEntry());
288
+ }
289
+
228
290
  async function openSkillsPage() {
229
291
  if ($('#skills-page')) return;
230
292
  const page = mkOverlayPage('skills-page', 'Skills', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
@@ -241,6 +303,7 @@
241
303
  body.innerHTML = `<div style="padding:18px 8px;text-align:center;color:var(--text-muted);line-height:1.8">
242
304
  还没有技能列表<br><span style="font-size:12px">电脑端 ~/.bolloon/skills/ 下的 skills,会在「从电脑端同步」后出现在这里</span>
243
305
  ${err ? `<div style="font-size:11px;margin-top:8px">(读取失败:${escapeHtml(err)})</div>` : ''}</div>`;
306
+ appendEfficodeBlock(body);
244
307
  return;
245
308
  }
246
309
  body.innerHTML = skills.map((s, i) => `<div class="conv-item" data-sk="${i}">
@@ -252,6 +315,7 @@
252
315
  const s = skills[Number(el.dataset.sk)] || {};
253
316
  alert(`/${s.name || ''}\n\n${s.description || '(无说明)'}`);
254
317
  }));
318
+ appendEfficodeBlock(body);
255
319
  };
256
320
  await render();
257
321
  const btn = page.querySelector('#skills-sync');
@@ -167,7 +167,8 @@ export function registerMobileTaskRoutes(app, opts = {}) {
167
167
  if (kind === 'group_create' || kind === 'group_join' || kind === 'group_leave') {
168
168
  const { createGroup, joinGroup, leaveGroup } = await import('../agents/gateway-group.js');
169
169
  if (kind === 'group_create') {
170
- const r = await createGroup(String(request.groupRef || '').trim());
170
+ // 手机端建群与 Web 端同一语义 (微信式群聊) ⇒ 显式声明开放写入
171
+ const r = await createGroup(String(request.groupRef || '').trim(), { acl: 'open' });
171
172
  if (!r.ok || !r.group)
172
173
  return fail(res, 409, 'GROUP_CREATE_FAILED', `建群失败: ${r.error || '未知原因'}`);
173
174
  return res.json({ ok: true, kind, group: { id: r.group.id, name: r.group.name }, text: `已建群「${r.group.name}」` });
@@ -4,6 +4,7 @@
4
4
  * 从 src/web/server.ts 抽出 (~160 行).
5
5
  * 包含 /api/tasks/* (GET, POST, PATCH, DELETE, execute, execute-next)
6
6
  */
7
+ import { deliverThroughActor } from '../kernel/channel-actor.js';
7
8
  import { loadTaskQueue, saveTaskQueue } from './server-storage.js';
8
9
  import { documentReader } from '../documents/reader.js';
9
10
  import { applyCancelRequest, shouldFinalizeAsCancelled } from './task-cancel.js';
@@ -309,7 +310,21 @@ async function executeTask(task, channelId, getAgentForChannel, broadcast) {
309
310
  case 'chat':
310
311
  if (task.description) {
311
312
  broadcast({ type: 'status', content: `执行任务: ${task.title}` }, channelId);
312
- result = await agent.prompt(task.description);
313
+ // 闭包里 TS 不保留收窄 ⇒ 先收成局部 const
314
+ const desc = task.description;
315
+ const agentForRun = agent;
316
+ // 2026-10-02 (K8): 走 Pi 的**唯一执行入口** `runExecution` (K5 步骤⑦ 已建), 不再由通道自己直呼 prompt
317
+ if (typeof agentForRun.runExecution !== 'function') {
318
+ // K8 纪律: 拿不到唯一执行入口 ⇒ **响亮失败**, 不静默回落直呼 prompt (否则收口变成纸面的)
319
+ throw new Error('K8: 该 session 未提供 runExecution (唯一执行入口) ⇒ 拒绝直呼 prompt');
320
+ }
321
+ // 注意: 这里**显式传 channelId** —— ExecutionRequest 要求它, 且语义上本就该绑定 task 的通道。
322
+ // 与旧写法 (prompt(desc), 隐式走 actor 绑定) 相比这是一处**行为差量**: 绑定从隐式变显式。
323
+ // 按纪律记过"待真跑核验", **2026-10-02 已核验通过** (`src/test/k8-routes-tasks-execution.test.ts`,
324
+ // 真 express + 真 HTTP + 真任务队列): ① agent 只给 runExecution (无 prompt) 也能跑通 ⇒ 活路径唯一;
325
+ // ② **两半 id 一致** (解析 agent 用的 id == 绑 run 用的 id) 是自检不变量; ③ 无门时响亮失败且 prompt 调用 0 次。
326
+ // 真变异: 绑成 `channelId + '-mut'` ⇒ 判红; 回落直呼 `prompt` ⇒ 判红; 还原 ⇒ 回绿无残差。
327
+ result = await deliverThroughActor(agentForRun, () => agentForRun.runExecution({ input: desc, channelId }));
313
328
  }
314
329
  break;
315
330
  case 'read':
@@ -20,6 +20,7 @@ import * as os from 'os';
20
20
  import { listInfo, getStoredInfo, publishInfo, removeInfo, buildPaymentRequired, checkAndSettlePayment, buyInfo, } from '../agents/x402/paid-info-store.js';
21
21
  import { buildSignedEnvelope, verifyEnvelope, summarizeVerify } from '../agents/x402/paid-info-protocol.js';
22
22
  import { makeDidResolver } from '../agents/x402/paid-info-tools.js';
23
+ import { handleDirectPayment, directHealth, directPaymentPath, directPaymentEnabled } from '../agents/x402/direct-payment.js';
23
24
  /** 本机 DIAP 身份 (签发信封用) — 缺失时明确报错, 不静默造一个 */
24
25
  async function loadProviderKeypair() {
25
26
  try {
@@ -122,7 +123,7 @@ export function registerX402InfoRoutes(app) {
122
123
  content: stored.content,
123
124
  keypair: kp,
124
125
  payment: {
125
- mode: pay.mode === 'facilitator' ? 'facilitator' : 'local-dev',
126
+ mode: pay.mode === 'facilitator' ? 'facilitator' : pay.mode === 'direct' ? 'direct' : 'local-dev',
126
127
  receipt: String(pay.receipt || ''),
127
128
  txHash: pay.txHash,
128
129
  network: pay.network || stored.item.price.network,
@@ -139,6 +140,32 @@ export function registerX402InfoRoutes(app) {
139
140
  res.status(500).json({ error: e?.message });
140
141
  }
141
142
  });
143
+ // ---- 去中心化直付 (mode = 'direct'): 买方自己发 USDC, 把 txHash 交回来 ----
144
+ // POST /api/x402/info/:id/payment body: { "txHash": "0x…" }
145
+ // 服务器**只读链**: receipt.status=1 + USDC Transfer → payTo + value >= accepts.amount
146
+ // + 两条不同 RPC 交叉一致 + 确认数够 → 落待办 + 给取件 token (202); 未过 → 402 (accepts 逐字)
147
+ app.post('/api/x402/info/:id/payment', async (req, res) => {
148
+ try {
149
+ const id = String(req.params.id);
150
+ const stored = await getStoredInfo(id);
151
+ if (!stored)
152
+ return res.status(404).json({ error: '信息不存在' });
153
+ const baseUrl = `${req.protocol || 'http'}://${req.headers?.host || `127.0.0.1:${process.env.PORT || 54188}`}`;
154
+ const out = await handleDirectPayment({
155
+ item: stored.item,
156
+ bodyText: typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {}),
157
+ resourceUrl: `${baseUrl}${directPaymentPath(stored.item.id)}`,
158
+ });
159
+ return res.status(out.status).set(out.headers).send(out.body);
160
+ }
161
+ catch (e) {
162
+ res.status(500).json({ error: e?.message });
163
+ }
164
+ });
165
+ // ---- 直付模式自述 (health 用: 开关与口径必须能从机器读到) ----
166
+ app.get('/api/x402/direct', async (_req, res) => {
167
+ res.json({ enabled: directPaymentEnabled(), paymentPath: directPaymentPath(':id'), ...directHealth() });
168
+ });
142
169
  // ---- 买方辅助: 代付 + 验真 ----
143
170
  app.post('/api/x402/info/buy', async (req, res) => {
144
171
  try {
@@ -11,6 +11,8 @@
11
11
  * Channel / Session / Task / SessionMessage / SessionSummary type 来自 ./server-types.ts.
12
12
  */
13
13
  import * as fs from 'fs/promises';
14
+ import * as path from 'path';
15
+ import { channelsPaths, assertTestWriteTarget } from '../agents/agent-identity-store.js';
14
16
  import { CHANNELS_PATH, SESSION_CACHE_PATH, TASK_QUEUE_PATH, THEME_PATH, } from './server-types.js';
15
17
  import { saveWindow as saveSessionWindow, loadWindow as loadSessionWindow } from '../bootstrap/session-window.js';
16
18
  import { parentsSatisfied } from './task-deps.js';
@@ -42,7 +44,7 @@ export async function updateChannels(fn) {
42
44
  }
43
45
  async function rawLoadChannels() {
44
46
  try {
45
- const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
47
+ const data = await fs.readFile(channelsPathNow(), 'utf-8');
46
48
  return JSON.parse(data);
47
49
  }
48
50
  catch (readErr) {
@@ -50,11 +52,11 @@ async function rawLoadChannels() {
50
52
  if (readErr?.code !== 'ENOENT') {
51
53
  console.warn('[loadChannels] channels.json 解析失败, 尝试从 .tmp 恢复:', readErr?.message?.slice(0, 80));
52
54
  try {
53
- const tmpData = await fs.readFile(CHANNELS_PATH + '.tmp', 'utf-8');
55
+ const tmpData = await fs.readFile(channelsPathNow() + '.tmp', 'utf-8');
54
56
  const recovered = JSON.parse(tmpData);
55
57
  console.log(`[loadChannels] 从 .tmp 恢复成功: ${recovered.length} 个 channel`);
56
58
  // 立即把恢复的内容写回主文件
57
- await fs.writeFile(CHANNELS_PATH, tmpData, 'utf-8');
59
+ await fs.writeFile(channelsPathNow(), tmpData, 'utf-8');
58
60
  return recovered;
59
61
  }
60
62
  catch (tmpErr) {
@@ -64,6 +66,22 @@ async function rawLoadChannels() {
64
66
  return [];
65
67
  }
66
68
  }
69
+ /**
70
+ * channels.json 的路径**每次调用时**解析 (2026-10-01, 数据事故修复)。
71
+ *
72
+ * 事故: `server-types.CHANNELS_PATH` 是 import 时定死的常量 (按那一刻的 HOME 解析),
73
+ * 于是测试里后设的 process.env.HOME **完全不起作用** ⇒ 测试夹具把用户的真实
74
+ * ~/.bolloon/sessions/channels.json 整表覆盖 (4 个智能体只剩 1 条)。
75
+ * 现在改成按**当前** HOME 解析 (与 CLI 侧 agent-identity-store.channelsPaths 同一口径)。
76
+ */
77
+ function channelsPathNow() {
78
+ try {
79
+ return channelsPaths()[0];
80
+ }
81
+ catch {
82
+ return CHANNELS_PATH;
83
+ }
84
+ }
67
85
  async function rawSaveChannels(channels) {
68
86
  const sanitized = channels.map(ch => {
69
87
  const { didDocument: _omit, ...rest } = ch;
@@ -75,9 +93,12 @@ async function rawSaveChannels(channels) {
75
93
  lastChannelsJson = jsonStr;
76
94
  console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
77
95
  // 2026-07-24: 原子写入 — 先写 .tmp 再 rename, 防止崩溃导致 channels.json 损坏
78
- const tmpPath = CHANNELS_PATH + '.tmp';
96
+ const target = channelsPathNow();
97
+ // 测试进程只允许写临时目录 —— 真实数据目录一律拒绝 (第二道闸)
98
+ assertTestWriteTarget(path.dirname(path.dirname(target)), 'channels.json');
99
+ const tmpPath = target + '.tmp';
79
100
  await fs.writeFile(tmpPath, jsonStr, 'utf-8');
80
- await fs.rename(tmpPath, CHANNELS_PATH);
101
+ await fs.rename(tmpPath, target);
81
102
  lastChannelsWriteAt = Date.now();
82
103
  }
83
104
  export async function loadChannels() {