@bolloon/bolloon-agent 0.6.1 → 0.6.3
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/README.md +128 -111
- package/dist/agents/gateway-group.js +231 -6
- package/dist/agents/group-autopilot.js +256 -0
- package/dist/agents/pi-sdk-tools.js +302 -0
- package/dist/agents/pi-sdk.js +89 -140
- package/dist/agents/task-group.js +21 -4
- package/dist/cli/log-gate.js +68 -4
- package/dist/cli/mention-data.js +1 -1
- package/dist/cli/model-selector.js +84 -104
- package/dist/cli/setup-wizard.js +2 -10
- package/dist/cli/tui-select.js +99 -10
- package/dist/cli-entry.js +1 -9
- package/dist/electron-build/agents/gateway-group.js +814 -0
- package/dist/electron-build/agents/gateway-group.js.map +1 -0
- package/dist/electron-build/agents/group-autopilot.js +302 -0
- package/dist/electron-build/agents/group-autopilot.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +88 -139
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/task-group.js +601 -0
- package/dist/electron-build/agents/task-group.js.map +1 -0
- package/dist/electron-build/agents/task-public-text.js +97 -0
- package/dist/electron-build/agents/task-public-text.js.map +1 -0
- package/dist/electron-build/efficode/compress.js +86 -0
- package/dist/electron-build/efficode/compress.js.map +1 -0
- package/dist/electron-build/efficode/crc32.js +44 -0
- package/dist/electron-build/efficode/crc32.js.map +1 -0
- package/dist/electron-build/efficode/did.js +74 -0
- package/dist/electron-build/efficode/did.js.map +1 -0
- package/dist/electron-build/efficode/lz77.js +175 -0
- package/dist/electron-build/efficode/lz77.js.map +1 -0
- package/dist/electron-build/efficode/negotiate.js +233 -0
- package/dist/electron-build/efficode/negotiate.js.map +1 -0
- package/dist/electron-build/efficode/ops.js +96 -0
- package/dist/electron-build/efficode/ops.js.map +1 -0
- package/dist/electron-build/efficode/packet.js +279 -0
- package/dist/electron-build/efficode/packet.js.map +1 -0
- package/dist/electron-build/efficode/types.js +50 -0
- package/dist/electron-build/efficode/types.js.map +1 -0
- package/dist/electron-build/kernel/code-write-gate.js +55 -0
- package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +23 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
- package/dist/electron-build/kernel/run-resume.js +62 -0
- package/dist/electron-build/kernel/run-resume.js.map +1 -0
- package/dist/electron-build/kernel/session-lifecycle.js +223 -0
- package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
- package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
- package/dist/electron-build/llm/config-store.js +2 -2
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/model-selection.js +16 -2
- package/dist/electron-build/llm/model-selection.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +2 -2
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +98 -19
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/group-access.js +399 -0
- package/dist/electron-build/orbitdb/group-access.js.map +1 -0
- package/dist/index.js +143 -10
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/code-write-gate.js +47 -0
- package/dist/kernel/gate-scan.js +5 -1
- package/dist/kernel/plan-harness.js +3 -2
- package/dist/kernel/roster.js +6 -6
- package/dist/kernel/run-lifecycle.js +21 -0
- package/dist/kernel/run-resume.js +58 -0
- package/dist/kernel/session-lifecycle.js +210 -0
- package/dist/kernel/turn-selfcheck.js +60 -0
- package/dist/llm/config-store.js +2 -2
- package/dist/llm/model-selection.js +16 -2
- package/dist/llm/pi-ai.js +2 -2
- package/dist/orbitdb/cid-database.js +95 -19
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -17,13 +17,15 @@ import { createRunContext } from './run-context.js';
|
|
|
17
17
|
import { createPrivateActor } from '../kernel/channel-actor.js';
|
|
18
18
|
// K10 ①: 运行生命周期写入经内核端口 (与 kernel/control.ts 的控制面分开 —— 一个回答"谁命令我停",
|
|
19
19
|
// 一个回答"我这一轮发生了什么"; 失败语义也不同: 控制面被拒 → 409, 事实写不进 → 响亮失败)
|
|
20
|
-
import { submitRunLifecycle, assertLifecycleOk } from '../kernel/run-lifecycle.js';
|
|
20
|
+
import { submitRunLifecycle, assertLifecycleOk, pickRunIdForResume } from '../kernel/run-lifecycle.js';
|
|
21
21
|
// K10 ④: 通信发送入口经内核端口 (上层不再直接对某条传输说话; 换传输只改这一处注入)
|
|
22
22
|
import { submitTransport, transportPeersSync } from '../kernel/transport.js';
|
|
23
23
|
// K10 ①: run 状态迁移经内核**控制面**端口 (它本来就管"谁命令这条 run 改状态": 带 origin 审计 + 拒绝归一化)
|
|
24
24
|
import { submitRunControl } from '../kernel/control.js';
|
|
25
25
|
// K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
|
|
26
26
|
import { ModelRuntime, snapshotFromSelection } from '../kernel/model-runtime.js';
|
|
27
|
+
// K10 余项: 会话生命周期的**规则**归内核 (落盘映射 · key 校验 · 读回过滤/水合 · 运行种子合并顺序), I/O 留本类经端口注入
|
|
28
|
+
import { submitSessionOp, assertSessionOk, composeRunSeed, filterSessionMessages, hydrateSessionMessages, } from '../kernel/session-lifecycle.js';
|
|
27
29
|
import { renderDelegateNotices, pushNotice, renderNoticeBlock } from './background-notices.js';
|
|
28
30
|
import { runWithWriteOrigin } from './skill-ledger.js';
|
|
29
31
|
import { createHash } from 'node:crypto';
|
|
@@ -31,7 +33,12 @@ import { shouldReviewTask, runExperienceReview } from './experience-review.js';
|
|
|
31
33
|
import { decideLessonSink, skillsFromDirs, logLessonSuggestions, routeLessonToSkill } from './lesson-to-skill.js';
|
|
32
34
|
import { managedServices } from './managed-services.js';
|
|
33
35
|
import { renderToolListWithParams } from './tool-subset.js';
|
|
34
|
-
|
|
36
|
+
// K10 余项: 收尾自检的**规则**归内核 (`kernel/code-write-gate.ts`) —— 原来是 agents 侧的一个模块
|
|
37
|
+
import { codeWriteTarget } from '../kernel/code-write-gate.js';
|
|
38
|
+
// K10 余项: 收尾自检的**编排与政策**也归内核 (该不该跑 · 系统自检也过门 · 被拒不执行且可见 · 失败不影响回合)
|
|
39
|
+
import { runTurnEndSelfCheck } from '../kernel/turn-selfcheck.js';
|
|
40
|
+
// K10 余项: 恢复一次运行的**编排与政策**也归内核 (漂移上报口径 · 只在漂了/核对不了时装 · 恢复态一定被清)
|
|
41
|
+
import { resumeRunViaKernel } from '../kernel/run-resume.js';
|
|
35
42
|
// 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
|
|
36
43
|
// require 是 undefined ⇒ 抛错被 catch 静默吞掉 ⇒ "自愈"根本没跑, DID 一直是空的 ✗。
|
|
37
44
|
import { loadOrCreateAgentIdentity } from './agent-identity.js';
|
|
@@ -453,7 +460,9 @@ export class PiAgentSession {
|
|
|
453
460
|
* · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
|
|
454
461
|
*/
|
|
455
462
|
seedRunContext(extra = {}) {
|
|
456
|
-
|
|
463
|
+
// **K10 余项**: 合并顺序 (活跃运行打底 · extra 覆盖) 是内核规则 —— 与 `seed-run` 端口共用 `composeRunSeed`
|
|
464
|
+
// "活跃运行"这一处读取仍**只在本方法体内** (K2 播种的唯一读点, 有门钉住); 合并顺序由内核给。
|
|
465
|
+
return createRunContext(composeRunSeed(this.actor.state.activeRun, extra));
|
|
457
466
|
}
|
|
458
467
|
/**
|
|
459
468
|
* **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
|
|
@@ -709,7 +718,7 @@ export class PiAgentSession {
|
|
|
709
718
|
if (this.actor) {
|
|
710
719
|
const n = await this.actor.hydrateHistory({
|
|
711
720
|
load: () => this._sessionStore.loadMessages(sessionKey),
|
|
712
|
-
filter: (loaded) =>
|
|
721
|
+
filter: (loaded) => filterSessionMessages(loaded),
|
|
713
722
|
maxMessages,
|
|
714
723
|
});
|
|
715
724
|
if (n > 0)
|
|
@@ -721,7 +730,7 @@ export class PiAgentSession {
|
|
|
721
730
|
console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
|
|
722
731
|
return;
|
|
723
732
|
}
|
|
724
|
-
const hydrated =
|
|
733
|
+
const hydrated = hydrateSessionMessages(loaded, maxMessages);
|
|
725
734
|
if (hydrated.length > 0) {
|
|
726
735
|
this.replaceHistory(hydrated);
|
|
727
736
|
console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
|
|
@@ -742,20 +751,24 @@ export class PiAgentSession {
|
|
|
742
751
|
* 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
|
|
743
752
|
* 即可获得"重启 / 跨进程接续"的语义.
|
|
744
753
|
*/
|
|
754
|
+
/**
|
|
755
|
+
* **K10 余项**: 取数拍 (Actor 快照, 走邮箱 ⇒ 与 append 串行) 与真写入仍是本类的;
|
|
756
|
+
* 而「消息 → 持久形态」的**映射规则**与 key 校验搬进内核 (`submitSessionOp` 的 `save-session`)。
|
|
757
|
+
* 写不进 ⇒ 响亮失败 (`assertSessionOk`), 不静默继续。
|
|
758
|
+
*/
|
|
745
759
|
async saveCurrentSession(key) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
await this._sessionStore.saveMessages(key, persisted);
|
|
760
|
+
const out = await submitSessionOp({ op: 'save-session', origin: 'pi-session', key }, this.sessionPorts());
|
|
761
|
+
assertSessionOk(out);
|
|
762
|
+
}
|
|
763
|
+
/** K10 余项: 会话生命周期端口实现 —— I/O 留本类, 规则在内核 (`kernel/session-lifecycle.ts`) */
|
|
764
|
+
sessionPorts() {
|
|
765
|
+
return {
|
|
766
|
+
historySnapshot: () => (this.actor ? this.actor.historySnapshot() : this.actor.state.messageHistory),
|
|
767
|
+
saveMessages: (key, messages) => this._sessionStore.saveMessages(key, messages),
|
|
768
|
+
loadMessages: (key) => this._sessionStore.loadMessages(key),
|
|
769
|
+
// 注意: **故意不提供** `activeRunId` / `newRunContext` —— "活跃运行"的读取必须只有一处 (K2 播种唯一读点, 门钉住),
|
|
770
|
+
// 本类走的是**同步** `seedRunContext` + 内核的 `composeRunSeed`。异步 `seed-run` 端口留给别的调用方 (其语义由内核门覆盖)。
|
|
771
|
+
};
|
|
759
772
|
}
|
|
760
773
|
/**
|
|
761
774
|
* 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
|
|
@@ -773,47 +786,12 @@ export class PiAgentSession {
|
|
|
773
786
|
* 返回 Message[] 数组 (空数组表示无历史).
|
|
774
787
|
*/
|
|
775
788
|
async peekSessionHistory(key, maxMessages = 30) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
return [];
|
|
780
|
-
return this._filterToMessage(loaded).slice(-maxMessages);
|
|
781
|
-
}
|
|
782
|
-
catch {
|
|
783
|
-
return [];
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
/** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
|
|
787
|
-
_filterToMessage(loaded) {
|
|
788
|
-
const hydrated = [];
|
|
789
|
-
const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
|
|
790
|
-
for (const m of loaded) {
|
|
791
|
-
// role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
|
|
792
|
-
if (!VALID_ROLES.has(m.role))
|
|
793
|
-
continue;
|
|
794
|
-
// 跳过污染消息
|
|
795
|
-
if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
796
|
-
continue;
|
|
797
|
-
if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
798
|
-
continue;
|
|
799
|
-
// 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
|
|
800
|
-
// 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
|
|
801
|
-
// 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
|
|
802
|
-
if (!m.content && !m.toolCall && !m.toolResult)
|
|
803
|
-
continue;
|
|
804
|
-
// 跳过空 tool role (tool result 占位但没有任何内容)
|
|
805
|
-
if (m.role === 'tool' && !m.toolResult)
|
|
806
|
-
continue;
|
|
807
|
-
hydrated.push({
|
|
808
|
-
role: m.role,
|
|
809
|
-
content: m.content ?? '',
|
|
810
|
-
toolCall: m.toolCall,
|
|
811
|
-
toolResult: m.toolResult,
|
|
812
|
-
toolCallId: m.toolCallId,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
return hydrated;
|
|
789
|
+
// **K10 余项**: 读回过滤/水合规则归内核; 读失败 ⇒ 空数组, 但**原因进内核审计** (不再完全静默)
|
|
790
|
+
const out = await submitSessionOp({ op: 'peek-history', origin: 'pi-session', key, maxMessages }, this.sessionPorts());
|
|
791
|
+
return out.ok ? out.result : [];
|
|
816
792
|
}
|
|
793
|
+
// **K10 余项 (减法)**: 原来的 `_filterToMessage` (合法 role 白名单 · 剔污染消息 · 保留"只有 tool call 无正文"的合法消息)
|
|
794
|
+
// 已整条搬进内核 `kernel/session-lifecycle.ts` 的 `filterSessionMessages` —— 规则一处一份, 这里不再留副本。
|
|
817
795
|
/** 暴露 store 给测试 / 高级集成用. */
|
|
818
796
|
get sessionStoreInstance() {
|
|
819
797
|
return this._sessionStore;
|
|
@@ -1121,7 +1099,14 @@ export class PiAgentSession {
|
|
|
1121
1099
|
return `[初始化未就绪] ${why}\n${hint}`;
|
|
1122
1100
|
}
|
|
1123
1101
|
}
|
|
1124
|
-
catch {
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
// fail-closed: 门禁异常不能绕过初始化,否则会出现"身份未就绪但 Agent 已调用工具"。
|
|
1104
|
+
const why = `初始化门禁读取失败: ${String(err?.message ?? err).slice(0, 240)}`;
|
|
1105
|
+
this.pushHistory({ role: 'user', content: input });
|
|
1106
|
+
this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n请运行 bolloon setup status;修复后再重试。` });
|
|
1107
|
+
console.warn(`[PiAgent] 拒绝执行: ${why}`);
|
|
1108
|
+
return `[初始化未就绪] ${why}\n请运行 bolloon setup status;修复后再重试。`;
|
|
1109
|
+
}
|
|
1125
1110
|
}
|
|
1126
1111
|
this.minimaxAvailable = this.checkMinimax();
|
|
1127
1112
|
this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
|
|
@@ -1286,8 +1271,10 @@ export class PiAgentSession {
|
|
|
1286
1271
|
.catch(() => { });
|
|
1287
1272
|
}
|
|
1288
1273
|
catch { /* 落日志失败不影响主流程 */ }
|
|
1274
|
+
// 2026-10-02 (leo: 「cli 的 UI 需要有渲染过滤」): 这条是**内部运行日志** (复盘过程), 不是用户要看的结论
|
|
1275
|
+
// ⇒ 不再进用户可见对话流; 走 console (由 CLI 的渲染过滤只落盘, `--verbose` 下照原样上屏)。
|
|
1289
1276
|
try {
|
|
1290
|
-
|
|
1277
|
+
console.log(`📚 复盘: ${m}`);
|
|
1291
1278
|
}
|
|
1292
1279
|
catch { /* 上屏失败不打断 */ }
|
|
1293
1280
|
};
|
|
@@ -2088,7 +2075,8 @@ ${await this.renderActivePlansSection()}
|
|
|
2088
2075
|
* 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
|
|
2089
2076
|
*/
|
|
2090
2077
|
getLastRunId() {
|
|
2091
|
-
|
|
2078
|
+
// K10 余项: 取值优先级 (显式 lastRunId 优先 · 回落活跃运行) 是**内核规则**
|
|
2079
|
+
return pickRunIdForResume(this.lastRunId, this.actor.state.activeRun);
|
|
2092
2080
|
}
|
|
2093
2081
|
/**
|
|
2094
2082
|
* 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
|
|
@@ -2101,49 +2089,32 @@ ${await this.renderActivePlansSection()}
|
|
|
2101
2089
|
* 并把结论放进返回值 (`modelDrift`) 让调用方能上报; 一致时也有一句明确结论。
|
|
2102
2090
|
* 判断失败 (读不到 Run 等) 不阻塞恢复。
|
|
2103
2091
|
*/
|
|
2092
|
+
/**
|
|
2093
|
+
* **K10 余项**: 编排与政策归内核 (`kernel/run-resume.ts`) —— 这里只做七件事:
|
|
2094
|
+
* 核对漂移 · 准备 · 按需装配 · 落恢复态 · 驱动同一 runId 继续 · 清态 · 日志。
|
|
2095
|
+
* 语义 (漂移上报口径 · 只在漂了/核对不了时装 · 装配失败不阻塞 · 恢复态一定被清) 由内核拥有。
|
|
2096
|
+
*/
|
|
2104
2097
|
async resumeRun(runId) {
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
}
|
|
2113
|
-
else if (modelDrift) {
|
|
2114
|
-
console.log('[pi-sdk] 恢复前核对:', modelDrift.message);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
catch { /* 核对本身失败不阻塞恢复 */ }
|
|
2118
|
-
const prep = await prepareResume(runId);
|
|
2119
|
-
if (!prep.ok || !prep.plan)
|
|
2120
|
-
return { ok: false, reason: prep.reason, ...(modelDrift ? { modelDrift } : {}) };
|
|
2121
|
-
// 2026-09-26 (P6 × P7): 恢复一个在跑的 Run, 用的是**它自己那份快照**, 不是盘上现在的全局默认 ——
|
|
2122
|
-
// 否则这次恢复就是"带着漂移继续跑", 之后再存快照会把漂移固化成历史。
|
|
2123
|
-
// 只在"漂了"或"核对不了"时重装 (一致时运行时本来就是对的, 重装是白跑一趟)。
|
|
2124
|
-
// 装配只有一处实现: `applyRunModelConfigToRuntime` → `installRuntime` (这里不自己 initMinimax)。
|
|
2125
|
-
if (modelDrift && (modelDrift.drifted || !modelDrift.verified)) {
|
|
2126
|
-
try {
|
|
2098
|
+
const out = await resumeRunViaKernel(runId, {
|
|
2099
|
+
detectDrift: async (id) => {
|
|
2100
|
+
const { detectRunConfigDrift } = await import('../llm/model-selection.js');
|
|
2101
|
+
return ((await detectRunConfigDrift(id)) || undefined);
|
|
2102
|
+
},
|
|
2103
|
+
prepareResume: (id) => prepareResume(id),
|
|
2104
|
+
applySnapshot: async (snapshot) => {
|
|
2127
2105
|
const { applyRunModelConfigToRuntime } = await import('../llm/model-selection.js');
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
|
|
2141
|
-
return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
|
|
2142
|
-
}
|
|
2143
|
-
finally {
|
|
2144
|
-
this.resumeRunId = '';
|
|
2145
|
-
this.resumePlan = null;
|
|
2146
|
-
}
|
|
2106
|
+
return (await applyRunModelConfigToRuntime(snapshot));
|
|
2107
|
+
},
|
|
2108
|
+
applyPlan: (plan) => {
|
|
2109
|
+
this.resumeRunId = runId;
|
|
2110
|
+
this.resumePlan = plan;
|
|
2111
|
+
this.actor.state.goalBinding = plan.goalId || this.actor.state.goalBinding;
|
|
2112
|
+
},
|
|
2113
|
+
continueRun: (plan) => this.prompt(buildResumeInstruction(plan), {}),
|
|
2114
|
+
clearPlan: () => { this.resumeRunId = ''; this.resumePlan = null; },
|
|
2115
|
+
log: (level, body) => (level === 'warn' ? console.warn('[pi-sdk] ' + body) : console.log('[pi-sdk] ' + body)),
|
|
2116
|
+
});
|
|
2117
|
+
return out;
|
|
2147
2118
|
}
|
|
2148
2119
|
/**
|
|
2149
2120
|
* 2026-10-02 (K7): 把**本会话的 Harness 判定**包成 skill 门, 供 `BollharnessIntegration.setSkillGuard` 注入。
|
|
@@ -2198,46 +2169,24 @@ ${await this.renderActivePlansSection()}
|
|
|
2198
2169
|
* (`usePivotLoop: true`, `promptWithPivotLoop` 提前 return) ⇒ 这条"系统自检过门"在**生产根本没触发过**。
|
|
2199
2170
|
* 抽成方法后两条 loop 路径都调它 (一次一轮, 由 `typecheckRanThisTurn` 保证)。
|
|
2200
2171
|
*/
|
|
2172
|
+
/**
|
|
2173
|
+
* **K10 余项**: 编排与政策归内核 (`kernel/turn-selfcheck.ts`) —— 这里只做三件事: 取工具 · 问门 · 上报。
|
|
2174
|
+
* 语义 (该不该跑 · 系统自检也过门 · 被拒不执行且可见 · 失败不影响回合) 由内核拥有, 换入口不必各写一份。
|
|
2175
|
+
* 状态维护 (一轮只跑一次 / 清空本回合触达) 跟内核的 `decided` 走: 与迁移前逐条一致。
|
|
2176
|
+
*/
|
|
2201
2177
|
async runTurnEndTypecheck() {
|
|
2202
|
-
|
|
2203
|
-
|
|
2178
|
+
const out = await runTurnEndSelfCheck({ touched: this.tsTouchedThisTurn, ranThisTurn: this.typecheckRanThisTurn }, {
|
|
2179
|
+
getTool: (name) => this.tools.get(name),
|
|
2180
|
+
askGate: async (req) => this.piHarness().beforeToolCall({
|
|
2181
|
+
tool: req.tool,
|
|
2182
|
+
args: req.args,
|
|
2183
|
+
ctx: this.harnessCtx(),
|
|
2184
|
+
permissionMode: this.currentPermissionMode,
|
|
2185
|
+
}),
|
|
2186
|
+
emit: (evt) => this.runCtx.eventSink?.({ type: 'status', content: evt.content, tool: 'system' }),
|
|
2187
|
+
});
|
|
2188
|
+
if (out.decided) {
|
|
2204
2189
|
this.typecheckRanThisTurn = true;
|
|
2205
|
-
try {
|
|
2206
|
-
const tscTool = this.tools.get('tsc_check');
|
|
2207
|
-
if (tscTool?.execute) {
|
|
2208
|
-
// 2026-10-02 (K7): **系统自检也过门** —— 统一成"任何工具执行都走同一扇 Harness 门"。
|
|
2209
|
-
// 这里不是模型发起的工具调用 (args 恒空, 是本回合改过 TS 后的收尾自检), 所以**更**不能例外:
|
|
2210
|
-
// 例外一旦靠"没人知道它绕过"活着, 门就不再是唯一的执行咽喉。
|
|
2211
|
-
// 语义: 被拒或门抛错 ⇒ **不执行** (fail-closed), 且**可见**地报出来 (拒绝不许静默)。
|
|
2212
|
-
let tscAllowed = true;
|
|
2213
|
-
let tscWhy = '';
|
|
2214
|
-
try {
|
|
2215
|
-
const d = await this.piHarness().beforeToolCall({
|
|
2216
|
-
tool: 'tsc_check',
|
|
2217
|
-
args: {},
|
|
2218
|
-
ctx: this.harnessCtx(),
|
|
2219
|
-
permissionMode: this.currentPermissionMode,
|
|
2220
|
-
});
|
|
2221
|
-
tscAllowed = d.allow;
|
|
2222
|
-
tscWhy = d.reason || d.rejectedBy || '';
|
|
2223
|
-
}
|
|
2224
|
-
catch (gateErr) {
|
|
2225
|
-
tscAllowed = false;
|
|
2226
|
-
tscWhy = `harness-error: ${String(gateErr?.message || gateErr)}`;
|
|
2227
|
-
}
|
|
2228
|
-
if (!tscAllowed) {
|
|
2229
|
-
this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查被门拒绝, 未执行: ${tscWhy.slice(0, 120)}`, tool: 'system' });
|
|
2230
|
-
}
|
|
2231
|
-
else {
|
|
2232
|
-
const r = await tscTool.execute({});
|
|
2233
|
-
const line = formatTypecheckResult(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
|
|
2234
|
-
this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
catch (e) {
|
|
2239
|
-
this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
|
|
2240
|
-
}
|
|
2241
2190
|
this.tsTouchedThisTurn = [];
|
|
2242
2191
|
}
|
|
2243
2192
|
}
|
|
@@ -384,8 +384,12 @@ export function buildPostMessage(input) {
|
|
|
384
384
|
}
|
|
385
385
|
return { ok: true, text: [...base, `verdict=${v}`].join(' ') };
|
|
386
386
|
}
|
|
387
|
-
/**
|
|
388
|
-
|
|
387
|
+
/**
|
|
388
|
+
* 2026-10-02: 富字段版 (leo: 「@成员 / 回复某条 / 图片音频 / 建分支」)。
|
|
389
|
+
* **text 与 mentions 都过隐私闸** —— @ 的内容可能被写成 DID/地址形态, 一样不许出网。
|
|
390
|
+
* 附件只带 CID (字节在内容寻址层), CID 本身不是隐私形态。
|
|
391
|
+
*/
|
|
392
|
+
export async function sendTrailMessage(groupId, text, from, rich = {}) {
|
|
389
393
|
const gate = requirePublicText(text);
|
|
390
394
|
if (!gate.ok) {
|
|
391
395
|
return {
|
|
@@ -398,10 +402,23 @@ export async function sendTrailMessage(groupId, text, from) {
|
|
|
398
402
|
if (!g.ok) {
|
|
399
403
|
return { ok: false, sent: false, text, error: `发送者标记命中隐私红线: ${g.violations.map((v) => v.rule).join(', ')}`, violations: g.violations, localFallback: false };
|
|
400
404
|
}
|
|
401
|
-
const
|
|
405
|
+
const mentionList = Array.isArray(rich.mentions) ? rich.mentions.map((x) => String(x ?? '').trim()).filter(Boolean) : [];
|
|
406
|
+
for (const m of mentionList) {
|
|
407
|
+
const mg = requirePublicText(m);
|
|
408
|
+
if (!mg.ok) {
|
|
409
|
+
return { ok: false, sent: false, text, error: `@ 的内容命中隐私红线, 拒绝发送: ${mg.violations.map((v) => v.rule).join(', ')}`, violations: mg.violations, localFallback: false };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const r = await groupSend(groupId, text, from, {
|
|
413
|
+
...(mentionList.length ? { mentions: mentionList } : {}),
|
|
414
|
+
...(rich.replyTo ? { replyTo: rich.replyTo } : {}),
|
|
415
|
+
...(rich.attachments?.length ? { attachments: rich.attachments } : {}),
|
|
416
|
+
...(rich.branch ? { branch: rich.branch } : {}),
|
|
417
|
+
...(rich.kind ? { kind: rich.kind } : {}),
|
|
418
|
+
});
|
|
402
419
|
if (!r.ok)
|
|
403
420
|
return { ok: false, sent: false, text, error: r.error || '群消息没发出去', localFallback: false };
|
|
404
|
-
return { ok: true, sent: true, text, localFallback: false };
|
|
421
|
+
return { ok: true, sent: true, text, ...(r.id ? { id: r.id } : {}), localFallback: false };
|
|
405
422
|
}
|
|
406
423
|
/** 解析一行群消息 (不是本模块发的 / 格式不认识 → null, 不猜) */
|
|
407
424
|
export function parseTrailLine(text) {
|
package/dist/cli/log-gate.js
CHANGED
|
@@ -116,6 +116,54 @@ const OPTIONAL_BOOT_TASK = /(?:本地\s*IPNS\s*发布失败|IPNS\s*发布失败[
|
|
|
116
116
|
// `[info]` 里的 info 还会被 ANSI 染色 ⇒ 两条都让旧规则漏网, 于是
|
|
117
117
|
// `2026-09-30T10:22:44.768Z [info]: 结果: ❌ 失败 (30004ms)` 照样上屏 (leo 报过多次)。
|
|
118
118
|
const SDK_DIAG_LINE = /^(?:\[[^\]]*\]\s*)?\d{4}-\d{2}-\d{2}T[\d:.]+Z?\s*\[(?:\x1b\[[0-9;]*m)*(?:info|debug)(?:\x1b\[[0-9;]*m)*\]:/i;
|
|
119
|
+
/**
|
|
120
|
+
* **对话流渲染过滤** (2026-10-02, leo: 「cli 的 UI 需要有渲染过滤」)。
|
|
121
|
+
*
|
|
122
|
+
* 真机污染样本 (用户贴的原文): `⚠ [identity] channel=… · 查到=yes · …` ·
|
|
123
|
+
* `[pi-ai timing] total=1143ms …` · `📚 复盘: 开始(换了任务 ⇒ 立刻复盘)` ·
|
|
124
|
+
* `✗ [session-note] 已更新: …` · `[PiAgent] reviewFinal …` (含**被截断的 prompt 片段**)。
|
|
125
|
+
*
|
|
126
|
+
* 口径与启动期门同一套: **按行首模块标签锚定**, 不做宽泛匹配 (免得吞掉用户内容) ——
|
|
127
|
+
* 只认实测过的这些内部模块; **带 `⚠/✗` 字样也不例外** (它们是内部诊断, 不是"需人介入")。
|
|
128
|
+
* 去处: 仍**一字不漏落日志文件**; `BOLLOON_VERBOSE=1` 照原样上屏 ⇒ 诊断能力一点不丢。
|
|
129
|
+
* 这是把「要不要占屏」和「要不要留痕」分开, 不是把问题藏起来。
|
|
130
|
+
*/
|
|
131
|
+
// 允许标签前有一个**严重度字形** (`⚠ [identity] …` / `✗ [session-note] …` 真机就是这样) —— 只放这几个字形, 不做通配
|
|
132
|
+
const INTERNAL_CHATTER_TAG = /^[ \t]{0,8}[⚠✗✓!·]?[ \t]*\[(?:pi-ai(?:\s+timing)?|identity|session-note|react-harness|DocumentStore|DocumentReceiver|kv-server|loadSkills|v3-manifest|v3-async|supervisor-host|tool-telemetry|runs|abort|broadcast)\b[^\]\n]*\]/i;
|
|
133
|
+
/**
|
|
134
|
+
* 没有 ASCII 标签、但实测属于内部运行日志的行 (2026-10-02 leo 追加: 「✅ 最终回复 (质量 8.5/10) ·
|
|
135
|
+
* 🔄 循环 N/10 这类也去掉」)。
|
|
136
|
+
*
|
|
137
|
+
* 定位: 这批本来是**发送点自己声明** `internal: true` (见 `reply-hygiene.ts` / pivot loop 的遥测),
|
|
138
|
+
* 这里是**兜底** —— 历史/未标的同族事件、以及包一层后丢了标记的副本, 也要在同一处收口。
|
|
139
|
+
* 所以它**只认实测过的具体形状** (不做通配), 且与正向声明共用同一个判据函数。
|
|
140
|
+
*/
|
|
141
|
+
const INTERNAL_CHATTER_LINE = [
|
|
142
|
+
/^\s*📚\s*复盘[::]/,
|
|
143
|
+
/^\s*\[PiAgent\]\s*reviewFinal\b/,
|
|
144
|
+
/^\s*\[PiAgent\]\s*推理适配器\s*=/,
|
|
145
|
+
/^\s*[✅✓]\s*(?:检测到)?最终回复/,
|
|
146
|
+
/^\s*🔄\s*循环\s*\d+\s*[//]\s*\d+/,
|
|
147
|
+
/^\s*🔍\s*任务复杂度[::]/,
|
|
148
|
+
/^\s*⚙️?\s*动态配置[::]/,
|
|
149
|
+
/^\s*⏹️?\s*pivot loop/,
|
|
150
|
+
/^\s*📋\s*参数/,
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* 循环过渡噪音: **行首锚定**, 不算任意子串 —— 老黑名单用的是 `content.includes('继续循环')`,
|
|
154
|
+
* 那会把像"好的, 继续总结一下"这种正经状态行一起吞掉 (误吞比漏吞更糟, 见本文件头)。
|
|
155
|
+
* 实测的真身形状: `工具执行完成继续循环` · `继续总结上一段` 这类**以它开头**的推进过程行。
|
|
156
|
+
*/
|
|
157
|
+
const INTERNAL_CHATTER_SUBSTR = [/^\s*工具执行完成\s*继续循环/, /^\s*继续(?:循环|总结)/];
|
|
158
|
+
/** 这一行是不是**内部诊断** (只该进日志, 不该进对话流) */
|
|
159
|
+
export function isInternalChatter(line) {
|
|
160
|
+
if (!line)
|
|
161
|
+
return false;
|
|
162
|
+
const l = String(line).replace(/\x1b\[[0-9;]*m/g, '');
|
|
163
|
+
return INTERNAL_CHATTER_TAG.test(l)
|
|
164
|
+
|| INTERNAL_CHATTER_LINE.some((re) => re.test(l))
|
|
165
|
+
|| INTERNAL_CHATTER_SUBSTR.some((re) => re.test(l));
|
|
166
|
+
}
|
|
119
167
|
/** 这一行是不是「错误 / 降级 / 需人介入」—— 是则任何模式下都不许被静默 */
|
|
120
168
|
export function carriesHumanSignal(line) {
|
|
121
169
|
if (!line)
|
|
@@ -228,7 +276,7 @@ export function installStartupLogGate(opts = {}) {
|
|
|
228
276
|
const rerouteSignals = mode === 'cli-interactive';
|
|
229
277
|
const writeFile = opts.writeFile !== false;
|
|
230
278
|
const stats = {
|
|
231
|
-
suppressed: 0, forwarded: 0, signalKept: 0, fileLines: 0, fileError: null,
|
|
279
|
+
suppressed: 0, forwarded: 0, signalKept: 0, internalSuppressed: 0, fileLines: 0, fileError: null,
|
|
232
280
|
};
|
|
233
281
|
const filtering = !verbose && !disabled;
|
|
234
282
|
// ---- 日志文件 (append; 每 chunk 一次 appendFileSync, 保证进程被强杀也不丢) ----
|
|
@@ -351,7 +399,11 @@ export function installStartupLogGate(opts = {}) {
|
|
|
351
399
|
const toFile = [];
|
|
352
400
|
for (const line of text.split('\n')) {
|
|
353
401
|
toFile.push(line);
|
|
354
|
-
|
|
402
|
+
// 2026-10-02: **内部诊断行优先判** —— 只落盘, 一律不进对话流 (即便带 ⚠/✗ 字样也不会被当成"需人介入")
|
|
403
|
+
if (isInternalChatter(line)) {
|
|
404
|
+
stats.internalSuppressed++;
|
|
405
|
+
}
|
|
406
|
+
else if (carriesHumanSignal(line)) {
|
|
355
407
|
// 需人介入的信息必须可见: 交互 CLI 的 stdout 归 Ink, 所以走 stderr
|
|
356
408
|
stats.signalKept++;
|
|
357
409
|
origStderr(line + '\n');
|
|
@@ -366,9 +418,21 @@ export function installStartupLogGate(opts = {}) {
|
|
|
366
418
|
function makeConsoleError() {
|
|
367
419
|
return (...args) => {
|
|
368
420
|
const text = formatConsoleArgs(args);
|
|
369
|
-
|
|
421
|
+
// 2026-10-02: error 里混进的**内部诊断行**同样只落盘 (`✗ [session-note] 已更新: …` 就是这么漏进对话流的);
|
|
422
|
+
// 真正的错误 (没有内部标签) 照旧一行不吞 —— 文件那份**全留**(留痕), 只有 stderr 这份过渲染过滤。
|
|
423
|
+
const lines = text.split('\n');
|
|
424
|
+
const passToScreen = lines.filter((l) => {
|
|
425
|
+
if (isInternalChatter(l)) {
|
|
426
|
+
stats.internalSuppressed++;
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
writeLines(lines);
|
|
432
|
+
if (passToScreen.length === 0)
|
|
433
|
+
return;
|
|
370
434
|
stats.forwarded++;
|
|
371
|
-
origStderr(
|
|
435
|
+
origStderr(passToScreen.join('\n') + '\n');
|
|
372
436
|
};
|
|
373
437
|
}
|
|
374
438
|
let stopped = false;
|
package/dist/cli/mention-data.js
CHANGED
|
@@ -48,7 +48,7 @@ const CLI_COMMANDS = [
|
|
|
48
48
|
{ kind: 'command', label: 'cron', hint: '定时任务', insert: 'cron' },
|
|
49
49
|
{ kind: 'command', label: 'dq', hint: '队列 (dequeue)', insert: 'dq' },
|
|
50
50
|
{ kind: 'command', label: 'goals', hint: '目标', insert: 'goals' },
|
|
51
|
-
{ kind: 'command', label: 'group', hint: 'OrbitDB 群聊 (new/join/send/log/members)', insert: 'group' },
|
|
51
|
+
{ kind: 'command', label: 'group', hint: 'OrbitDB 群聊 (new/join/send/log/members/invite/kick/link/privacy/status)', insert: 'group' },
|
|
52
52
|
{ kind: 'command', label: 'judgments', hint: '判断记录', insert: 'judgments' },
|
|
53
53
|
{ kind: 'command', label: 'net', hint: 'Agent 网络 (join/status/ctx)', insert: 'net' },
|
|
54
54
|
{ kind: 'command', label: 'p2p', hint: 'P2P 节点 / 连接', insert: 'p2p' },
|