@bolloon/bolloon-agent 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/ha-daemon.js +85 -0
  9. package/dist/agents/ha-supervisor.js +146 -0
  10. package/dist/agents/intent-classifier.js +1 -1
  11. package/dist/agents/iteration-budget.js +12 -0
  12. package/dist/agents/knowledge-organizer.js +0 -2
  13. package/dist/agents/lesson-to-skill.js +0 -1
  14. package/dist/agents/loop-review.js +2 -2
  15. package/dist/agents/memory-store.js +384 -0
  16. package/dist/agents/opportunity-match.js +15 -3
  17. package/dist/agents/opportunity-web-search.js +39 -0
  18. package/dist/agents/p2p-chat-tools.js +1 -2
  19. package/dist/agents/parse-tool-call.js +9 -22
  20. package/dist/agents/pi-harness.js +1 -2
  21. package/dist/agents/pi-sdk-session-manager.js +8 -0
  22. package/dist/agents/pi-sdk-tools.js +180 -5
  23. package/dist/agents/pi-sdk.js +210 -503
  24. package/dist/agents/process-runner.js +28 -9
  25. package/dist/agents/react-loop.js +9 -17
  26. package/dist/agents/runner-resolver.js +4 -3
  27. package/dist/agents/skill-loader.js +22 -15
  28. package/dist/agents/skills-manager.js +1 -25
  29. package/dist/agents/workflow-pivot-loop.js +152 -61
  30. package/dist/bootstrap/context-collector.js +67 -7
  31. package/dist/bootstrap/context-hierarchy.js +13 -1
  32. package/dist/bootstrap/persona-loader.js +26 -0
  33. package/dist/cli/ink-app.js +44 -17
  34. package/dist/cli/intent-command.js +22 -12
  35. package/dist/cli/loading-tui.js +8 -2
  36. package/dist/cli/log-gate.js +40 -8
  37. package/dist/cli/mention-data.js +1 -0
  38. package/dist/cli/reply-hygiene.js +22 -0
  39. package/dist/cli/trace-line.js +61 -2
  40. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  41. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  42. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  43. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  44. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  45. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  46. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  47. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  48. package/dist/electron-build/agents/contacts/tools.js +112 -0
  49. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  50. package/dist/electron-build/agents/experience-review.js +0 -3
  51. package/dist/electron-build/agents/experience-review.js.map +1 -1
  52. package/dist/electron-build/agents/external-events.js.map +1 -1
  53. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  55. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  56. package/dist/electron-build/agents/goal-resume.js +0 -4
  57. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  58. package/dist/electron-build/agents/intent-classifier.js +1 -1
  59. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  60. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  61. package/dist/electron-build/agents/loop-review.js +2 -2
  62. package/dist/electron-build/agents/loop-review.js.map +1 -1
  63. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  64. package/dist/electron-build/agents/memory-store.js +430 -0
  65. package/dist/electron-build/agents/memory-store.js.map +1 -0
  66. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  67. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  68. package/dist/electron-build/agents/pi-harness.js +1 -2
  69. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  71. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  72. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  73. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  74. package/dist/electron-build/agents/pi-sdk.js +208 -501
  75. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  76. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  77. package/dist/electron-build/agents/process-runner.js +28 -9
  78. package/dist/electron-build/agents/process-runner.js.map +1 -1
  79. package/dist/electron-build/agents/runner-resolver.js +4 -3
  80. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  81. package/dist/electron-build/agents/skill-loader.js +22 -16
  82. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  83. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  84. package/dist/electron-build/agents/skills-manager.js +0 -24
  85. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  86. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  87. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  88. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  89. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  90. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  91. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  92. package/dist/electron-build/bootstrap/context-collector.js +68 -7
  93. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  94. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  95. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  96. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  97. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  98. package/dist/electron-build/electron-preload.js.map +1 -1
  99. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  100. package/dist/electron-build/kernel/channel-actor.js +31 -2
  101. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  102. package/dist/electron-build/kernel/model-runtime.js +8 -1
  103. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  104. package/dist/electron-build/llm/config-store.js +20 -0
  105. package/dist/electron-build/llm/config-store.js.map +1 -1
  106. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  107. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  108. package/dist/electron-build/llm/pi-ai.js +112 -38
  109. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  110. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  111. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  112. package/dist/electron-build/network/agent-network.js.map +1 -1
  113. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  114. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  115. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  116. package/dist/electron-build/security/tool-gate.js +2 -0
  117. package/dist/electron-build/security/tool-gate.js.map +1 -1
  118. package/dist/electron-build/setup/setup-store.js +34 -6
  119. package/dist/electron-build/setup/setup-store.js.map +1 -1
  120. package/dist/electron-build/social/heartbeat.js +18 -5
  121. package/dist/electron-build/social/heartbeat.js.map +1 -1
  122. package/dist/electron-build/utils/update-manager.js.map +1 -1
  123. package/dist/index.js +250 -12
  124. package/dist/ios/bolloon-web.json +3 -3
  125. package/dist/ios/client.js +74 -35
  126. package/dist/ios/icons/icon-1400x1400.png +0 -0
  127. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  128. package/dist/ios/index.html +122 -108
  129. package/dist/ios/mobile-agent.js +572 -45
  130. package/dist/ios/mobile-contacts.js +0 -4
  131. package/dist/ios/mobile-core.js +8079 -4590
  132. package/dist/ios/mobile-harness.js +1004 -0
  133. package/dist/ios/mobile-memory.js +317 -0
  134. package/dist/ios/mobile-orbitdb.js +194 -0
  135. package/dist/ios/mobile-persona.js +80 -0
  136. package/dist/ios/mobile-privacy.js +2 -3
  137. package/dist/ios/mobile.css +286 -21
  138. package/dist/ios/mobile.html +122 -108
  139. package/dist/ios/mobile.js +434 -152
  140. package/dist/ios/routes-contacts.js +94 -1
  141. package/dist/ios/routes-world.js +13 -2
  142. package/dist/ios/server.js +54 -41
  143. package/dist/ios/style.css +13 -5
  144. package/dist/ios/sw.js +1 -1
  145. package/dist/ios/ui/message-renderer.js +4 -18
  146. package/dist/ios/ui/step-timeline.js +14 -1
  147. package/dist/kernel/channel-actor.js +30 -2
  148. package/dist/kernel/gate-scan.js +2 -2
  149. package/dist/kernel/ha/ha-arbiter.js +177 -0
  150. package/dist/kernel/ha/ha-guard.js +151 -0
  151. package/dist/kernel/index.js +36 -0
  152. package/dist/kernel/model-runtime.js +8 -1
  153. package/dist/kernel/plan.js +5 -5
  154. package/dist/kernel/roster.js +3 -3
  155. package/dist/llm/config-store.js +20 -0
  156. package/dist/llm/embedding-adapter.js +69 -0
  157. package/dist/llm/pi-ai.js +110 -37
  158. package/dist/llm/system-prompt/registry.js +8 -6
  159. package/dist/orbitdb/memory-sync.js +70 -0
  160. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  161. package/dist/security/tool-gate.js +2 -0
  162. package/dist/setup/setup-store.js +34 -6
  163. package/dist/social/heartbeat.js +18 -5
  164. package/dist/web/bolloon-web.json +3 -3
  165. package/dist/web/client.js +74 -35
  166. package/dist/web/icons/icon-1400x1400.png +0 -0
  167. package/dist/web/icons/icon-1400x1400.webp +0 -0
  168. package/dist/web/index.html +37 -25
  169. package/dist/web/mobile-agent.js +572 -45
  170. package/dist/web/mobile-contacts.js +0 -4
  171. package/dist/web/mobile-core.js +8079 -4590
  172. package/dist/web/mobile-harness.js +1004 -0
  173. package/dist/web/mobile-memory.js +317 -0
  174. package/dist/web/mobile-orbitdb.js +194 -0
  175. package/dist/web/mobile-persona.js +80 -0
  176. package/dist/web/mobile-privacy.js +2 -3
  177. package/dist/web/mobile.css +286 -21
  178. package/dist/web/mobile.html +122 -108
  179. package/dist/web/mobile.js +434 -152
  180. package/dist/web/routes-contacts.js +94 -1
  181. package/dist/web/routes-world.js +13 -2
  182. package/dist/web/server.js +54 -41
  183. package/dist/web/style.css +13 -5
  184. package/dist/web/sw.js +1 -1
  185. package/dist/web/ui/message-renderer.js +4 -18
  186. package/dist/web/ui/step-timeline.js +14 -1
  187. package/package.json +3 -2
  188. package/scripts/build-web.ts +16 -1
@@ -18,7 +18,6 @@ import { loadChainConfig, DEFAULT_CONFIRMATIONS, } from './chain-config.js';
18
18
  /** ERC20 Transfer 事件 topic0 */
19
19
  export const ERC20_TRANSFER_TOPIC0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
20
20
  const ERC20_IFACE = new Interface(['event Transfer(address indexed from, address indexed to, uint256 value)']);
21
- const ZERO32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
22
21
  function base(partial) {
23
22
  return {
24
23
  chainSettled: false,
@@ -44,25 +44,13 @@ export function segmentChatReply(reply, ctx) {
44
44
  if (remaining.trim()) {
45
45
  remaining = filterFillerText(remaining);
46
46
  }
47
- // === 6. final 之前的 text 段 (中间对话) push — 必须在 final push 之前 ===
48
- // 注意: 这时 remaining 还含 <final gen> 标记 + 标记后内容. 切 final 前先把 <final gen> 之后内容清掉
49
- // 实际: 切 final 后再切 text 更清楚
50
- const finalIdx = remaining.indexOf('<final gen>');
51
- let beforeFinalText = remaining;
52
- if (finalIdx >= 0) {
53
- beforeFinalText = remaining.substring(0, finalIdx);
54
- }
55
- const textContent = beforeFinalText.trim();
47
+ // === 6. 剩余内容全部作为 text 段 (2026-10-12: 移除 <final gen> 后不再切 final segment) ===
48
+ // 旧实现找 `<final gen>` 标记把内容切成 before/after 两段; 标记已全项目移除,
49
+ // 剩余内容就是给用户的完整答复, 全部进 text。
50
+ const textContent = remaining.trim();
56
51
  if (textContent) {
57
52
  segments.push({ type: 'text', content: textContent });
58
53
  }
59
- // === 7. 切 <final gen>...</final gen> (永远最后 push, 渲染时在最后) ===
60
- if (finalIdx >= 0) {
61
- const afterFinal = remaining.substring(finalIdx + '<final gen>'.length).trim();
62
- if (afterFinal) {
63
- segments.push({ type: 'final', content: afterFinal });
64
- }
65
- }
66
54
  return segments;
67
55
  }
68
56
  /**
@@ -0,0 +1,155 @@
1
+ /**
2
+ * human-directory.ts — 手机号 → DID 人类账号目录 (2026-10-11, phone-account-lookup-design §3)
3
+ *
4
+ * 落点 A (群内/已知盐目录, 推荐起步):
5
+ * - 发布: 用户 opt-in → 本地算 HMAC(salt || phone, pepper) 盲化哈希 → 存目录条目
6
+ * - 查询: 输入对方手机号 → 用同一算法算哈希 → 遍历本地目录比对 → 命中拿 DID
7
+ * - 隐私: 目录永不存明文手机号 · 哈希带 per-user salt (不可离线枚举) ·
8
+ * pepper 本地持有不进目录 · 未发布 = 查不到 (与不存在同义)
9
+ *
10
+ * 诚实边界 (写死):
11
+ * - 落点 A 只能搜「已与你交换过 salt / 已加入同一群」的节点 (三角不可能: 隐私/可搜/去中心化只取二)
12
+ * - 本模块只做「本地目录 + 哈希运算」; 跨节点查询靠既有 P2P (群目录复制 / direct 消息)
13
+ * - pepper 是本机私密 (~/.bolloon/contacts/directory-pepper.json, 0600), 不进目录条目
14
+ */
15
+ import * as fs from 'fs/promises';
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import { createHmac, randomBytes } from 'node:crypto';
19
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
20
+ export function directoryPath(homeDir) {
21
+ return path.join(homeDir || home(), '.bolloon', 'contacts', 'human-directory.json');
22
+ }
23
+ /** pepper 路径 (0600 私密) */
24
+ export function pepperPath(homeDir) {
25
+ return path.join(homeDir || home(), '.bolloon', 'contacts', 'directory-pepper.json');
26
+ }
27
+ /** 原子写: tmp + rename (写坏不残留半截文件; 0600 私密权限) */
28
+ async function atomicWriteJson(file, value) {
29
+ await fs.mkdir(path.dirname(file), { recursive: true });
30
+ const tmp = `${file}.tmp-${process.pid}-${Date.now().toString(36)}`;
31
+ await fs.writeFile(tmp, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });
32
+ await fs.rename(tmp, file);
33
+ }
34
+ /** 读取/生成 pepper (首次生成随机 32 字节; 幂等复用) */
35
+ export async function loadPepper(homeDir) {
36
+ const p = pepperPath(homeDir);
37
+ try {
38
+ const raw = await fs.readFile(p, 'utf8');
39
+ const j = JSON.parse(raw);
40
+ if (j?.pepper)
41
+ return j.pepper;
42
+ }
43
+ catch { /* 不存在 → 生成 */ }
44
+ const pepper = randomBytes(32).toString('hex');
45
+ await atomicWriteJson(p, { pepper });
46
+ return pepper;
47
+ }
48
+ /** 手机号归一化: 只留数字与 + (同一规则只此一处, 供校验与哈希共用) */
49
+ export function normalizePhone(value) {
50
+ return String(value || '').replace(/[^\d+]/g, '');
51
+ }
52
+ /** 盲化哈希: HMAC-SHA256(salt || phone, pepper) → hex (同一输入永远同一输出) */
53
+ export function blindHash(phone, salt, pepper) {
54
+ const normalized = normalizePhone(phone);
55
+ return createHmac('sha256', pepper).update(`${salt}||${normalized}`).digest('hex');
56
+ }
57
+ /** 生成 per-user salt */
58
+ export function genSalt() {
59
+ return randomBytes(16).toString('hex');
60
+ }
61
+ /** 校验手机号形状 (宽松: 8-15 位数字, 可带 + 前缀) */
62
+ export function isPlausiblePhone(value) {
63
+ return /^\+?\d{8,15}$/.test(normalizePhone(value));
64
+ }
65
+ /** 目录存储: 读/写原子 (与 contacts store 同模式) */
66
+ export async function readDirectory(homeDir) {
67
+ try {
68
+ const raw = await fs.readFile(directoryPath(homeDir), 'utf8');
69
+ const j = JSON.parse(raw);
70
+ return Array.isArray(j) ? j : [];
71
+ }
72
+ catch (err) {
73
+ if (err?.code === 'ENOENT')
74
+ return [];
75
+ throw new Error(`手机号目录损坏 (${directoryPath(homeDir)}): ${err?.message || err}`);
76
+ }
77
+ }
78
+ async function writeDirectory(entries, homeDir) {
79
+ await atomicWriteJson(directoryPath(homeDir), entries);
80
+ }
81
+ /** 从本机 identity/user.json 读取 64 hex publicKey (目录发布自动带上, 供对方直达加好友) */
82
+ export async function loadOwnPublicKeyHex(homeDir) {
83
+ try {
84
+ const p = path.join(homeDir || home(), '.bolloon', 'identity', 'user.json');
85
+ const j = JSON.parse(await fs.readFile(p, 'utf8'));
86
+ const pk = String(j?.publicKeyHex || '');
87
+ return /^[0-9a-f]{64}$/i.test(pk) ? pk.toLowerCase() : undefined;
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
93
+ /** 发布一条目录条目 (opt-in; 同 phoneHash 已存在 → 更新为最新, 撤销后重发 = 恢复) */
94
+ export async function publishEntry(phone, did, opts = {}) {
95
+ if (!isPlausiblePhone(phone))
96
+ throw new Error('手机号格式不正确 (8-15 位数字, 可带 + 前缀)');
97
+ if (!did || !did.startsWith('did:'))
98
+ throw new Error('DID 格式不正确 (须以 did: 开头)');
99
+ const pepper = await loadPepper(opts.homeDir);
100
+ const salt = genSalt(); // 每次发布新 salt (旧哈希不可回推, 重发布 = 新哈希)
101
+ const publicKeyHex = opts.publicKeyHex
102
+ ? opts.publicKeyHex.toLowerCase()
103
+ : await loadOwnPublicKeyHex(opts.homeDir); // 缺省自动带本机 P2P 公钥 (若有)
104
+ const entry = {
105
+ phoneHash: blindHash(phone, salt, pepper),
106
+ salt,
107
+ did,
108
+ publicKeyHex,
109
+ displayName: opts.displayName,
110
+ publishedAt: new Date().toISOString(),
111
+ revokedAt: null,
112
+ ownerSig: '', // P2 落点 B 才要求签名
113
+ };
114
+ const entries = await readDirectory(opts.homeDir);
115
+ const idx = entries.findIndex((e) => e.did === did); // 同一 DID 只留一条 (更新)
116
+ if (idx >= 0)
117
+ entries[idx] = entry;
118
+ else
119
+ entries.push(entry);
120
+ await writeDirectory(entries, opts.homeDir);
121
+ return entry;
122
+ }
123
+ /** 撤销 (软删): 标记 revokedAt, 条目保留 (审计/可恢复) */
124
+ export async function retractEntry(did, homeDir) {
125
+ const entries = await readDirectory(homeDir);
126
+ const e = entries.find((x) => x.did === did);
127
+ if (!e)
128
+ return false;
129
+ e.revokedAt = new Date().toISOString();
130
+ await writeDirectory(entries, homeDir);
131
+ return true;
132
+ }
133
+ /** 查询: 用同一算法算哈希, 遍历比对 (未发布/已撤销 → not found) */
134
+ export async function lookupPhone(phone, homeDir) {
135
+ const pepper = await loadPepper(homeDir);
136
+ const entries = await readDirectory(homeDir);
137
+ // 对方的 salt 未知 → 逐个试本目录里的条目 salt (落点 A: 群小, N 可接受)
138
+ for (const e of entries) {
139
+ if (blindHash(phone, e.salt, pepper) === e.phoneHash) {
140
+ if (e.revokedAt)
141
+ return { found: false, revoked: true }; // 撤销 = 与不存在同义
142
+ return { found: true, did: e.did, publicKeyHex: e.publicKeyHex, displayName: e.displayName };
143
+ }
144
+ }
145
+ return { found: false };
146
+ }
147
+ /** 列出已发布条目 (本机审计用; 只回脱敏: 哈希前 8 位 + DID + 时间) */
148
+ export function summarizeEntries(entries) {
149
+ return entries.map((e) => ({
150
+ hash: e.phoneHash.slice(0, 8) + '…',
151
+ did: e.did,
152
+ publishedAt: e.publishedAt,
153
+ revoked: !!e.revokedAt,
154
+ }));
155
+ }
@@ -150,5 +150,84 @@ export function registerContactTools(ctx, chain) {
150
150
  },
151
151
  });
152
152
  registered.push(...CONTACT_TOOL_NAMES);
153
+ // ── 手机号目录 (2026-10-11, phone-account-lookup-design 落点 A) ──────────
154
+ // 工具面: 发布自己的手机号指针 / 查对方 / 撤销。隐私: 只操作哈希, 明文手机号只在本机。
155
+ ctx.tools.set('directory.publish', {
156
+ name: 'directory.publish',
157
+ description: '把自己的手机号 → DID 指针发布到本机目录 (opt-in, 用户显式要求才做)。之后对方凭手机号可查到你的 DID 加好友。**先问用户确认**再调用 (发布手机号是隐私动作)。',
158
+ parameters: { phone: '手机号 (11 位, 可带 +86 前缀)', displayName: '公开昵称 (可选, 不是真名)' },
159
+ execute: async (args) => {
160
+ const { publishEntry, isPlausiblePhone } = await import('./human-directory.js');
161
+ const phone = text(args.phone);
162
+ if (!isPlausiblePhone(phone))
163
+ return { success: false, error: '手机号格式不正确 (8-15 位数字)' };
164
+ const did = globalThis.__bolloonUserDid || 'did:bolln:local';
165
+ const e = await publishEntry(phone, did, { displayName: text(args.displayName) || undefined });
166
+ return { success: true, phoneHash: e.phoneHash.slice(0, 8) + '…', did: e.did, note: '已发布到本机目录 (对方凭手机号可查到你的 DID)' };
167
+ },
168
+ });
169
+ ctx.tools.set('directory.lookup', {
170
+ name: 'directory.lookup',
171
+ description: '按手机号查对方 DID (在本机目录内找)。用于: 用户说"用手机号找 XX 加好友"、想加某人为好友但只有手机号。响应只回 DID/昵称, 不回手机号。',
172
+ parameters: { phone: '对方手机号 (11 位)' },
173
+ execute: async (args) => {
174
+ const { lookupPhone, isPlausiblePhone } = await import('./human-directory.js');
175
+ const phone = text(args.phone);
176
+ if (!isPlausiblePhone(phone))
177
+ return { success: false, error: '手机号格式不正确' };
178
+ const r = await lookupPhone(phone);
179
+ if (!r.found)
180
+ return { success: true, found: false, note: '未找到 (对方未注册目录或已撤销)' };
181
+ return { success: true, found: true, did: r.did, publicKeyHex: r.publicKeyHex || null, displayName: r.displayName || null, note: '已找到, 可用 directory.add_friend 发起好友请求' };
182
+ },
183
+ });
184
+ ctx.tools.set('directory.add_friend', {
185
+ name: 'directory.add_friend',
186
+ description: '按手机号发起好友申请: 目录 lookup → P2P friend request。用于用户明确说“用手机号加好友”。这是对外通信动作,调用前应确认目标手机号和用户意图; 对方仍需 consent 接受,不会自动成为好友。',
187
+ parameters: { phone: '对方手机号 (8-15 位)', name: '好友备注 (可选)', message: '申请附言 (可选)' },
188
+ execute: async (args) => {
189
+ const { lookupPhone, isPlausiblePhone } = await import('./human-directory.js');
190
+ const phone = text(args.phone);
191
+ if (!isPlausiblePhone(phone))
192
+ return { success: false, error: '手机号格式不正确' };
193
+ const r = await lookupPhone(phone);
194
+ if (!r.found)
195
+ return { success: true, found: false, note: '未找到 (对方未注册目录或已撤销)' };
196
+ if (!r.publicKeyHex)
197
+ return { success: false, found: true, did: r.did, error: '对方已公开目录但未发布 P2P 公钥, 暂时无法直接发好友请求' };
198
+ try {
199
+ const port = process.env.PORT || '54188';
200
+ const resp = await fetch(`http://127.0.0.1:${port}/api/friend-request`, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({
204
+ targetPublicKey: r.publicKeyHex,
205
+ name: text(args.name) || r.displayName || undefined,
206
+ message: text(args.message) || '来自手机号目录的好友申请',
207
+ note: 'via-phone-directory',
208
+ }),
209
+ });
210
+ const data = await resp.json();
211
+ return resp.ok
212
+ ? { success: true, found: true, did: r.did, requestId: data.requestId, output: `已向 ${r.displayName || r.did} 发起好友申请; 对方同意后才会建立好友关系` }
213
+ : { success: false, found: true, did: r.did, error: data.error || '好友申请发送失败', code: data.code };
214
+ }
215
+ catch (err) {
216
+ return { success: false, error: `好友申请链路不可用: ${String(err?.message || err)}` };
217
+ }
218
+ },
219
+ });
220
+ ctx.tools.set('directory.retract', {
221
+ name: 'directory.retract',
222
+ description: '从本机目录撤销自己的手机号 → DID 指针 (软删)。用于: 用户说"不要让别人查到我手机号了"。',
223
+ parameters: {},
224
+ execute: async () => {
225
+ const { retractEntry } = await import('./human-directory.js');
226
+ const did = globalThis.__bolloonUserDid || 'did:bolln:local';
227
+ const ok = await retractEntry(did);
228
+ return { success: ok, note: ok ? '已从目录撤销 (手机号不再可查)' : '目录里没有你的条目' };
229
+ },
230
+ });
231
+ registered.push('directory.publish', 'directory.lookup', 'directory.add_friend', 'directory.retract');
153
232
  return registered;
154
233
  }
@@ -173,9 +173,6 @@ export function applyExperience(d, home = os.homedir(), nowIso = new Date().toIS
173
173
  return { wrote: false };
174
174
  }
175
175
  }
176
- function escapeRe(s) {
177
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
178
- }
179
176
  /**
180
177
  * 自审编排 (纯旁路, **绝不抛错**)。
181
178
  *
@@ -299,7 +299,7 @@ function impactFor(kind, scope) {
299
299
  };
300
300
  }
301
301
  const EMPTY_IMPACT = (scope) => impactFor('clarification', scope);
302
- function describeKind(kind, r, dir) {
302
+ function describeKind(kind, dir) {
303
303
  switch (kind) {
304
304
  case 'abort':
305
305
  return `撤销/终止: 在下一个 Run 边界终止该目标 (优先级最高, 压过同批其余变更)`;
@@ -334,7 +334,6 @@ function describeKind(kind, r, dir) {
334
334
  export function classifyChange(r, goal) {
335
335
  if (TERMINAL_STATUSES.includes(r.status))
336
336
  return { ...r, appliesToFutureRunsOnly: true };
337
- const t = lower(r.instruction);
338
337
  const criteria = Array.isArray(goal?.successCriteria) ? goal.successCriteria.map(String) : [];
339
338
  let intents = detectChangeIntents(r.instruction);
340
339
  if (intents.includes('success_criteria_change') && isCriteriaRestatement(r.instruction, criteria)) {
@@ -362,7 +361,7 @@ export function classifyChange(r, goal) {
362
361
  status: 'triaged',
363
362
  requiresReplan: false,
364
363
  impact: impactFor('abort', scope),
365
- interpreted: `${describeKind('abort', r, null)}; 原文: ${head(r.instruction)}` +
364
+ interpreted: `${describeKind('abort', null)}; 原文: ${head(r.instruction)}` +
366
365
  (superseded.length ? `; 同批的 ${superseded.join('/')} 一律作废 (superseded), 需单独重新提交` : ''),
367
366
  };
368
367
  }
@@ -386,7 +385,7 @@ export function classifyChange(r, goal) {
386
385
  status: 'triaged',
387
386
  requiresReplan: REPLAN_KINDS.has(kind),
388
387
  impact,
389
- interpreted: `${KIND_LABELS[kind]}: ${describeKind(kind, r, dir)}; 影响面: ${impactLabel(impact)}; 原文: ${head(r.instruction)}`,
388
+ interpreted: `${KIND_LABELS[kind]}: ${describeKind(kind, dir)}; 影响面: ${impactLabel(impact)}; 原文: ${head(r.instruction)}`,
390
389
  };
391
390
  }
392
391
  function impactLabel(i) {
@@ -28,7 +28,6 @@
28
28
  import * as fs from 'fs/promises';
29
29
  import * as os from 'os';
30
30
  import * as path from 'path';
31
- import * as crypto from 'crypto';
32
31
  import { sessionStore } from './session-store.js';
33
32
  import * as taskState from './task-state.js';
34
33
  import { onGoalParked, onGoalResumed } from '../bootstrap/lifecycle-hooks.js';
@@ -88,9 +87,6 @@ async function loadAllSnapshots() {
88
87
  }
89
88
  return out;
90
89
  }
91
- function newGoalId() {
92
- return `goal-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
93
- }
94
90
  // ============================================================
95
91
  // Park 现有 task (复用 task-state.ts 'paused' 状态)
96
92
  // ============================================================
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ha-daemon.ts — 独立 kernel 守护进程入口 (业务侧宿主, 2026-10-16, leo: 双内核冗余 — 拆独立 kernel 守护进程)
3
+ *
4
+ * 由主进程 spawn (见 ha-supervisor.ts 的 startHaDaemon): `node dist/agents/ha-daemon.js --role standby --home <HOME>`
5
+ * 职责: 以 **standby** 角色运行 HA 守护循环 —— 探测主进程心跳, 主死则接管其 orphan run。
6
+ *
7
+ * 位置说明: **不放在 src/kernel/** — kernel 目录有 K1 边界门 (禁 import 业务); 本文件要注入 run-store
8
+ * 的 reconcileOrphans/prepareResume, 是**业务侧宿主**, 放 src/agents/。kernel/ha/ 只留纯机制
9
+ * (ha-arbiter 心跳格式/锁/决策 + ha-guard 守护循环, 端口注入), 零业务依赖。
10
+ *
11
+ * 环境变量:
12
+ * BOLLOON_HOME 数据目录 (缺省 ~/.bolloon)
13
+ * BOLLOON_HA_BEAT_MS 心跳间隔 (缺省 10s)
14
+ * BOLLOON_HA_STALE_MS 心跳过期阈值 (缺省 30s)
15
+ *
16
+ * 退出: SIGTERM/SIGINT → 停心跳 → exit(0); 父进程退出 → 管道关闭 → 退出 (不孤儿存活)。
17
+ */
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ import * as fs from 'fs/promises';
21
+ import { startHaGuard } from '../kernel/ha/ha-guard.js';
22
+ import { runsDir } from './run-store.js';
23
+ const home = process.env.BOLLOON_HOME || os.homedir();
24
+ const beatMs = Number(process.env.BOLLOON_HA_BEAT_MS) || 10_000;
25
+ const staleMs = Number(process.env.BOLLOON_HA_STALE_MS) || 30_000;
26
+ // 2026-10-16: run-store 的 runsDir() 用 os.homedir() (POSIX 读 $HOME)。
27
+ // 设置了 BOLLOON_HOME = daemon 操作隔离数据目录 → 无条件把 $HOME 指过去
28
+ // (daemon 是独立 spawn 的守护进程, 改自己的 HOME 不影响主进程; 这里不设的话
29
+ // reconcileOrphans/prepareResume 会读真实 ~/.bolloon/runs, 接不到隔离 runs 的孤儿)。
30
+ if (process.env.BOLLOON_HOME) {
31
+ process.env.HOME = process.env.BOLLOON_HOME;
32
+ }
33
+ // 业务侧端口注入: run-store 实现 (守护循环不自己 import 业务, 由宿主注入)
34
+ const ports = {
35
+ reconcileOrphans: async () => {
36
+ const { reconcileOrphans } = await import('./run-store.js');
37
+ return reconcileOrphans();
38
+ },
39
+ prepareResume: async (runId) => {
40
+ const { prepareResume } = await import('./run-store.js');
41
+ const r = await prepareResume(runId);
42
+ return r.ok ? { ok: true } : { ok: false, reason: r.reason };
43
+ },
44
+ collectActiveRunIds: async () => {
45
+ // 2026-10-16: BOLLOON_HOME 优先 (隔离 HOME 的 daemon 读隔离 runs), 否则 fallback 真实 runsDir()
46
+ const home = process.env.BOLLOON_HOME || '';
47
+ const dir = home ? path.join(home, '.bolloon', 'runs') : runsDir();
48
+ const files = await fs.readdir(dir).catch(() => []);
49
+ const active = [];
50
+ for (const n of files) {
51
+ if (!n.endsWith('.json') || n.endsWith('.bak'))
52
+ continue;
53
+ const id = n.replace(/\.json$/, '');
54
+ if (/^(run|goal)_/.test(id))
55
+ active.push(id); // 保守: 只认 run/goal 前缀
56
+ }
57
+ return active;
58
+ },
59
+ log: (level, message) => {
60
+ process.stdout.write(`[ha-daemon:${level}] ${message}\n`);
61
+ },
62
+ };
63
+ const guard = startHaGuard(ports, {
64
+ role: 'standby',
65
+ home,
66
+ kernelVersion: process.env.npm_package_version || '',
67
+ beatMs,
68
+ staleMs,
69
+ onTakeover: (info) => {
70
+ process.stdout.write(`[ha-daemon] 接管主进程: adopted=${info.adoptedRunIds.length} runs (${info.reason})\n`);
71
+ },
72
+ });
73
+ process.stdout.write(`[ha-daemon] 启动 role=standby pid=${process.pid} home=${home} beat=${beatMs}ms stale=${staleMs}ms\n`);
74
+ let shuttingDown = false;
75
+ function shutdown(sig) {
76
+ if (shuttingDown)
77
+ return;
78
+ shuttingDown = true;
79
+ process.stdout.write(`[ha-daemon] 收到 ${sig}, 停止心跳退出\n`);
80
+ guard.stop();
81
+ process.exit(0);
82
+ }
83
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
84
+ process.on('SIGINT', () => shutdown('SIGINT'));
85
+ export { guard };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ha-supervisor.ts — 主进程侧 HA 守护管理器 (2026-10-16, leo: 双内核冗余 — 独立 kernel 守护进程)
3
+ *
4
+ * 主进程 (createWebServer / CLI) 侧:
5
+ * ① 自己以 **primary** 角色写心跳 (含当前活跃 runIds)
6
+ * ② spawn 独立 kernel 守护进程 (src/agents/ha-daemon.ts, 以 standby 角色跑)
7
+ * ③ 监控 daemon 存活: 心跳过期 / 进程退出 → **自动重启 + 接管** (重启后它自己会 reconcileOrphans + prepareResume)
8
+ * ④ daemon 启动即探测主进程: 主死 → daemon 接管 (反向故障转移)
9
+ *
10
+ * 依赖注入 (业务侧端口):
11
+ * - primary 心跳的 runIds 来源: collectActiveRunIds (runs 目录扫描)
12
+ * - 全端口注入 → 可单测 (注入假 DAEMON_TS 路径 / 假 child_process)
13
+ */
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ import { spawn } from 'node:child_process';
17
+ import { startHaGuard } from '../kernel/ha/ha-guard.js';
18
+ import { collectActiveRunIds, STALE_MS, BEAT_MS } from '../kernel/ha/ha-arbiter.js';
19
+ import * as fs from 'fs';
20
+ const noop = () => { };
21
+ function defaultSpawn(cmd, args, opts) {
22
+ return spawn(cmd, args, opts);
23
+ }
24
+ export class HaSupervisor {
25
+ opts;
26
+ guard = null;
27
+ daemon = null;
28
+ restartTimer = null;
29
+ state = {
30
+ primaryStarted: false,
31
+ daemonPid: null,
32
+ daemonAlive: false,
33
+ restarts: 0,
34
+ daemonExitCode: null,
35
+ };
36
+ stopped = false;
37
+ constructor(opts = {}) {
38
+ this.opts = opts;
39
+ }
40
+ getState() { return { ...this.state }; }
41
+ /** 启动: primary 心跳 + spawn daemon + 监控重启 */
42
+ async start() {
43
+ const home = this.opts.home || os.homedir();
44
+ const beatMs = this.opts.beatMs ?? BEAT_MS;
45
+ const staleMs = this.opts.staleMs ?? STALE_MS;
46
+ const log = this.opts.log || noop;
47
+ // ① primary 心跳 (主进程自己; runIds 从 runs 目录收集)
48
+ const ports = {
49
+ reconcileOrphans: async () => ({ interrupted: [], stillRunning: [], failed: [] }),
50
+ prepareResume: async () => ({ ok: false, reason: 'primary 不接管 (它是主)' }),
51
+ collectActiveRunIds: async () => collectActiveRunIds(path.join(home, '.bolloon', 'runs')),
52
+ log: (l, m) => log(l, `[ha:primary] ${m}`),
53
+ };
54
+ this.guard = startHaGuard(ports, {
55
+ role: 'primary',
56
+ home,
57
+ kernelVersion: process.env.npm_package_version || '',
58
+ beatMs,
59
+ staleMs,
60
+ });
61
+ this.state.primaryStarted = true;
62
+ log('info', `[ha] primary 心跳已启动 (home=${home}, beat=${beatMs}ms, stale=${staleMs}ms)`);
63
+ // ② spawn 独立 kernel 守护进程 (standby)
64
+ this.spawnDaemon();
65
+ // ③ 监控 daemon: 退出 → 自动重启 (kernel 死则重启 + 它接管)
66
+ // 监控交给 child 'exit' 事件 (已在 spawnDaemon 里挂), 无需额外轮询。
67
+ }
68
+ spawnDaemon() {
69
+ if (this.stopped)
70
+ return;
71
+ const log = this.opts.log || noop;
72
+ const home = this.opts.home || os.homedir();
73
+ const nodeBin = this.opts.nodeBin || process.execPath;
74
+ const entry = this.opts.daemonEntry || path.join(__dirname, 'ha-daemon.js');
75
+ const spawnImpl = this.opts.spawnImpl || defaultSpawn;
76
+ log('info', `[ha] spawn kernel 守护进程: ${nodeBin} ${entry} (role=standby)`);
77
+ try {
78
+ const child = spawnImpl(nodeBin, [entry], {
79
+ env: {
80
+ ...process.env,
81
+ BOLLOON_HOME: home,
82
+ BOLLOON_HA_BEAT_MS: String(this.opts.beatMs ?? BEAT_MS),
83
+ BOLLOON_HA_STALE_MS: String(this.opts.staleMs ?? STALE_MS),
84
+ },
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ detached: false,
87
+ });
88
+ this.daemon = child;
89
+ this.state.daemonPid = child.pid ?? null;
90
+ this.state.daemonAlive = true;
91
+ child.stdout?.on('data', (d) => {
92
+ const s = String(d).trim();
93
+ if (s)
94
+ log('info', `[ha:daemon] ${s}`);
95
+ });
96
+ child.stderr?.on('data', (d) => {
97
+ const s = String(d).trim();
98
+ if (s)
99
+ log('warn', `[ha:daemon:err] ${s}`);
100
+ });
101
+ child.on('exit', (code, signal) => {
102
+ this.state.daemonAlive = false;
103
+ this.state.daemonExitCode = code;
104
+ log('warn', `[ha] kernel 守护进程退出 code=${code} signal=${signal || ''}`);
105
+ if (!this.stopped && this.opts.autoRestart !== false) {
106
+ log('info', `[ha] 守护进程退出, 自动重启 (第 ${this.state.restarts + 1} 次)`);
107
+ this.restartTimer = setTimeout(() => {
108
+ if (!this.stopped) {
109
+ this.state.restarts++;
110
+ this.spawnDaemon();
111
+ }
112
+ }, 1000);
113
+ }
114
+ });
115
+ }
116
+ catch (e) {
117
+ log('error', `[ha] spawn 守护进程失败: ${String(e?.message || e).slice(0, 160)}`);
118
+ }
119
+ }
120
+ /** 停止: 停 primary 心跳 + 杀 daemon (幂等) */
121
+ stop() {
122
+ this.stopped = true;
123
+ this.guard?.stop();
124
+ this.guard = null;
125
+ if (this.restartTimer) {
126
+ clearTimeout(this.restartTimer);
127
+ this.restartTimer = null;
128
+ }
129
+ if (this.daemon && !this.daemon.killed) {
130
+ try {
131
+ this.daemon.kill('SIGTERM');
132
+ }
133
+ catch { /* 忽略 */ }
134
+ }
135
+ // best-effort 清 primary 心跳
136
+ try {
137
+ const f = path.join(this.opts.home || os.homedir(), '.bolloon', 'kernel-ha', 'primary.heartbeat');
138
+ fs.unlinkSync(f);
139
+ }
140
+ catch { /* 忽略 */ }
141
+ }
142
+ }
143
+ /** 便捷工厂 */
144
+ export function createHaSupervisor(opts = {}) {
145
+ return new HaSupervisor(opts);
146
+ }
@@ -60,7 +60,7 @@ export function intentHint(intent) {
60
60
  2. 用 \`edit_file\` 精确定位改 (old_text 全文匹配) 或 \`write_file\` 整体覆写
61
61
  3. 改完跑 \`npx tsc --noEmit\` 和 \`npx vitest run --reporter=dot --bail=1\` 验证
62
62
  4. 用 \`git_commit\` 提交到当前分支 (分支名 agent/<task-id>)
63
- 不要调用 \`<final gen>\` 标记除非任务真正完成 (commit + 测试通过).
63
+ 不要提前收尾: 任务真正完成 (commit + 测试通过) 之前持续调用工具推进.
64
64
 
65
65
  ## 信息不足时主动查询
66
66
  **重要**: 不要假设! 如果任何步骤信息不足 (路径不明确/命令参数不知道/分支策略不清楚/保护规则不明白), 用工具主动查:
@@ -1,3 +1,15 @@
1
+ /**
2
+ * 迭代预算 (2026-10-01 落实第二轮: 学的是"**可退还**的迭代预算"那套)。
3
+ *
4
+ * 意图: 惩罚**零碎**调用, 奖励**批处理** —— 一次调用干多件事(程序化工具调用)不该吃预算。
5
+ * 用户的抱怨正是"一件小事花太多次工具调用" ⇒ 把预算变成**激励**而不是单纯的刹车。
6
+ * 设计对齐 (只搬约束, 不搬实现):
7
+ * · `consume()` 扣一次; **`refund()` 还一次** —— 批处理工具(见 REFUNDABLE_TOOLS)调用后归还;
8
+ * · 每个 agent 一份(父/子各自的 cap 不同);
9
+ * · 阈值**警告**(默认 80%) ⇒ 提前提示, 而不是到顶了才硬停;
10
+ * · 非法配置 **fail-open**(坏值当关闭, 绝不因配置坏掉就把 agent 卡死)。
11
+ * 纪律: 这是**记账 + 激励**, 不做硬刹车 —— 循环退出条件仍由调用方决定(它读 effective 值)。
12
+ */
1
13
  /**
2
14
  * 这些工具"一次调用可顶多次操作" ⇒ 用它换来的迭代要**退还**。
3
15
  * 加新工具时问自己: 它能不能在一次调用里做完 N 件事? 能 ⇒ 加进来。
@@ -419,7 +419,6 @@ export async function maintainGoals(home, llm) {
419
419
  const queue = (await readJson(path.join(goalsDir, 'queue.json'))) || {};
420
420
  // queue.json 结构未知, 兼容 object/array
421
421
  const queueItems = Array.isArray(queue) ? queue : Object.values(queue).filter((v) => v && typeof v === 'object');
422
- const events = await readDir(goalsDir);
423
422
  const now = new Date().toISOString().slice(0, 10);
424
423
  const currentDir = path.join(home, '.bolloon', 'context-os', '03-Current');
425
424
  const currentFiles = (await listMd(currentDir)).filter((f) => f.file !== 'README.md');
@@ -453,7 +452,6 @@ export async function maintainGoals(home, llm) {
453
452
  /** 自动整理心跳 · 知识层: 串行跑全部 9 个整理器, 汇总 sections */
454
453
  export async function runKnowledgeOrganize(opts = {}) {
455
454
  const home = opts.home || os.homedir();
456
- const cwd = opts.cwd || process.cwd();
457
455
  const llm = opts.llm;
458
456
  const sections = [];
459
457
  sections.push(await archiveContextOs(home));
@@ -198,7 +198,6 @@ export function routeLessonToSkill(lesson, skills, home = os.homedir()) {
198
198
  // 命中了但写不进去(文件读不到/空) ⇒ 退到沉淀技能, 保证"都进去" ✓
199
199
  }
200
200
  const sink = ensureSinkSkill(home);
201
- const body = fs.readFileSync(sink.file, 'utf-8');
202
201
  const withCandidates = d.suggestions.length
203
202
  ? { ...lesson, body: `${lesson.body} (候选技能: ${d.suggestions.map((s) => `${s.name}(${s.score})`).join(', ')})` }
204
203
  : lesson;
@@ -61,9 +61,9 @@ export function buildReviewHint(state, maxReviews = DEFAULT_MAX_REVIEWS) {
61
61
  `\n用户需求: ${intent.slice(0, 300)}` +
62
62
  actionLines +
63
63
  `\n已完成工具: ${tools}` +
64
- `\n[完成契约] 宣布 <final gen> 前必须展示具体证据 (命令输出 / 文件内容 / 测试结果), 只写结论不算完成.` +
64
+ `\n[完成契约] 宣布完成前必须展示具体证据 (命令输出 / 文件内容 / 测试结果), 只写结论不算完成.` +
65
65
  `\n请对照需求逐条自查: 用户需求是否每一项都真正完成了? 如果还有未完成的子目标或自然衔接的后续步骤 → 继续调用工具推进 (已完成动作不要重复, 直接基于已有结果做下一步); ` +
66
- `如果用户需求已全部满足 → 直接输出 <final gen> 结束.` +
66
+ `如果用户需求已全部满足 → 直接给出完整最终回答结束.` +
67
67
  `\n[重要] 不要因为做了一部分就提前结束 — 结束前逐条对照「用户需求」确认每一项都完成.`);
68
68
  }
69
69
  /** 布尔门: 是否该继续 review (测试/消融快速断言) */