@bolloon/bolloon-agent 0.2.7 → 0.2.8
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/agent-manifest-protocol.js +52 -0
- package/dist/agents/peer-manifest-loader.js +210 -0
- package/dist/agents/pi-sdk.js +24 -41
- package/dist/agents/session-store.js +17 -3
- package/dist/agents/shell-guard.js +2 -2
- package/dist/agents/workflow-pivot-loop.js +12 -3
- package/dist/bootstrap/chat-archiver.js +276 -0
- package/dist/bootstrap/context-collector.js +6 -3
- package/dist/bootstrap/lifecycle-hooks.js +15 -1
- package/dist/bootstrap/memory-compressor.js +170 -0
- package/dist/bootstrap/persona-loader.js +94 -0
- package/dist/network/p2p-outbox.js +161 -0
- package/dist/network/peer-fs.js +420 -0
- package/dist/network/peer-resource-bridge.js +216 -0
- package/dist/web/client.js +29 -27
- package/dist/web/components/p2p/index.js +17 -5
- package/dist/web/components/p2p/p2p-modal.js +9 -2
- package/dist/web/server.js +660 -20
- package/dist/web/util/safe-name.js +32 -0
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -74,6 +74,9 @@ import { irohTransport } from '../network/iroh-transport.js';
|
|
|
74
74
|
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
75
75
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
76
76
|
import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
77
|
+
// 2026-07-05: peer 目录管理 + manifest 协议
|
|
78
|
+
import * as peerFs from '../network/peer-fs.js';
|
|
79
|
+
import { loadLocalResources, writeRemoteResources } from '../network/peer-resource-bridge.js';
|
|
77
80
|
// 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
|
|
78
81
|
// - src 跑 (tsx): __dirname = .../src/web → .../dist/web
|
|
79
82
|
// - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
|
|
@@ -285,6 +288,29 @@ let sseClients = new Set();
|
|
|
285
288
|
// v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
|
|
286
289
|
// in-memory only, 进程重启清空 (judgment 内容永远不在这里)
|
|
287
290
|
let remoteChannelCache = new Map();
|
|
291
|
+
// 2026-07-05: 一次性 prompt 附加块 — key: channelId, value: 下一次 LLM prompt 时 prepend 的内容
|
|
292
|
+
// 用于 manifest-loader 加载对方能力后, 仅影响本次对话, 不污染主 prompt
|
|
293
|
+
const nextPromptHints = new Map();
|
|
294
|
+
// 2026-07-05: 直接读 ~/.bolloon/agents/agents.json, 跳过 subagent-manager 的 lazy init
|
|
295
|
+
// 原因: getSubAgentManager().getAllAgents() 只在 SubAgentManager.initialize() 后才加载,
|
|
296
|
+
// 但没人调 initialize(), 永远返回 []. 这里用 raw 文件 IO 直接拿数据.
|
|
297
|
+
async function loadLocalSubAgents() {
|
|
298
|
+
try {
|
|
299
|
+
const fsPromises = await import('fs/promises');
|
|
300
|
+
const path = await import('path');
|
|
301
|
+
const home = process.env.BOLLOON_HOME || process.env.HOME || '/tmp';
|
|
302
|
+
const file = path.join(home, '.bolloon', 'agents', 'agents.json');
|
|
303
|
+
const raw = await fsPromises.readFile(file, 'utf-8');
|
|
304
|
+
const parsed = JSON.parse(raw);
|
|
305
|
+
const arr = Array.isArray(parsed) ? parsed : [];
|
|
306
|
+
process.stderr.write(`[LOADED] ${arr.length} agents from ${file}\n`);
|
|
307
|
+
return arr;
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
process.stderr.write(`[LOAD FAIL] ${e?.message}\n`);
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
288
314
|
// 2026-06-10: 持久化 remote channel cache 到 ~/.bolloon/remote-channels-cache.json
|
|
289
315
|
// 之前是纯内存 Map, nodeA 重启后所有对端 channel 列表丢失, 需要等对面再推一次
|
|
290
316
|
const REMOTE_CACHE_FILE = `${process.env.HOME || '/tmp'}/.bolloon/remote-channels-cache.json`;
|
|
@@ -454,22 +480,25 @@ async function routeMentionsInReply(originChannelId, replyText, localChannels, r
|
|
|
454
480
|
continue;
|
|
455
481
|
}
|
|
456
482
|
try {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (ok) {
|
|
483
|
+
const rpcPayload = {
|
|
484
|
+
targetChannelId: remoteTarget.id,
|
|
485
|
+
targetChannelName: remoteTarget.name,
|
|
486
|
+
originChannelId,
|
|
487
|
+
originChannelName,
|
|
488
|
+
text,
|
|
489
|
+
fromPublicKey: v3P2PRef.getPublicKey()
|
|
490
|
+
};
|
|
491
|
+
// 2026-07-05: 用 outbox.sendOrQueue 兜底 — 对方不在线时自动入队, 上线后批量重发
|
|
492
|
+
const { sendOrQueue } = await import('../network/p2p-outbox.js');
|
|
493
|
+
const r = await sendOrQueue(ownerPk, 'agent.cross.post', rpcPayload, v3P2PRef);
|
|
494
|
+
if (r === 'SENT') {
|
|
470
495
|
console.log(`[v3-cross] (${originChannelName}) @${targetName} → 远端 peer ${ownerPk.substring(0, 12)}... (channelId=${remoteTarget.id})`);
|
|
471
496
|
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'sent' });
|
|
472
497
|
}
|
|
498
|
+
else if (r === 'QUEUED') {
|
|
499
|
+
console.log(`[v3-cross] (${originChannelName}) @${targetName} → 远端 peer ${ownerPk.substring(0, 12)}... 已入队 (对方不在线)`);
|
|
500
|
+
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'queued' });
|
|
501
|
+
}
|
|
473
502
|
else {
|
|
474
503
|
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'failed' });
|
|
475
504
|
}
|
|
@@ -605,6 +634,28 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
605
634
|
catch (saveErr) {
|
|
606
635
|
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, saveErr.message);
|
|
607
636
|
}
|
|
637
|
+
// 2026-07-05: 同时追加到 sender 的 peer 月度归档 — 让 A 本地的"与 sender 对话历史"也能离线查看
|
|
638
|
+
try {
|
|
639
|
+
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
640
|
+
const chanObj = (await loadChannels()).find(c => c.id === channelId);
|
|
641
|
+
await appendChatArchive({
|
|
642
|
+
publicKey: senderKey,
|
|
643
|
+
entry: {
|
|
644
|
+
ts: new Date().toISOString(),
|
|
645
|
+
source: 'remote',
|
|
646
|
+
channelId, channelName: chanObj?.name,
|
|
647
|
+
text,
|
|
648
|
+
fromPublicKey: senderKey,
|
|
649
|
+
msgType: 'user',
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
// 顺手把 sender 加入 known peers (如果没有)
|
|
653
|
+
const { addOrUpdatePeer } = await import('../network/known-peers.js');
|
|
654
|
+
await addOrUpdatePeer(undefined, senderKey);
|
|
655
|
+
}
|
|
656
|
+
catch (archiveErr) {
|
|
657
|
+
console.warn('[chat-archive] remote user 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
658
|
+
}
|
|
608
659
|
// v3 修复: 同步给 A 自己的 UI — broadcast SSE 事件让 A 的 owner 实时看到 B 的消息
|
|
609
660
|
broadcast({
|
|
610
661
|
type: 'user',
|
|
@@ -705,6 +756,25 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
705
756
|
catch (saveErr) {
|
|
706
757
|
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, saveErr.message);
|
|
707
758
|
}
|
|
759
|
+
// 2026-07-05: A 的 assistant 回复也归档到 sender 的月度 — 这样 sender 本地也有一份"与 A 的对话"
|
|
760
|
+
try {
|
|
761
|
+
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
762
|
+
const chanObj = (await loadChannels()).find(c => c.id === channelId);
|
|
763
|
+
await appendChatArchive({
|
|
764
|
+
publicKey: senderKey,
|
|
765
|
+
entry: {
|
|
766
|
+
ts: new Date().toISOString(),
|
|
767
|
+
source: 'ai-mention-remote',
|
|
768
|
+
channelId, channelName: chanObj?.name,
|
|
769
|
+
text: `[${v3P2PRef?.getPublicKey()?.slice(0, 12)}…] ${fullResponse.slice(0, 1500)}`,
|
|
770
|
+
fromPublicKey: v3P2PRef?.getPublicKey(),
|
|
771
|
+
msgType: 'ai',
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (archiveErr) {
|
|
776
|
+
console.warn('[chat-archive] remote ai 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
777
|
+
}
|
|
708
778
|
// v3 修复: 同步给 A 自己的 UI — broadcast AI 回复给 A 的 owner 实时看到
|
|
709
779
|
broadcast({
|
|
710
780
|
type: 'ai',
|
|
@@ -873,6 +943,26 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
873
943
|
session.lastUpdated = new Date().toISOString();
|
|
874
944
|
await saveSession(session);
|
|
875
945
|
console.log(`[v3-cross] 收到远端 @-mention: ${originChannelName} → 本地 ${targetChannelName} (${text.length} chars)`);
|
|
946
|
+
// 2026-07-05: 跨渠道 @-mention 也归档到 fromPublicKey 的月度 — 本节点 owner 看月度历史能还原跨节点对话
|
|
947
|
+
if (fromPublicKey) {
|
|
948
|
+
try {
|
|
949
|
+
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
950
|
+
await appendChatArchive({
|
|
951
|
+
publicKey: fromPublicKey,
|
|
952
|
+
entry: {
|
|
953
|
+
ts: new Date().toISOString(),
|
|
954
|
+
source: 'ai-mention-remote',
|
|
955
|
+
channelId: targetChannelId, channelName: targetChannelName,
|
|
956
|
+
text: `[跨渠道] from ${originChannelName}: ${text.slice(0, 1500)}`,
|
|
957
|
+
fromPublicKey,
|
|
958
|
+
msgType: 'ai',
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
catch (archiveErr) {
|
|
963
|
+
console.warn('[chat-archive] cross.post 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
876
966
|
// 推 SSE 让本地 UI 知道有跨渠道消息到达
|
|
877
967
|
broadcast({
|
|
878
968
|
type: 'cross-mention-received',
|
|
@@ -887,6 +977,201 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
887
977
|
}
|
|
888
978
|
return;
|
|
889
979
|
}
|
|
980
|
+
// ============== 2026-07-05: agent.manifest.exchange ==============
|
|
981
|
+
// 对方问: "给我你的 agent 清单 + capabilities"
|
|
982
|
+
// 我们回: agent.manifest.exchange.reply, 含本地 SubAgent + persona 描述
|
|
983
|
+
if (op === 'agent.manifest.exchange') {
|
|
984
|
+
try {
|
|
985
|
+
// 直接读 agents.json, 跳过 subagent-manager 的 lazy initialize
|
|
986
|
+
const localAgents = await loadLocalSubAgents();
|
|
987
|
+
// 转成 AgentManifestEntry
|
|
988
|
+
const entries = localAgents.map((a) => ({
|
|
989
|
+
id: a.id,
|
|
990
|
+
name: a.name,
|
|
991
|
+
capabilities: a.capabilities || [],
|
|
992
|
+
status: a.status || 'active',
|
|
993
|
+
peerId: a.peerId,
|
|
994
|
+
irohNodeId: a.irohNodeId,
|
|
995
|
+
sessionId: a.sessionId,
|
|
996
|
+
cid: a.cid,
|
|
997
|
+
ipnsName: a.ipnsName,
|
|
998
|
+
}));
|
|
999
|
+
// 读本地 persona 拿 owner 名字 + 简介
|
|
1000
|
+
let ownerName = '';
|
|
1001
|
+
let ownerDescription = '';
|
|
1002
|
+
try {
|
|
1003
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
1004
|
+
const p = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
|
|
1005
|
+
if (existsSync(p)) {
|
|
1006
|
+
const pj = JSON.parse(readFileSync(p, 'utf-8'));
|
|
1007
|
+
ownerName = pj.name || '';
|
|
1008
|
+
ownerDescription = pj.description || '';
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch { }
|
|
1012
|
+
const manifest = {
|
|
1013
|
+
ownerName,
|
|
1014
|
+
ownerDescription,
|
|
1015
|
+
ownerPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
1016
|
+
agents: entries,
|
|
1017
|
+
publishedAt: Date.now(),
|
|
1018
|
+
...(await loadLocalResources()),
|
|
1019
|
+
};
|
|
1020
|
+
const reply = JSON.stringify({
|
|
1021
|
+
v: 3, op: 'agent.manifest.exchange.reply',
|
|
1022
|
+
payload: {
|
|
1023
|
+
manifest,
|
|
1024
|
+
since: parsed.payload?.since || 0,
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
await comm.sendToConnection(conn.id, reply);
|
|
1028
|
+
// 2026-07-05: P2PDirect.sendToWithWait 双发 (兼容同机 P2PDirect 互不相连场景)
|
|
1029
|
+
if (v3P2PRef && peerKey) {
|
|
1030
|
+
try {
|
|
1031
|
+
const r = await v3P2PRef.sendToWithWait(peerKey, reply, 2000);
|
|
1032
|
+
console.log(`[v3-manifest] 回 ${peerKey.substring(0, 12)}... manifest (comm=${await comm.sendToConnection.length}, p2p=${r}, ${entries.length} agents)`);
|
|
1033
|
+
}
|
|
1034
|
+
catch { }
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
console.log(`[v3-manifest] 回 ${peerKey.substring(0, 12)}... manifest (${entries.length} agents, owner=${ownerName || '?'})`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch (err) {
|
|
1041
|
+
console.error('[v3-manifest] 处理 agent.manifest.exchange 失败:', err.message);
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
// 对方推 manifest 过来 (我们这边是接收方)
|
|
1046
|
+
if (op === 'agent.manifest.exchange.reply') {
|
|
1047
|
+
try {
|
|
1048
|
+
const manifest = parsed.payload?.manifest;
|
|
1049
|
+
if (!manifest || !manifest.ownerPublicKey) {
|
|
1050
|
+
console.warn('[v3-manifest] manifest.exchange.reply 缺 manifest/ownerPublicKey');
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
// 2026-07-05 修复: 跳过自己的 manifest (Hyperswarm 内部 peer discovery 会推送自己)
|
|
1054
|
+
const peerKey2 = manifest.ownerPublicKey;
|
|
1055
|
+
if (v3P2PRef && peerKey2 === v3P2PRef.getPublicKey()) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// 增量: 如果 since >= 本地最新 ts, 跳过
|
|
1059
|
+
const existing = await peerFs.readPeerIndex(peerKey2);
|
|
1060
|
+
if (existing?.manifestTs && existing.manifestTs >= manifest.publishedAt && parsed.payload?.since) {
|
|
1061
|
+
console.log(`[v3-manifest] ${peerKey2.substring(0, 12)}... manifest 已是最新 (ts=${manifest.publishedAt}), 跳过`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
// 写 peer.json + _index.json + 每个 agent 的 md
|
|
1065
|
+
await peerFs.upsertPeer({
|
|
1066
|
+
publicKey: peerKey2,
|
|
1067
|
+
name: manifest.ownerName,
|
|
1068
|
+
lastSeenAt: new Date().toISOString(),
|
|
1069
|
+
lastManifestTs: manifest.publishedAt,
|
|
1070
|
+
manifestCount: (await peerFs.getPeer(peerKey2))?.manifestCount ?? 0 + 1,
|
|
1071
|
+
});
|
|
1072
|
+
// 拼 _index.json
|
|
1073
|
+
const ownerDescription = manifest.ownerDescription || '';
|
|
1074
|
+
const idx = {
|
|
1075
|
+
version: 1,
|
|
1076
|
+
publicKey: peerKey2,
|
|
1077
|
+
ownerName: manifest.ownerName,
|
|
1078
|
+
ownerDescription,
|
|
1079
|
+
agents: manifest.agents,
|
|
1080
|
+
groups: manifest.groups,
|
|
1081
|
+
functions: manifest.functions,
|
|
1082
|
+
exportments: manifest.exportments,
|
|
1083
|
+
updatedAt: new Date().toISOString(),
|
|
1084
|
+
manifestTs: manifest.publishedAt,
|
|
1085
|
+
};
|
|
1086
|
+
await peerFs.writePeerIndex(peerKey2, idx);
|
|
1087
|
+
// 每个 agent 写一份 markdown
|
|
1088
|
+
for (const a of manifest.agents) {
|
|
1089
|
+
await peerFs.writeAgentDescription(peerKey2, a, ownerDescription);
|
|
1090
|
+
}
|
|
1091
|
+
// 2026-07-05: 4 类资源 (groups/function/exportment/science) 也落盘
|
|
1092
|
+
const counts = await writeRemoteResources(peerKey2, manifest);
|
|
1093
|
+
// capability-index.md (≤500 字, 进 prompt)
|
|
1094
|
+
await peerFs.writeCapabilityIndex(peerKey2, idx);
|
|
1095
|
+
console.log(`[v3-manifest] 收到 ${peerKey2.substring(0, 12)}... manifest (${manifest.agents.length} agents, owner=${manifest.ownerName || '?'}, +g${counts.groups}/f${counts.functions}/e${counts.exportments}/s${counts.sciences}) → 落盘`);
|
|
1096
|
+
// 推 SSE 让前端知道"对方能力刷新了"
|
|
1097
|
+
broadcast({
|
|
1098
|
+
type: 'peer-manifest-updated',
|
|
1099
|
+
fromPublicKey: peerKey2,
|
|
1100
|
+
ownerName: manifest.ownerName,
|
|
1101
|
+
agentCount: manifest.agents.length,
|
|
1102
|
+
capabilityIndex: await peerFs.readCapabilityIndex(peerKey2),
|
|
1103
|
+
}, 'p2p-global');
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
console.error('[v3-manifest] 处理 manifest.exchange.reply 失败:', err.message);
|
|
1107
|
+
}
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
// 对方问: "给我某个 agent 的详细描述" (markdown 全文)
|
|
1111
|
+
if (op === 'agent.resource.get') {
|
|
1112
|
+
try {
|
|
1113
|
+
const agentId = parsed.payload?.agentId;
|
|
1114
|
+
if (!agentId)
|
|
1115
|
+
return;
|
|
1116
|
+
// 2026-07-05: 4 类资源 (group/function/exportment/science) 也走同一个 RPC
|
|
1117
|
+
// 识别方式: id 前缀 group:/fn:/game:/exp: → 读 ~/.bolloon/local-resources/<cat>/<id>.md
|
|
1118
|
+
let body = '';
|
|
1119
|
+
const prefixMatch = String(agentId).match(/^(group|fn|game|exp):(.+)$/);
|
|
1120
|
+
if (prefixMatch) {
|
|
1121
|
+
const cat = prefixMatch[1] === 'fn' ? 'functions'
|
|
1122
|
+
: prefixMatch[1] === 'game' ? 'exportments'
|
|
1123
|
+
: prefixMatch[1] === 'exp' ? 'sciences'
|
|
1124
|
+
: 'groups';
|
|
1125
|
+
try {
|
|
1126
|
+
const fsPromises = await import('fs/promises');
|
|
1127
|
+
const safe = String(prefixMatch[2]).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
1128
|
+
const file = path.join(process.env.HOME || '/tmp', '.bolloon', 'local-resources', cat, `${safe}.md`);
|
|
1129
|
+
body = await fsPromises.readFile(file, 'utf-8');
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
body = '';
|
|
1133
|
+
}
|
|
1134
|
+
const reply = JSON.stringify({
|
|
1135
|
+
v: 3, op: 'agent.resource.get.reply',
|
|
1136
|
+
payload: { agentId, body: body || '(空)', fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
1137
|
+
});
|
|
1138
|
+
await comm.sendToConnection(conn.id, reply);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
// 默认: 读本地 persona/<agentId>/agent.md (6 段 persona 之一)
|
|
1142
|
+
try {
|
|
1143
|
+
const fsPromises = await import('fs/promises');
|
|
1144
|
+
const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
1145
|
+
const file = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona', safeId, 'agent.md');
|
|
1146
|
+
body = await fsPromises.readFile(file, 'utf-8');
|
|
1147
|
+
}
|
|
1148
|
+
catch {
|
|
1149
|
+
body = '';
|
|
1150
|
+
}
|
|
1151
|
+
// fallback: 用 subagent-manager 的 description
|
|
1152
|
+
if (!body) {
|
|
1153
|
+
const all = await loadLocalSubAgents();
|
|
1154
|
+
const a = all.find((x) => x.id === agentId);
|
|
1155
|
+
if (a?.persona?.description)
|
|
1156
|
+
body = a.persona.description;
|
|
1157
|
+
}
|
|
1158
|
+
const reply = JSON.stringify({
|
|
1159
|
+
v: 3, op: 'agent.resource.get.reply',
|
|
1160
|
+
payload: { agentId, body: body || '(空)', fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
1161
|
+
});
|
|
1162
|
+
await comm.sendToConnection(conn.id, reply);
|
|
1163
|
+
}
|
|
1164
|
+
catch (err) {
|
|
1165
|
+
console.error('[v3-manifest] resource.get 失败:', err.message);
|
|
1166
|
+
}
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
// 对方回某 agent 的详细描述
|
|
1170
|
+
if (op === 'agent.resource.get.reply') {
|
|
1171
|
+
// 由 caller 端通过 rpcId 解析 (这里只 log)
|
|
1172
|
+
console.log(`[v3-manifest] 收到 agent.resource.get.reply (agentId=${parsed.payload?.agentId}, body=${(parsed.payload?.body || '').length} chars)`);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
890
1175
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
891
1176
|
}
|
|
892
1177
|
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
@@ -1008,6 +1293,8 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
1008
1293
|
cwd: process.cwd(),
|
|
1009
1294
|
peerId: `channel-${channelId}:${currentSessionId}`,
|
|
1010
1295
|
identityDoc,
|
|
1296
|
+
// 2026-07-04: 透传 agentId 让 onSessionStart 加载 persona docs
|
|
1297
|
+
agentId: channel?.agentId,
|
|
1011
1298
|
// M2.3 (2026-06-17): 构造时从 session JSON 回灌历史 — 服务重启后 LLM 仍记得前面对话
|
|
1012
1299
|
// key 跟 server.ts:240 写入路径保持一致: ~/.bolloon/sessions/cache/<key>.json
|
|
1013
1300
|
loadSessionKey: sessionKey,
|
|
@@ -1255,6 +1542,66 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1255
1542
|
}, 'p2p-global');
|
|
1256
1543
|
return;
|
|
1257
1544
|
}
|
|
1545
|
+
// 2026-07-05: v3 P2PDirect 主路径也要独立处理 manifest.exchange.reply — 否则只走老通道就拿不到
|
|
1546
|
+
if (parsed.op === 'agent.manifest.exchange.reply') {
|
|
1547
|
+
const manifest = parsed.payload?.manifest;
|
|
1548
|
+
if (!manifest || !manifest.ownerPublicKey) {
|
|
1549
|
+
console.warn('[v3-manifest] manifest.exchange.reply 缺 manifest/ownerPublicKey');
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const peerKey2 = manifest.ownerPublicKey;
|
|
1553
|
+
// 2026-07-05 修复: 跳过自己的 manifest
|
|
1554
|
+
if (v3P2PRef && peerKey2 === v3P2PRef.getPublicKey()) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
// 异步落盘, 不阻塞 data handler
|
|
1558
|
+
(async () => {
|
|
1559
|
+
try {
|
|
1560
|
+
const existing = await peerFs.readPeerIndex(peerKey2);
|
|
1561
|
+
if (existing?.manifestTs && existing.manifestTs >= manifest.publishedAt && parsed.payload?.since) {
|
|
1562
|
+
console.log(`[v3-manifest] ${peerKey2.substring(0, 12)}... manifest 已是最新, 跳过`);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
await peerFs.upsertPeer({
|
|
1566
|
+
publicKey: peerKey2,
|
|
1567
|
+
name: manifest.ownerName,
|
|
1568
|
+
lastSeenAt: new Date().toISOString(),
|
|
1569
|
+
lastManifestTs: manifest.publishedAt,
|
|
1570
|
+
});
|
|
1571
|
+
const ownerDescription = manifest.ownerDescription || '';
|
|
1572
|
+
const idx = {
|
|
1573
|
+
version: 1,
|
|
1574
|
+
publicKey: peerKey2,
|
|
1575
|
+
ownerName: manifest.ownerName,
|
|
1576
|
+
ownerDescription,
|
|
1577
|
+
agents: manifest.agents,
|
|
1578
|
+
groups: manifest.groups,
|
|
1579
|
+
functions: manifest.functions,
|
|
1580
|
+
exportments: manifest.exportments,
|
|
1581
|
+
updatedAt: new Date().toISOString(),
|
|
1582
|
+
manifestTs: manifest.publishedAt,
|
|
1583
|
+
};
|
|
1584
|
+
await peerFs.writePeerIndex(peerKey2, idx);
|
|
1585
|
+
for (const a of manifest.agents) {
|
|
1586
|
+
await peerFs.writeAgentDescription(peerKey2, a, ownerDescription);
|
|
1587
|
+
}
|
|
1588
|
+
const counts = await writeRemoteResources(peerKey2, manifest);
|
|
1589
|
+
await peerFs.writeCapabilityIndex(peerKey2, idx);
|
|
1590
|
+
console.log(`[v3-manifest] (P2PDirect) 收到 ${peerKey2.substring(0, 12)}... manifest (${manifest.agents.length} agents, owner=${manifest.ownerName || '?'}, +g${counts.groups}/f${counts.functions}/e${counts.exportments}/s${counts.sciences}) → 落盘`);
|
|
1591
|
+
broadcast({
|
|
1592
|
+
type: 'peer-manifest-updated',
|
|
1593
|
+
fromPublicKey: peerKey2,
|
|
1594
|
+
ownerName: manifest.ownerName,
|
|
1595
|
+
agentCount: manifest.agents.length,
|
|
1596
|
+
capabilityIndex: await peerFs.readCapabilityIndex(peerKey2),
|
|
1597
|
+
}, 'p2p-global');
|
|
1598
|
+
}
|
|
1599
|
+
catch (err) {
|
|
1600
|
+
console.error('[v3-manifest] (P2PDirect) manifest.exchange.reply 失败:', err.message);
|
|
1601
|
+
}
|
|
1602
|
+
})();
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1258
1605
|
// 2026-06-10: 收到对方请求本机的 channel 列表 (启动时主动发请求, 加速 cache 填充)
|
|
1259
1606
|
if (parsed.op === 'agent.meta.list.request') {
|
|
1260
1607
|
console.log(`[v3-meta] 收到 ${evt.fromPublicKey.substring(0, 12)}... 的 channel 列表请求 → 立刻回包`);
|
|
@@ -1296,6 +1643,47 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1296
1643
|
console.error('[v3] 新连接发 list.reply 失败:', err.message);
|
|
1297
1644
|
}
|
|
1298
1645
|
}, 500);
|
|
1646
|
+
// 2026-07-05: 新连接到来立刻推自己的 manifest — 让对方秒知道我有哪些 agent
|
|
1647
|
+
setTimeout(async () => {
|
|
1648
|
+
try {
|
|
1649
|
+
// 跳过自己的连接 (Hyperswarm 内部 peer discovery 会推自己过来, 形成循环)
|
|
1650
|
+
if (v3P2PRef && evt.remotePublicKey === v3P2PRef.getPublicKey()) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const localAgents = await loadLocalSubAgents();
|
|
1654
|
+
const entries = localAgents.map((a) => ({
|
|
1655
|
+
id: a.id, name: a.name,
|
|
1656
|
+
capabilities: a.capabilities || [],
|
|
1657
|
+
status: a.status || 'active',
|
|
1658
|
+
peerId: a.peerId, irohNodeId: a.irohNodeId,
|
|
1659
|
+
sessionId: a.sessionId, cid: a.cid, ipnsName: a.ipnsName,
|
|
1660
|
+
}));
|
|
1661
|
+
let ownerName = '';
|
|
1662
|
+
try {
|
|
1663
|
+
const fsPromises = await import('fs/promises');
|
|
1664
|
+
const p = (process.env.HOME || '/tmp') + '/.bolloon/persona.json';
|
|
1665
|
+
const raw = await fsPromises.readFile(p, 'utf-8');
|
|
1666
|
+
ownerName = JSON.parse(raw).name || '';
|
|
1667
|
+
}
|
|
1668
|
+
catch { }
|
|
1669
|
+
const manifest = {
|
|
1670
|
+
ownerName,
|
|
1671
|
+
ownerPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
1672
|
+
agents: entries,
|
|
1673
|
+
publishedAt: Date.now(),
|
|
1674
|
+
...(await loadLocalResources()),
|
|
1675
|
+
};
|
|
1676
|
+
const msg = JSON.stringify({
|
|
1677
|
+
v: 3, op: 'agent.manifest.exchange.reply',
|
|
1678
|
+
payload: { manifest, since: 0 }
|
|
1679
|
+
});
|
|
1680
|
+
const r = await v3P2PRef.sendToWithWait(evt.remotePublicKey, msg, 3000);
|
|
1681
|
+
console.log(`[v3-manifest] 新连接 ${evt.remotePublicKey.substring(0, 12)}... → 主动推 manifest (${entries.length} agents, p2p=${r})`);
|
|
1682
|
+
}
|
|
1683
|
+
catch (err) {
|
|
1684
|
+
console.error('[v3-manifest] 新连接推 manifest 失败:', err.message);
|
|
1685
|
+
}
|
|
1686
|
+
}, 800);
|
|
1299
1687
|
});
|
|
1300
1688
|
console.log(`[v3] P2PDirect 已启动, role=${v3P2PRef.getRole()}, publicKey=${v3P2PRef.getPublicKey().substring(0, 12)}...`);
|
|
1301
1689
|
// v3: 启动后自动重连 known peers — 让"启动就互联"成为现实
|
|
@@ -1325,6 +1713,10 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1325
1713
|
// 2026-06-10: 同时主动请求每个 known peer 把 ta 的 channel 列表推过来
|
|
1326
1714
|
// 避免对面 publicKey 没变但 cache 丢了(本机重启) → 一直空
|
|
1327
1715
|
setTimeout(() => requestChannelsFromAllPeers(), 3500);
|
|
1716
|
+
// 2026-07-05: 拉每个 known peer 的 manifest (agent 能力清单)
|
|
1717
|
+
setTimeout(() => requestManifestsFromAllPeers(), 4500);
|
|
1718
|
+
// 2026-07-05: 对方刚连上来, 把本地的 outbox 重发出去
|
|
1719
|
+
setTimeout(() => flushAllOutboxes(), 6000);
|
|
1328
1720
|
}
|
|
1329
1721
|
catch (err) {
|
|
1330
1722
|
console.error('[v3] 自动重连失败:', err.message);
|
|
@@ -1357,6 +1749,78 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1357
1749
|
// 立即跑一次 + 每 30s 兜底 (跟 v3BroadcastOwn 一样的节奏)
|
|
1358
1750
|
setTimeout(requestChannelsFromAllPeers, 4000);
|
|
1359
1751
|
setInterval(requestChannelsFromAllPeers, 30000);
|
|
1752
|
+
// 2026-07-05 新增: 主动向所有 known peer 发起 manifest 拉取
|
|
1753
|
+
async function requestManifestsFromAllPeers() {
|
|
1754
|
+
if (!v3P2PRef)
|
|
1755
|
+
return;
|
|
1756
|
+
try {
|
|
1757
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
1758
|
+
const peers = await listPeers();
|
|
1759
|
+
const myPk = v3P2PRef.getPublicKey();
|
|
1760
|
+
let sent = 0;
|
|
1761
|
+
for (const peer of peers) {
|
|
1762
|
+
if (peer.publicKey === myPk)
|
|
1763
|
+
continue;
|
|
1764
|
+
// 增量: since = 本地已有的 lastManifestTs, 对端 manifestTs <= since 则跳过
|
|
1765
|
+
const peerRec = await peerFs.getPeer(peer.publicKey);
|
|
1766
|
+
const since = peerRec?.lastManifestTs || 0;
|
|
1767
|
+
const req = JSON.stringify({
|
|
1768
|
+
v: 3, op: 'agent.manifest.exchange',
|
|
1769
|
+
payload: { since, fromPublicKey: myPk }
|
|
1770
|
+
});
|
|
1771
|
+
const r = await v3P2PRef.sendToWithWait(peer.publicKey, req, 3000);
|
|
1772
|
+
if (r === 'SENT')
|
|
1773
|
+
sent++;
|
|
1774
|
+
}
|
|
1775
|
+
console.log(`[v3-manifest] requestManifestsFromAllPeers → sent=${sent}/${peers.length - 1}`);
|
|
1776
|
+
}
|
|
1777
|
+
catch (err) {
|
|
1778
|
+
console.warn('[v3-manifest] requestManifestsFromAllPeers failed:', err.message);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
// 每 5 分钟兜底 (manifest 增量拉取)
|
|
1782
|
+
setInterval(requestManifestsFromAllPeers, 5 * 60 * 1000);
|
|
1783
|
+
// 2026-07-05 新增: 把每个 peer 的 outbox 队列重发出去 (对方上线后)
|
|
1784
|
+
async function flushAllOutboxes() {
|
|
1785
|
+
if (!v3P2PRef)
|
|
1786
|
+
return;
|
|
1787
|
+
try {
|
|
1788
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
1789
|
+
const peers = await listPeers();
|
|
1790
|
+
const myPk = v3P2PRef.getPublicKey();
|
|
1791
|
+
let totalFlushed = 0;
|
|
1792
|
+
for (const peer of peers) {
|
|
1793
|
+
if (peer.publicKey === myPk)
|
|
1794
|
+
continue;
|
|
1795
|
+
const outbox = await peerFs.readOutbox(peer.publicKey);
|
|
1796
|
+
if (outbox.length === 0)
|
|
1797
|
+
continue;
|
|
1798
|
+
let sent = 0;
|
|
1799
|
+
for (const entry of outbox) {
|
|
1800
|
+
const rpc = JSON.stringify({ v: 3, op: entry.op, payload: entry.payload });
|
|
1801
|
+
const r = await v3P2PRef.sendToWithWait(peer.publicKey, rpc, 3000);
|
|
1802
|
+
if (r === 'SENT')
|
|
1803
|
+
sent++;
|
|
1804
|
+
}
|
|
1805
|
+
if (sent > 0) {
|
|
1806
|
+
// 全部成功 → 清空; 部分成功 → 写回剩余 (简单起见, 全成功才清)
|
|
1807
|
+
if (sent === outbox.length) {
|
|
1808
|
+
await peerFs.clearOutbox(peer.publicKey);
|
|
1809
|
+
totalFlushed += sent;
|
|
1810
|
+
console.log(`[v3-outbox] flush → ${peer.publicKey.substring(0, 12)}... 重发 ${sent} 条 ✓`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (totalFlushed > 0) {
|
|
1815
|
+
console.log(`[v3-outbox] 累计重发 ${totalFlushed} 条离线消息`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
catch (err) {
|
|
1819
|
+
console.warn('[v3-outbox] flushAllOutboxes failed:', err.message);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
// 每 2 分钟兜底 flush
|
|
1823
|
+
setInterval(flushAllOutboxes, 2 * 60 * 1000);
|
|
1360
1824
|
}
|
|
1361
1825
|
catch (err) {
|
|
1362
1826
|
console.error('[v3] P2PDirect 启动失败:', err.message);
|
|
@@ -1367,6 +1831,9 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1367
1831
|
// v3 修复: 用 setInterval 替代一次性 setTimeout, 确保分享变更后能持续推送给 peer
|
|
1368
1832
|
setInterval(v3BroadcastOwn, 30000);
|
|
1369
1833
|
// 保留 @diap/sdk 的旧实例 (它的 Hyperswarm 实例能帮 P2PDirect 做 DHT bootstrap)
|
|
1834
|
+
// 2026-07-04: hyperswarm 4.x 删除了 Discovery.update() (被 .flushed() 替代).
|
|
1835
|
+
// @diap/sdk 0.1.10 还调 .update(), 是上游 bug (已记录 docs/plans/2026-06-17-supervisor-iter-1.md).
|
|
1836
|
+
// 这里静默 joinTopic 失败, P2PDirect (v3 主路径) 不依赖 @diap/sdk, 不影响功能.
|
|
1370
1837
|
try {
|
|
1371
1838
|
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
1372
1839
|
p2pCommunicator = createHyperswarmCommunicator({
|
|
@@ -1380,13 +1847,38 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1380
1847
|
// 旧 p2p_message 路径 (非 v3)
|
|
1381
1848
|
const content = new TextDecoder().decode(msg.content);
|
|
1382
1849
|
broadcast({ type: 'p2p_message', from: conn.publicKey.substring(0, 8), content }, undefined);
|
|
1850
|
+
// 2026-07-05: 老通道也要走 v3 RPC handler — 兼容同机/跨机 P2PDirect 互不相连的场景
|
|
1851
|
+
// 实测: P2PDirect 用 hyperswarm 4.x, 同机 2 role 互不相连; 但 @diap/sdk 0.1.10 的 HyperswarmCommunicator 能互通
|
|
1852
|
+
// 所以 manifest/exchange/chat.send 都可能从老通道来, 必须进 handleV3P2PMessage
|
|
1853
|
+
try {
|
|
1854
|
+
const parsed = JSON.parse(content);
|
|
1855
|
+
if (parsed && parsed.v === 3 && parsed.op && p2pCommunicator) {
|
|
1856
|
+
await handleV3P2PMessage(parsed, conn, p2pCommunicator);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
catch (err) {
|
|
1860
|
+
// 非 v3 JSON 帧 (老 p2p_message), 静默忽略
|
|
1861
|
+
}
|
|
1383
1862
|
});
|
|
1384
1863
|
await p2pCommunicator.start();
|
|
1385
1864
|
// @diap/sdk 也 join topic — 它的 Hyperswarm 实例帮 P2PDirect 做 DHT 引导
|
|
1386
|
-
// @diap/sdk
|
|
1865
|
+
// joinTopic 失败已知 (hyperswarm 4.x + @diap/sdk 0.1.10), catch 静默
|
|
1387
1866
|
const oldTopic = createTopic('bolloon-agent-harness');
|
|
1388
|
-
|
|
1389
|
-
|
|
1867
|
+
try {
|
|
1868
|
+
await p2pCommunicator.joinTopic(oldTopic);
|
|
1869
|
+
console.log(`P2P 老通道已就绪 (DHT bootstrap 帮 P2PDirect, 实际数据走 P2PDirect)`);
|
|
1870
|
+
}
|
|
1871
|
+
catch (joinErr) {
|
|
1872
|
+
// 已知: @diap/sdk 0.1.10 + hyperswarm 4.x → discovery.update is not a function
|
|
1873
|
+
// v3 P2PDirect 是主路径, 此处不阻断
|
|
1874
|
+
const msg = String(joinErr?.message || joinErr);
|
|
1875
|
+
if (msg.includes('discovery.update') || msg.includes('is not a function')) {
|
|
1876
|
+
console.warn(`[v3-legacy] @diap/sdk joinTopic 失败 (已知 hyperswarm 4.x 兼容问题, 已忽略): ${msg}`);
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
throw joinErr;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1390
1882
|
}
|
|
1391
1883
|
catch (e) {
|
|
1392
1884
|
console.log(`P2P 老通道初始化失败: ${e.message}`);
|
|
@@ -1567,6 +2059,14 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1567
2059
|
}
|
|
1568
2060
|
runState.running = true;
|
|
1569
2061
|
runState.abortController = new AbortController();
|
|
2062
|
+
// 2026-07-04: pivot loop safety net — 60s 强制 timeout 防止 LLM hang (minimax M3
|
|
2063
|
+
// 偶尔反复 think 不输出 <final gen>, pivot 连 5 次无进展时会 hang 在
|
|
2064
|
+
// quality 评估). setTimeout 让 LLM 客户端收 signal 主动 break.
|
|
2065
|
+
const PIVOT_FORCE_TIMEOUT_MS = 30_000;
|
|
2066
|
+
const forceTimeout = setTimeout(() => {
|
|
2067
|
+
console.warn(`[server] /message pivot 强制 timeout (${PIVOT_FORCE_TIMEOUT_MS}ms), aborting`);
|
|
2068
|
+
runState.abortController?.abort();
|
|
2069
|
+
}, PIVOT_FORCE_TIMEOUT_MS);
|
|
1570
2070
|
broadcastQueueUpdate(channelId);
|
|
1571
2071
|
// 2026-07-01 (v0.2.5): hoist sessionKey 到 try 外, 让 finally 块的 saveCurrentSession 能用
|
|
1572
2072
|
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
@@ -1798,7 +2298,14 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1798
2298
|
// 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
|
|
1799
2299
|
// 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
|
|
1800
2300
|
// 的 "..." 误判为整个 input 截断)
|
|
1801
|
-
|
|
2301
|
+
// 2026-07-05: prepend nextPromptHints (manifest-loader 加载的对方能力, 仅本次生效)
|
|
2302
|
+
let extraHint = '';
|
|
2303
|
+
const hint = nextPromptHints.get(channelId);
|
|
2304
|
+
if (hint) {
|
|
2305
|
+
extraHint = hint + '\n\n';
|
|
2306
|
+
nextPromptHints.delete(channelId);
|
|
2307
|
+
}
|
|
2308
|
+
const markedPrompt = `${extraHint}【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
|
|
1802
2309
|
fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
|
|
1803
2310
|
}
|
|
1804
2311
|
catch (err) {
|
|
@@ -1814,8 +2321,12 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1814
2321
|
broadcast({ type: 'error', content: fullResponse }, channelId);
|
|
1815
2322
|
}
|
|
1816
2323
|
}
|
|
1817
|
-
// abort 模式: 给 partial 拼后缀
|
|
1818
|
-
|
|
2324
|
+
// abort 模式: 给 partial 拼后缀 — 只在 LLM 真有输出时加中断标记,
|
|
2325
|
+
// 跳过空响应 + 跳过 [AI 服务调用失败] fallback (这些本身就是错误占位, 拼
|
|
2326
|
+
// "[生成已中断]" 会误导用户)
|
|
2327
|
+
const hasRealLlmOutput = fullResponse.trim().length > 0 &&
|
|
2328
|
+
!fullResponse.trimStart().startsWith('[AI 服务调用失败]');
|
|
2329
|
+
if (runState.abortController?.signal.aborted && hasRealLlmOutput) {
|
|
1819
2330
|
fullResponse = fullResponse + '\n\n_[生成已中断]_';
|
|
1820
2331
|
}
|
|
1821
2332
|
// 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
|
|
@@ -1839,6 +2350,117 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1839
2350
|
});
|
|
1840
2351
|
session.lastUpdated = new Date().toISOString();
|
|
1841
2352
|
await saveSession(session);
|
|
2353
|
+
// 2026-07-04: 写 session 后, 调 memory-compressor 把消息历史压缩到
|
|
2354
|
+
// ~/.bolloon/memory/<channel.agentId>/sessions/<safe-channel>__<safe-session>.summary.md
|
|
2355
|
+
// 失败静默, 不阻塞 /message 主路径.
|
|
2356
|
+
try {
|
|
2357
|
+
const channelsForMemory = await loadChannels();
|
|
2358
|
+
const channelForMemory = channelsForMemory.find(c => c.id === channelId);
|
|
2359
|
+
if (channelForMemory?.agentId) {
|
|
2360
|
+
const { compressSessionToMemory } = await import('../bootstrap/memory-compressor.js');
|
|
2361
|
+
const compressRes = await compressSessionToMemory({
|
|
2362
|
+
agentId: channelForMemory.agentId,
|
|
2363
|
+
channelId,
|
|
2364
|
+
sessionId: currentSessionId,
|
|
2365
|
+
});
|
|
2366
|
+
if (!compressRes.skipped && compressRes.bytesWritten > 0) {
|
|
2367
|
+
console.log(`[memory] compressed ${compressRes.messagesCount} new messages for ${channelForMemory.agentId}/${channelId} → ${compressRes.summaryPath} (${compressRes.bytesWritten}B)`);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
catch (memErr) {
|
|
2372
|
+
console.warn('[memory] compressSessionToMemory failed (non-fatal):', memErr?.message?.slice(0, 200));
|
|
2373
|
+
}
|
|
2374
|
+
// 2026-07-05: 把 session 里的 user/ai 消息按涉及到的远端 peer 归档到月度 markdown
|
|
2375
|
+
// 触发条件: 消息里出现过 @远端 channel, 或者 session 历史上与该 peer 通信过
|
|
2376
|
+
// 失败静默, 不阻塞主对话
|
|
2377
|
+
try {
|
|
2378
|
+
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
2379
|
+
// 找到这条消息涉及到的远端 peer (从 routeMentionsInReply 的结果推断 — 简单起见直接遍历 remoteChannels)
|
|
2380
|
+
const localChannels2 = (await loadChannels());
|
|
2381
|
+
const remoteChannels2 = [];
|
|
2382
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
2383
|
+
for (const ch of list) {
|
|
2384
|
+
remoteChannels2.push({ ...ch, _ownerPublicKey: peerPk });
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
// 找出本次 text/fullResponse 里出现 @ 的 channel 对应的 owner pk
|
|
2388
|
+
const peerSet = new Set();
|
|
2389
|
+
const mentionRe = /@([一-龥A-Za-z0-9_\-]{1,30})/g;
|
|
2390
|
+
const mentionedNames = new Set();
|
|
2391
|
+
for (const m of text.matchAll(mentionRe))
|
|
2392
|
+
mentionedNames.add(m[1]);
|
|
2393
|
+
for (const m of fullResponse.matchAll(mentionRe))
|
|
2394
|
+
mentionedNames.add(m[1]);
|
|
2395
|
+
for (const rc of remoteChannels2) {
|
|
2396
|
+
if (mentionedNames.has(rc.name))
|
|
2397
|
+
peerSet.add(rc._ownerPublicKey);
|
|
2398
|
+
}
|
|
2399
|
+
// 追加到每个相关 peer 的月度归档
|
|
2400
|
+
for (const pk of peerSet) {
|
|
2401
|
+
await appendChatArchive({
|
|
2402
|
+
publicKey: pk,
|
|
2403
|
+
entry: {
|
|
2404
|
+
ts: new Date().toISOString(),
|
|
2405
|
+
source: 'local',
|
|
2406
|
+
channelId, channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
|
|
2407
|
+
text: `[本节点] user: ${text.slice(0, 500)}\n[本节点] ai: ${fullResponse.slice(0, 800)}`,
|
|
2408
|
+
fromPublicKey: v3P2PRef?.getPublicKey(),
|
|
2409
|
+
msgType: 'user',
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
catch (archiveErr) {
|
|
2415
|
+
console.warn('[chat-archive] append failed (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
2416
|
+
}
|
|
2417
|
+
// 2026-07-05: 懒加载触发器 — 探测到 @-mention 远端 / 连续失败 / 关键词, 拉对方 manifest
|
|
2418
|
+
// 把 capability-index 拼到下一次 prompt 上下文 (只生效 1 次, 不污染主循环)
|
|
2419
|
+
try {
|
|
2420
|
+
const { detectLoadTrigger, loadPeerManifest, clearFailure } = await import('../agents/peer-manifest-loader.js');
|
|
2421
|
+
const remoteChannelsForDetect = [];
|
|
2422
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
2423
|
+
for (const ch of list) {
|
|
2424
|
+
remoteChannelsForDetect.push({ ...ch, _ownerPublicKey: peerPk });
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
// 检测 + 加载
|
|
2428
|
+
const detection = detectLoadTrigger({
|
|
2429
|
+
text,
|
|
2430
|
+
channelId,
|
|
2431
|
+
remoteChannels: remoteChannelsForDetect,
|
|
2432
|
+
});
|
|
2433
|
+
if (detection.shouldLoad && detection.remotePublicKey && v3P2PRef) {
|
|
2434
|
+
const result = await loadPeerManifest({
|
|
2435
|
+
channelId,
|
|
2436
|
+
channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
|
|
2437
|
+
reason: detection.reason,
|
|
2438
|
+
triggerValue: detection.triggerValue,
|
|
2439
|
+
remotePublicKey: detection.remotePublicKey,
|
|
2440
|
+
}, { p2p: v3P2PRef });
|
|
2441
|
+
if (result && result.promptBlock) {
|
|
2442
|
+
console.log(`[manifest-loader] ${detection.reason} → ${detection.remotePublicKey.slice(0, 12)}... (${result.durationMs}ms, rpc=${result.rpcTriggered}, agents=${result.agentDescriptions.length})`);
|
|
2443
|
+
// 把 promptBlock 推给前端 + 写入一次性的 prompt 附加 (nextPromptHint 全局变量, 在下次 agent.promptStream 前 prepend)
|
|
2444
|
+
broadcast({
|
|
2445
|
+
type: 'peer-manifest-loaded',
|
|
2446
|
+
fromPublicKey: detection.remotePublicKey,
|
|
2447
|
+
reason: detection.reason,
|
|
2448
|
+
promptBlock: result.promptBlock,
|
|
2449
|
+
capabilityIndex: result.capabilityIndex,
|
|
2450
|
+
}, channelId);
|
|
2451
|
+
// 缓存到 channel 维度的 nextPromptHint (下一次 LLM prompt 前 inject)
|
|
2452
|
+
nextPromptHints.set(channelId, (nextPromptHints.get(channelId) || '') + '\n\n' + result.promptBlock);
|
|
2453
|
+
clearFailure(channelId);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
else {
|
|
2457
|
+
// 没触发, 也清掉连续失败计数 (用户消息说明对话还在正常进行)
|
|
2458
|
+
clearFailure(channelId);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
catch (loaderErr) {
|
|
2462
|
+
console.warn('[manifest-loader] failed (non-fatal):', loaderErr?.message?.slice(0, 200));
|
|
2463
|
+
}
|
|
1842
2464
|
const channels = await loadChannels();
|
|
1843
2465
|
const channel = channels.find(c => c.id === channelId);
|
|
1844
2466
|
// 2026-06-11: 移除 suggestRename 的二次 LLM 调用 — 之前每次用户发消息, 智能体 channel 都会再调一次 LLM (5-8s) 自动改名
|
|
@@ -1900,6 +2522,10 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1900
2522
|
res.status(500).json({ error: err.message });
|
|
1901
2523
|
}
|
|
1902
2524
|
finally {
|
|
2525
|
+
// 2026-07-04: 清掉 pivot 强制 timeout timer, 避免 hang 情况下 timer 在
|
|
2526
|
+
// process 里一直挂着. forceTimeout 在 try 外创建, finally 在闭包里
|
|
2527
|
+
// 能直接拿到引用.
|
|
2528
|
+
clearTimeout(forceTimeout);
|
|
1903
2529
|
// queue dequeue: 跑完或失败都要清状态
|
|
1904
2530
|
// 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
|
|
1905
2531
|
// 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
|
|
@@ -3663,11 +4289,25 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
3663
4289
|
res.json({ initialized: false });
|
|
3664
4290
|
return;
|
|
3665
4291
|
}
|
|
4292
|
+
// 2026-07-04: irohTransport.getNodeId() 在某些环境下返回空字符串 (@rayhanadev/iroh + Windows
|
|
4293
|
+
// native binding 启动延迟). fallback: 用 v3 P2PDirect (hyperswarm) 的 publicKey 兜底,
|
|
4294
|
+
// 保证客户端拿到一个可用的 peer id (v3 是实际数据通道, 不是 mock).
|
|
4295
|
+
let effectiveNodeId = irohNodeInfo.irohNodeId;
|
|
4296
|
+
if (!effectiveNodeId && v3P2PRef) {
|
|
4297
|
+
try {
|
|
4298
|
+
effectiveNodeId = v3P2PRef.getPublicKey() || '';
|
|
4299
|
+
if (effectiveNodeId) {
|
|
4300
|
+
console.log('[iroh API] irohNodeId 为空, fallback 到 v3 P2PDirect publicKey:', effectiveNodeId.substring(0, 16) + '...');
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
catch { /* ignore */ }
|
|
4304
|
+
}
|
|
3666
4305
|
res.json({
|
|
3667
4306
|
initialized: true,
|
|
3668
4307
|
did: irohNodeInfo.did,
|
|
3669
4308
|
cid: irohNodeInfo.cid,
|
|
3670
|
-
irohNodeId:
|
|
4309
|
+
irohNodeId: effectiveNodeId,
|
|
4310
|
+
irohNodeIdSource: effectiveNodeId === irohNodeInfo.irohNodeId ? 'iroh' : (effectiveNodeId ? 'v3-p2p-fallback' : 'unavailable'),
|
|
3671
4311
|
name: irohNodeInfo.name
|
|
3672
4312
|
});
|
|
3673
4313
|
});
|