@bolloon/bolloon-agent 0.3.9 → 0.3.10

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.
@@ -31,7 +31,8 @@ import { registerBuiltinTools, registerWalletTools, setupInboxListener, Idempote
31
31
  export { createAgentSession, getAgentSession, resetAgentSession, runSelfImproveLoop, } from './pi-sdk-session-factory.js';
32
32
  // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
33
33
  // 失败静默, 不阻塞主对话
34
- import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
34
+ import { injectJudgmentGate, injectNegativeGuard, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
35
+ import { getInjectionMaxChars } from '../bootstrap/exhaust-scrubber.js';
35
36
  // 持续监控门 (P3): AI 回复后审计是否违反原则
36
37
  import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
37
38
  // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
@@ -109,6 +110,8 @@ export class PiAgentSession {
109
110
  */
110
111
  judgmentGateAddition = '';
111
112
  judgmentGateUsedIds = [];
113
+ /** 2026-07-22 设计 B: 负向判断力 (避免清单) 注入用到的 judgment id */
114
+ judgmentGateNegativeUsedIds = [];
112
115
  // 2026-06-18: 来自 web server markedPrompt 外的 contextHint (channel/judgment/distill/remote channels),
113
116
  // 拼到 systemPrompt 末尾, 别再混进 user message
114
117
  contextHintAddition = '';
@@ -147,11 +150,25 @@ export class PiAgentSession {
147
150
  try {
148
151
  // P-Action 4 (2026-06-15) 路径 1 整合: 透传 maxChars=1500 (≈ 375 tokens 硬上限)
149
152
  // 路径 2/3 检测由 injection-gate 内部 alreadyInjectedSources 处理 (目前 assembleSystemPrompt 还没注入 value-store 标记, 所以这里不传)
150
- const gate = await injectJudgmentGate(input, {}, { maxChars: 1500 });
153
+ // 2026-07-22 设计 C: maxChars 读背压动态值 (涡轮增压进气调参)
154
+ // 上下文紧张 (high) → 收紧 800; 宽裕 (idle/low) → 放宽 1800; 默认 medium 1500
155
+ const gate = await injectJudgmentGate(input, {}, { maxChars: getInjectionMaxChars() });
151
156
  this.judgmentGateAddition = gate.systemAddition;
152
157
  this.judgmentGateUsedIds = gate.usedIds;
153
- if (gate.usedIds.length > 0) {
154
- safePhase('gate_done', { usedCount: gate.usedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
158
+ // 2026-07-22 设计 B: 负向判断力回收 — "避免清单"注入 (显式, prompt)
159
+ // 判断力负向是"判断力"非"废气", 可进 prompt 作为约束 (精准 = 正向指引 + 负向避免)
160
+ try {
161
+ const neg = await injectNegativeGuard(input, {}, { maxChars: 300 });
162
+ if (neg.didInject && neg.systemAddition) {
163
+ this.judgmentGateAddition += '\n' + neg.systemAddition;
164
+ this.judgmentGateNegativeUsedIds = neg.usedIds;
165
+ }
166
+ }
167
+ catch (negErr) {
168
+ console.warn('[PiAgent] negative guard failed (non-fatal):', negErr);
169
+ }
170
+ if (this.judgmentGateUsedIds.length > 0 || this.judgmentGateNegativeUsedIds.length > 0) {
171
+ safePhase('gate_done', { usedCount: this.judgmentGateUsedIds.length, negativeCount: this.judgmentGateNegativeUsedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
155
172
  }
156
173
  }
157
174
  catch (err) {
@@ -163,6 +180,7 @@ export class PiAgentSession {
163
180
  clearJudgmentGate() {
164
181
  this.judgmentGateAddition = '';
165
182
  this.judgmentGateUsedIds = [];
183
+ this.judgmentGateNegativeUsedIds = [];
166
184
  }
167
185
  constructor(config) {
168
186
  this.cwd = config.cwd;
@@ -491,7 +509,10 @@ export class PiAgentSession {
491
509
  }
492
510
  finally {
493
511
  if (this.judgmentGateUsedIds.length > 0) {
494
- recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
512
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input, polarity: 'positive' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
513
+ }
514
+ if (this.judgmentGateNegativeUsedIds.length > 0) {
515
+ recordJudgmentUsage(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
495
516
  }
496
517
  this.clearJudgmentGate();
497
518
  this.currentSignal = null;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * exhaust-scrubber.ts — 上下文废气涡轮 (2026-07-22 设计 C)
3
+ *
4
+ * 思想锚点: 涡轮增压 (Turbocharger)
5
+ * 排气 (废气) = session-window dropped / memory-compressor skipped / compaction stage drops / truncation
6
+ * 涡轮 (本模块) = 采样丢弃事件, 聚合成"背压"指标
7
+ * 进气增压 = 背压反向调进气侧参数 (压缩阈值 / 检索 top-k / judgment 注入 maxChars)
8
+ * 燃烧室 (prompt) = 废气**不进**这里 (保持精准), 只让压力调参
9
+ *
10
+ * 拍板: 上下文废气 → 不进 prompt, 只调参, 进 log/memory, 隐式
11
+ *
12
+ * 三个职责:
13
+ * 1. recordExhaust(event): 采样丢弃事件 → 环形缓冲 + 落盘 ~/.bolloon/engine/backpressure.jsonl (log)
14
+ * 2. getBackpressure(): 算背压等级 (idle/low/medium/high) — 进气侧读它调参
15
+ * 3. getInjectionMaxChars(level): 背压 → judgment 注入 maxChars 映射 (进气增压)
16
+ * 4. maybeWriteExhaustMemory(): 背压高峰持续 → 模板摘要写 memory (月度滚动, 不调 LLM)
17
+ *
18
+ * 设计原则:
19
+ * - 零新数据源: 只订阅已有丢弃事件
20
+ * - 隐式: 用户看不到废气内容, 只看到压力等级 (可选背压表)
21
+ * - 静默: 任何失败 console.warn 不阻塞主流程
22
+ * - 不存原文: 只存 source + reason + 估算 token 数 (防隐私/膨胀)
23
+ */
24
+ import * as fs from 'fs/promises';
25
+ import * as os from 'os';
26
+ import * as path from 'path';
27
+ // ============== 路径 ==============
28
+ function getEngineDir(home) {
29
+ return path.join(home || os.homedir(), '.bolloon', 'engine');
30
+ }
31
+ function getBackpressureLogPath(home) {
32
+ return path.join(getEngineDir(home), 'backpressure.jsonl');
33
+ }
34
+ function getMemoryEngineDir(agentId, home) {
35
+ // 跟 memory-compressor.ts 一致: ~/.bolloon/memory/<agentId>/engine/
36
+ const safe = agentId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
37
+ return path.join(home || os.homedir(), '.bolloon', 'memory', safe, 'engine');
38
+ }
39
+ // ============== 状态 (模块级单例, 跟 chat-archiver 同模式) ==============
40
+ const RING_CAPACITY = 100;
41
+ const ringBuffer = [];
42
+ let droppedTokensTotal = 0;
43
+ const bySource = {};
44
+ let monthlyHighCount = 0; // 当月 high 事件累计 (触发 memory 摘要用)
45
+ // ============== 采样 (涡轮入口) ==============
46
+ /**
47
+ * 记录一个废气事件. 推入环形缓冲 + 落盘 jsonl (log). 静默失败.
48
+ *
49
+ * @param event 废气事件 (source + reason + 可选 droppedTokens; ts 不传则自动填)
50
+ * @param home 可选 home 目录 (测试注入)
51
+ */
52
+ export async function recordExhaust(event, home) {
53
+ try {
54
+ const full = {
55
+ ts: event.ts || new Date().toISOString(),
56
+ source: event.source,
57
+ reason: event.reason,
58
+ droppedTokens: event.droppedTokens,
59
+ };
60
+ // 环形缓冲
61
+ ringBuffer.push(full);
62
+ if (ringBuffer.length > RING_CAPACITY) {
63
+ ringBuffer.shift();
64
+ }
65
+ // 计数
66
+ if (full.droppedTokens && full.droppedTokens > 0) {
67
+ droppedTokensTotal += full.droppedTokens;
68
+ }
69
+ bySource[full.source] = (bySource[full.source] || 0) + 1;
70
+ // 落盘 jsonl (log) — append 模式, 跟 chat-archiver 同
71
+ const logPath = getBackpressureLogPath(home);
72
+ await fs.mkdir(path.dirname(logPath), { recursive: true });
73
+ await fs.appendFile(logPath, JSON.stringify(full) + '\n', 'utf-8');
74
+ // high 事件累计 → 触发 memory 摘要 (火忘)
75
+ const snap = getBackpressure(home);
76
+ if (snap.level === 'high') {
77
+ monthlyHighCount++;
78
+ // 每 10 次 high 触发一次 memory 摘要 (节流, 防频繁写盘)
79
+ if (monthlyHighCount % 10 === 0) {
80
+ maybeWriteExhaustMemorySummary('default', home).catch(() => { });
81
+ }
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.warn('[exhaust-scrubber] recordExhaust failed (non-fatal):', err);
86
+ }
87
+ }
88
+ /**
89
+ * 同步版本 (供不方便 await 的调用方; 只更新内存环形缓冲, 不落盘).
90
+ * 落盘由下次 async recordExhaust 或显式 flush 触发.
91
+ */
92
+ export function recordExhaustSync(event) {
93
+ try {
94
+ const full = {
95
+ ts: event.ts || new Date().toISOString(),
96
+ source: event.source,
97
+ reason: event.reason,
98
+ droppedTokens: event.droppedTokens,
99
+ };
100
+ ringBuffer.push(full);
101
+ if (ringBuffer.length > RING_CAPACITY)
102
+ ringBuffer.shift();
103
+ if (full.droppedTokens && full.droppedTokens > 0)
104
+ droppedTokensTotal += full.droppedTokens;
105
+ bySource[full.source] = (bySource[full.source] || 0) + 1;
106
+ }
107
+ catch { /* 静默 */ }
108
+ }
109
+ // ============== 聚合 (背压等级) ==============
110
+ /**
111
+ * 算当前背压快照. 基于环形缓冲 + 最近 60s 事件速率.
112
+ *
113
+ * 等级映射 (dropRatePerMin = 最近 60s 内事件数 / 60 * 60... 简化为最近 60s 事件数本身):
114
+ * 0 → idle
115
+ * 1-2 → low
116
+ * 3-10 → medium
117
+ * > 10 → high
118
+ */
119
+ export function getBackpressure(_home) {
120
+ const now = Date.now();
121
+ const recent = ringBuffer.filter((e) => {
122
+ const t = Date.parse(e.ts);
123
+ return Number.isFinite(t) && now - t < 60_000; // 最近 60s
124
+ });
125
+ const dropRatePerMin = recent.length;
126
+ let level;
127
+ if (dropRatePerMin === 0)
128
+ level = 'idle';
129
+ else if (dropRatePerMin <= 2)
130
+ level = 'low';
131
+ else if (dropRatePerMin <= 10)
132
+ level = 'medium';
133
+ else
134
+ level = 'high';
135
+ const srcCount = {};
136
+ for (const e of ringBuffer) {
137
+ srcCount[e.source] = (srcCount[e.source] || 0) + 1;
138
+ }
139
+ return {
140
+ level,
141
+ dropCount: ringBuffer.length,
142
+ droppedTokensTotal,
143
+ dropRatePerMin,
144
+ lastTs: ringBuffer.length > 0 ? ringBuffer[ringBuffer.length - 1].ts : '',
145
+ bySource: srcCount,
146
+ };
147
+ }
148
+ export function getPressureLevel(home) {
149
+ return getBackpressure(home).level;
150
+ }
151
+ // ============== 进气增压 (背压 → 调参) ==============
152
+ /**
153
+ * 背压 → judgment 注入 maxChars 映射 (进气增压核心).
154
+ *
155
+ * idle/low → 1800 (放宽注入, 上下文宽裕)
156
+ * medium → 1500 (默认, 现状)
157
+ * high → 800 (收紧注入, 上下文紧张, 留空间给主任务)
158
+ *
159
+ * 调用方: pi-sdk.ts computeJudgmentGate 的 maxChars 从固定 1500 改为读这个.
160
+ */
161
+ export function getInjectionMaxChars(level, home) {
162
+ const lvl = level ?? getPressureLevel(home);
163
+ switch (lvl) {
164
+ case 'idle':
165
+ case 'low':
166
+ return 1800;
167
+ case 'medium':
168
+ return 1500;
169
+ case 'high':
170
+ return 800;
171
+ default:
172
+ return 1500;
173
+ }
174
+ }
175
+ /**
176
+ * 背压 → 检索 top-k 映射 (进气增压, 检索侧).
177
+ * idle/low → 8, medium → 5, high → 3
178
+ */
179
+ export function getRetrievalTopK(level, home) {
180
+ const lvl = level ?? getPressureLevel(home);
181
+ switch (lvl) {
182
+ case 'idle':
183
+ case 'low':
184
+ return 8;
185
+ case 'medium':
186
+ return 5;
187
+ case 'high':
188
+ return 3;
189
+ default:
190
+ return 5;
191
+ }
192
+ }
193
+ // ============== memory 落地 (月度摘要, 拍板要求"进 memory") ==============
194
+ /**
195
+ * 背压高峰持续 → 模板摘要写 memory (月度滚动, 不调 LLM).
196
+ * ~/.bolloon/memory/<agentId>/engine/exhaust-<YYYY-MM>.summary.md (append)
197
+ *
198
+ * 让"为什么这段时间上下文一直紧张"沉淀进 memory 供 agent 回看.
199
+ * 节流: 每 10 次 high 触发一次 (recordExhaust 内控制).
200
+ */
201
+ export async function maybeWriteExhaustMemorySummary(agentId, home) {
202
+ try {
203
+ const snap = getBackpressure(home);
204
+ if (snap.level !== 'high')
205
+ return { written: false };
206
+ const now = new Date();
207
+ const yearMonth = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
208
+ const dir = getMemoryEngineDir(agentId, home);
209
+ const file = path.join(dir, `exhaust-${yearMonth}.summary.md`);
210
+ await fs.mkdir(dir, { recursive: true });
211
+ const block = `\n\n---\n\n## 引擎背压高峰 @ ${now.toISOString()} (level=high)\n\n` +
212
+ `- 最近 60s 丢弃事件: ${snap.dropRatePerMin} 次/min\n` +
213
+ `- 环形缓冲事件总数: ${snap.dropCount}\n` +
214
+ `- 估算丢弃 token 累计: ${snap.droppedTokensTotal}\n` +
215
+ `- 按 source 分布: ${Object.entries(snap.bySource).map(([k, v]) => `${k}=${v}`).join(', ') || '(无)'}\n` +
216
+ `- 含义: 上下文持续紧张, 进气侧已自动收紧 (judgment 注入 maxChars=800, 检索 top-k=3)\n`;
217
+ await fs.appendFile(file, block, 'utf-8');
218
+ return { written: true, path: file };
219
+ }
220
+ catch (err) {
221
+ console.warn('[exhaust-scrubber] maybeWriteExhaustMemorySummary failed (non-fatal):', err);
222
+ return { written: false };
223
+ }
224
+ }
225
+ // ============== 测试辅助 ==============
226
+ /** 重置模块状态 (仅测试用) */
227
+ export function __resetForTest() {
228
+ ringBuffer.length = 0;
229
+ droppedTokensTotal = 0;
230
+ for (const k of Object.keys(bySource))
231
+ delete bySource[k];
232
+ monthlyHighCount = 0;
233
+ }
@@ -161,6 +161,17 @@ export async function compressSessionToMemory(opts) {
161
161
  await fs.mkdir(path.dirname(summaryPath), { recursive: true });
162
162
  await fs.appendFile(summaryPath, block, 'utf-8');
163
163
  await writeCursor(cursorPath, allMessages.length);
164
+ // 2026-07-22 设计 C: 废气采样 — 压缩成功 = 上下文需要压缩的信号, 记入涡轮 (隐式)
165
+ // 废气不进 prompt, 只调参 (背压高 → judgment 注入收紧). 落 log/memory.
166
+ try {
167
+ const { recordExhaust } = await import('./exhaust-scrubber.js');
168
+ recordExhaust({
169
+ source: 'memory-compressor',
170
+ reason: 'compress-summary-written',
171
+ droppedTokens: newMessages.length * 200, // 粗估 200 tokens/msg
172
+ }, opts.home).catch(() => { });
173
+ }
174
+ catch { /* 静默 */ }
164
175
  return {
165
176
  summaryPath,
166
177
  cursorPath,
@@ -138,7 +138,7 @@ function formatInjection(values, mode, resolvedCount, maxChars = 1500) {
138
138
  // 使用记录 (回溯): AI 实际"用了"哪些判断力
139
139
  // ============================================================
140
140
  const USAGE_LOG = (os.homedir() || '/tmp') + '/.bolloon/human-values/usage.jsonl';
141
- export async function recordJudgmentUsage(usedIds, meta) {
141
+ export async function recordJudgmentUsage(usedIds, meta = {}) {
142
142
  if (usedIds.length === 0)
143
143
  return;
144
144
  try {
@@ -147,6 +147,7 @@ export async function recordJudgmentUsage(usedIds, meta) {
147
147
  channelId: meta.channelId ?? null,
148
148
  userInputPreview: (meta.userInput ?? '').substring(0, 80),
149
149
  usedIds,
150
+ polarity: meta.polarity ?? 'positive',
150
151
  };
151
152
  await fs.appendFile(USAGE_LOG, JSON.stringify(entry) + '\n', 'utf-8');
152
153
  }
@@ -154,6 +155,89 @@ export async function recordJudgmentUsage(usedIds, meta) {
154
155
  console.warn('[injection-gate] recordJudgmentUsage failed:', err);
155
156
  }
156
157
  }
158
+ // ============================================================
159
+ // 2026-07-22 设计 B: 负向判断力回收 — "避免清单"注入 (显式, 进 prompt)
160
+ //
161
+ // 涡轮增压锚点: 判断力的负向是"判断力"不是"上下文废气", 可进 prompt 作为约束
162
+ // (精准 = 正向指引 + 负向避免). 从 reject 类 + 高 stakes + 高 confidence 选 Top N,
163
+ // 以"避免清单"语义产出 systemAddition. maxChars=300 (远小于正向 1500, 防噪音).
164
+ // ============================================================
165
+ export const DEFAULT_NEGATIVE_CONFIG = {
166
+ topN: 3,
167
+ mode: 'concise',
168
+ skip: false,
169
+ maxChars: 300,
170
+ /** 最低置信度门槛 (只注入足够可信的否决) */
171
+ minConfidence: 0.7,
172
+ };
173
+ function emptyGateResult(skipReason) {
174
+ return { systemAddition: '', usedIds: [], matchedCount: 0, didInject: false, skipReason };
175
+ }
176
+ /**
177
+ * 负向判断力注入门: 给定用户输入, 返回"避免清单"追加文本 + 用到的负向 judgment id.
178
+ *
179
+ * 筛选: decision_type='reject' && status='active' && stakes∈{high,critical} && confidence>=minConfidence
180
+ * 排序: critical 优先, 再按 confidence desc
181
+ * 静默: 任意步骤失败返回空字符串, 不 throw (主对话不阻塞)
182
+ */
183
+ export async function injectNegativeGuard(userInput, _ctx = {}, options = {}) {
184
+ const cfg = { ...DEFAULT_NEGATIVE_CONFIG, ...options };
185
+ if (cfg.skip)
186
+ return emptyGateResult('skip');
187
+ if (!userInput || userInput.trim().length === 0)
188
+ return emptyGateResult('no-input');
189
+ try {
190
+ const all = await loadAllJudgments();
191
+ const negatives = all.filter((j) => j.decision_type === 'reject' &&
192
+ (j.status ?? 'active') === 'active' &&
193
+ (j.context?.stakes === 'high' || j.context?.stakes === 'critical') &&
194
+ (j.metadata?.confidence ?? 0.5) >= (cfg.minConfidence ?? 0.7));
195
+ if (negatives.length === 0)
196
+ return emptyGateResult('empty-negatives');
197
+ // 排序: critical > high, 再按 confidence desc
198
+ negatives.sort((a, b) => {
199
+ const sa = a.context?.stakes === 'critical' ? 2 : 1;
200
+ const sb = b.context?.stakes === 'critical' ? 2 : 1;
201
+ const ca = a.metadata?.confidence ?? 0.5;
202
+ const cb = b.metadata?.confidence ?? 0.5;
203
+ return (sb - sa) || (cb - ca);
204
+ });
205
+ const top = negatives.slice(0, cfg.topN);
206
+ const usedIds = top.map((j) => j.id);
207
+ const systemAddition = formatNegativeInjection(top, cfg.maxChars);
208
+ return {
209
+ systemAddition,
210
+ usedIds,
211
+ matchedCount: negatives.length,
212
+ didInject: true,
213
+ skipReason: null,
214
+ };
215
+ }
216
+ catch (err) {
217
+ console.warn('[injection-gate] injectNegativeGuard failed (silent fallback):', err);
218
+ return emptyGateResult('exception');
219
+ }
220
+ }
221
+ function formatNegativeInjection(items, maxChars) {
222
+ if (items.length === 0)
223
+ return '';
224
+ const SOURCE_TAG = '<!-- source: injection-gate (negative) -->';
225
+ const lines = items.map((j, i) => {
226
+ const stakes = j.context?.stakes === 'critical' ? ' [关键风险]' : ' [高风险]';
227
+ const decision = (j.decision || '').slice(0, 80);
228
+ return `${i + 1}. 避免: ${decision}${stakes}`;
229
+ });
230
+ let result = `${SOURCE_TAG}\n` +
231
+ `# 避免清单 (负向判断力, 自动注入)\n` +
232
+ `- 以下行为已被明确否决, 执行时主动规避; 如情境冲突在回复中说明\n` +
233
+ `${lines.join('\n')}\n`;
234
+ if (maxChars > 0 && result.length > maxChars) {
235
+ result =
236
+ result.substring(0, maxChars) +
237
+ '\n[System Note: 避免清单因长度截断, 这是背景约束, 不影响用户实际请求.]\n';
238
+ }
239
+ return result;
240
+ }
157
241
  /**
158
242
  * 给定 channelId, 取最近 N 条 usage 记录 (UI 显示用)
159
243
  */
@@ -3438,8 +3438,16 @@ ${data.error || "channel not found"}`, "error");
3438
3438
  });
3439
3439
  renderJudgments(lastJudgmentsCache);
3440
3440
  }
3441
+ function switchPolarity(polarity) {
3442
+ currentPolarity = polarity;
3443
+ currentAdvancedFilter = null;
3444
+ document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
3445
+ btn.classList.toggle("active", btn.dataset.polarity === polarity);
3446
+ });
3447
+ loadJudgments();
3448
+ }
3441
3449
  function switchStatusFilter(status) {
3442
- currentStatusFilter = status;
3450
+ currentAdvancedFilter = status;
3443
3451
  document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
3444
3452
  btn.classList.toggle("active", btn.dataset.status === status);
3445
3453
  });
@@ -3464,7 +3472,8 @@ ${data.error || "channel not found"}`, "error");
3464
3472
  if (judgmentsModal) judgmentsModal.classList.remove("active");
3465
3473
  }
3466
3474
  var currentJudgmentTab = "channel";
3467
- var currentStatusFilter = "all";
3475
+ var currentPolarity = "positive";
3476
+ var currentAdvancedFilter = null;
3468
3477
  var lastJudgmentsCache = [];
3469
3478
  function renderJudgments(items) {
3470
3479
  if (!judgmentsList) return;
@@ -3554,7 +3563,7 @@ ${data.error || "channel not found"}`, "error");
3554
3563
  async function loadJudgments() {
3555
3564
  if (!judgmentsList) return;
3556
3565
  try {
3557
- if (currentStatusFilter === "violations") {
3566
+ if (currentAdvancedFilter === "violations") {
3558
3567
  const res2 = await fetch("/api/judgments/violations?limit=50");
3559
3568
  if (!res2.ok) throw new Error("HTTP " + res2.status);
3560
3569
  const data2 = await res2.json();
@@ -3562,7 +3571,7 @@ ${data.error || "channel not found"}`, "error");
3562
3571
  judgmentsLoaded = true;
3563
3572
  return;
3564
3573
  }
3565
- if (currentStatusFilter === "adaptive") {
3574
+ if (currentAdvancedFilter === "adaptive") {
3566
3575
  const res2 = await fetch("/api/judgments/adaptive-suggestions");
3567
3576
  if (!res2.ok) throw new Error("HTTP " + res2.status);
3568
3577
  const data2 = await res2.json();
@@ -3570,7 +3579,7 @@ ${data.error || "channel not found"}`, "error");
3570
3579
  judgmentsLoaded = true;
3571
3580
  return;
3572
3581
  }
3573
- if (currentStatusFilter === "causal") {
3582
+ if (currentAdvancedFilter === "causal") {
3574
3583
  const res2 = await fetch("/api/judgments/causal/correlation?topN=10");
3575
3584
  if (!res2.ok) throw new Error("HTTP " + res2.status);
3576
3585
  const data2 = await res2.json();
@@ -3578,17 +3587,25 @@ ${data.error || "channel not found"}`, "error");
3578
3587
  judgmentsLoaded = true;
3579
3588
  return;
3580
3589
  }
3581
- const res = await fetch("/api/judgments?status=" + encodeURIComponent(currentStatusFilter));
3590
+ const POSITIVE_TYPES = ["approve", "modify", "escalate"];
3591
+ const fetchStatus = currentPolarity === "positive" ? "active" : "all";
3592
+ const res = await fetch("/api/judgments?status=" + encodeURIComponent(fetchStatus));
3582
3593
  if (!res.ok) throw new Error("HTTP " + res.status);
3583
3594
  const data = await res.json();
3584
- lastJudgmentsCache = data.judgments || [];
3585
- renderJudgments(lastJudgmentsCache);
3595
+ let list = data.judgments || [];
3596
+ if (currentPolarity === "positive") {
3597
+ list = list.filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active");
3598
+ } else {
3599
+ list = list.filter((j) => j.decision_type === "reject" || ["rejected", "superseded"].includes(j.status ?? ""));
3600
+ }
3601
+ lastJudgmentsCache = list;
3602
+ renderJudgments(list);
3586
3603
  if (judgmentsBadge) {
3587
3604
  let activeCount;
3588
- if (currentStatusFilter === "active") {
3605
+ if (currentPolarity === "positive") {
3589
3606
  activeCount = data.count;
3590
3607
  } else {
3591
- activeCount = lastJudgmentsCache.filter((j) => (j.status ?? "active") === "active").length;
3608
+ activeCount = (data.judgments || []).filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active").length;
3592
3609
  }
3593
3610
  if (activeCount > 0) {
3594
3611
  judgmentsBadge.textContent = activeCount;
@@ -3863,6 +3880,9 @@ ${data.error || "channel not found"}`, "error");
3863
3880
  document.querySelectorAll(".judgment-tab").forEach((btn) => {
3864
3881
  btn.addEventListener("click", () => switchJudgmentTab(btn.dataset.tab));
3865
3882
  });
3883
+ document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
3884
+ btn.addEventListener("click", () => switchPolarity(btn.dataset.polarity));
3885
+ });
3866
3886
  document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
3867
3887
  btn.addEventListener("click", () => switchStatusFilter(btn.dataset.status));
3868
3888
  });
@@ -3980,12 +4000,14 @@ ${data.error || "channel not found"}`, "error");
3980
4000
  judgmentSubmitBtn.disabled = true;
3981
4001
  if (judgmentError) judgmentError.style.display = "none";
3982
4002
  try {
4003
+ const polarity = document.querySelector('input[name="judgment-polarity"]:checked')?.value || "positive";
3983
4004
  const res = await fetch("/api/judgments", {
3984
4005
  method: "POST",
3985
4006
  headers: { "Content-Type": "application/json" },
3986
4007
  body: JSON.stringify({
3987
4008
  decision,
3988
4009
  reason: reason || void 0,
4010
+ decision_type: polarity === "negative" ? "reject" : "approve",
3989
4011
  context: { domain: judgmentDomain?.value, stakes: judgmentStakes?.value }
3990
4012
  })
3991
4013
  });
@@ -205,21 +205,39 @@
205
205
  <input type="text" id="judgment-reason" placeholder="例: 信任 Bolloon 的判断存储">
206
206
  </div>
207
207
  <div class="form-group judgment-form-row">
208
- <label>领域 (domain)</label>
209
- <select id="judgment-domain">
210
- <option value="general">general</option>
211
- <option value="code">code</option>
212
- <option value="architecture">architecture</option>
213
- <option value="security">security</option>
214
- <option value="testing">testing</option>
215
- </select>
216
- <label>风险 (stakes)</label>
217
- <select id="judgment-stakes">
218
- <option value="medium">medium</option>
219
- <option value="low">low</option>
220
- <option value="high">high</option>
221
- <option value="critical">critical</option>
222
- </select>
208
+ <label>分类</label>
209
+ <div class="judgment-polarity-toggle" id="judgment-polarity-toggle">
210
+ <label class="polarity-opt active" data-polarity="positive">
211
+ <input type="radio" name="judgment-polarity" value="positive" checked>
212
+ <span>▲ 正向 <small>采纳</small></span>
213
+ </label>
214
+ <label class="polarity-opt" data-polarity="negative">
215
+ <input type="radio" name="judgment-polarity" value="negative">
216
+ <span>▼ 负向 <small>否决/避免</small></span>
217
+ </label>
218
+ </div>
219
+ <details class="judgment-advanced-fold" style="margin-left:auto;">
220
+ <summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">高级 (领域/风险)</summary>
221
+ <div style="display:flex;gap:10px;padding:6px 0 0;flex-wrap:wrap;font-size:11px;">
222
+ <label>领域
223
+ <select id="judgment-domain">
224
+ <option value="general">general</option>
225
+ <option value="code">code</option>
226
+ <option value="architecture">architecture</option>
227
+ <option value="security">security</option>
228
+ <option value="testing">testing</option>
229
+ </select>
230
+ </label>
231
+ <label>风险
232
+ <select id="judgment-stakes">
233
+ <option value="medium">medium</option>
234
+ <option value="low">low</option>
235
+ <option value="high">high</option>
236
+ <option value="critical">critical</option>
237
+ </select>
238
+ </label>
239
+ </div>
240
+ </details>
223
241
  </div>
224
242
  <div class="btn-group">
225
243
  <button id="judgment-submit-btn" class="btn-primary">记录</button>
@@ -235,15 +253,21 @@
235
253
  </button>
236
254
  <button class="judgment-tab" data-tab="global">全局</button>
237
255
  </div>
238
- <!-- AI 演化 status 过滤: active / superseded / all / violations / 自适应 -->
256
+ <!-- 2026-07-22 简化: 正向 / 负向 两个主分类 (替换原 6 status tab)
257
+ 正向 = 采纳类 (approve/modify/escalate), 会注入 prompt 复用
258
+ 负向 = 否决/被推翻 (reject/rejected/superseded), 负向回收为避免清单
259
+ 高级分析 (违规/自适应/因果) 折叠保留, 数据/API 不删 -->
239
260
  <div id="judgments-status-filter" class="judgment-status-bar">
240
- <span class="label">演化过滤:</span>
241
- <button class="judgment-status-tab active" data-status="all">全部</button>
242
- <button class="judgment-status-tab" data-status="active">活跃</button>
243
- <button class="judgment-status-tab" data-status="superseded">已过时</button>
244
- <button class="judgment-status-tab" data-status="violations">违规记录</button>
245
- <button class="judgment-status-tab" data-status="adaptive">📊 自适应</button>
246
- <button class="judgment-status-tab" data-status="causal">🔍 因果分析</button>
261
+ <button class="judgment-polarity-tab active" data-polarity="positive" title="采纳类: approve/modify/escalate, 会注入 prompt 复用">▲ 正向</button>
262
+ <button class="judgment-polarity-tab" data-polarity="negative" title="否决/被推翻: reject/rejected/superseded, 负向回收为避免清单">▼ 负向</button>
263
+ <details class="judgment-advanced-fold" style="margin-left:auto;">
264
+ <summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">⋯ 高级分析</summary>
265
+ <div style="display:flex;gap:4px;padding:6px 0 0;flex-wrap:wrap;">
266
+ <button class="judgment-status-tab" data-status="violations" style="font-size:11px;">违规记录</button>
267
+ <button class="judgment-status-tab" data-status="adaptive" style="font-size:11px;">📊 自适应</button>
268
+ <button class="judgment-status-tab" data-status="causal" style="font-size:11px;">🔍 因果分析</button>
269
+ </div>
270
+ </details>
247
271
  </div>
248
272
  <h3 class="judgment-list-header">
249
273
  <span id="judgments-list-title">本 channel 的判断力</span>
@@ -36,15 +36,21 @@ export function registerJudgmentsRoutes(app) {
36
36
  // 极简版: 只记录 decision + reason; 其它字段可选
37
37
  app.post('/api/judgments', async (req, res) => {
38
38
  try {
39
- const { decision, reason, context } = req.body;
39
+ const { decision, reason, context, decision_type } = req.body;
40
40
  if (!decision || typeof decision !== 'string' || !decision.trim()) {
41
41
  return res.status(400).json({ error: 'decision required' });
42
42
  }
43
+ // 2026-07-22: 接受前端 polarity toggle 传来的 decision_type (正=approve / 负=reject)
44
+ // 不传或非法 → 默认 approve (向后兼容)
45
+ const allowedTypes = ['approve', 'reject', 'modify', 'escalate'];
46
+ const finalType = decision_type && allowedTypes.includes(decision_type)
47
+ ? decision_type
48
+ : 'approve';
43
49
  const { storeHumanJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
44
50
  await initializeValueStore();
45
51
  const j = await storeHumanJudgment({
46
52
  decision: decision.trim(),
47
- decision_type: 'approve',
53
+ decision_type: finalType,
48
54
  reasons: reason ? [reason.trim()] : [],
49
55
  values_derived: [],
50
56
  context: {
@@ -4484,6 +4484,17 @@ ${goalDesc}
4484
4484
  }
4485
4485
  });
4486
4486
  // 获取 iroh 节点信息
4487
+ // 2026-07-22 设计 C: 引擎背压 API (涡轮增压表, 隐式可观测 — 废气内容不暴露, 只展示压力等级)
4488
+ app.get('/api/engine/backpressure', async (_req, res) => {
4489
+ try {
4490
+ const { getBackpressure } = await import('../bootstrap/exhaust-scrubber.js');
4491
+ const snap = getBackpressure();
4492
+ res.json({ ok: true, ...snap });
4493
+ }
4494
+ catch (err) {
4495
+ res.status(500).json({ error: err.message });
4496
+ }
4497
+ });
4487
4498
  app.get('/api/iroh/info', async (_req, res) => {
4488
4499
  if (!irohInitialized || !irohNodeInfo) {
4489
4500
  res.json({ initialized: false });
@@ -1795,6 +1795,36 @@ body {
1795
1795
  background: var(--accent);
1796
1796
  color: var(--bg);
1797
1797
  }
1798
+
1799
+ /* 2026-07-22 设计 A: 正向/负向主分类 tab + 表单 toggle */
1800
+ .judgment-polarity-tab {
1801
+ background: var(--bg-hover);
1802
+ color: var(--text-secondary);
1803
+ border: none;
1804
+ padding: 4px 14px;
1805
+ border-radius: 4px;
1806
+ cursor: pointer;
1807
+ font-size: 12px;
1808
+ font-weight: 600;
1809
+ transition: var(--transition);
1810
+ }
1811
+ .judgment-polarity-tab:hover { background: var(--bg-active); color: var(--text-primary); }
1812
+ .judgment-polarity-tab[data-polarity="positive"].active { background: #059669; color: #fff; }
1813
+ .judgment-polarity-tab[data-polarity="negative"].active { background: #dc2626; color: #fff; }
1814
+ .judgment-advanced-fold > summary::-webkit-details-marker { display: none; }
1815
+
1816
+ .judgment-polarity-toggle { display: flex; gap: 4px; }
1817
+ .polarity-opt {
1818
+ display: flex; align-items: center; gap: 4px;
1819
+ padding: 3px 10px; border-radius: 4px; cursor: pointer;
1820
+ font-size: 12px; background: var(--bg-hover); color: var(--text-secondary);
1821
+ border: 1px solid transparent; transition: var(--transition);
1822
+ }
1823
+ .polarity-opt input { display: none; }
1824
+ .polarity-opt small { color: var(--text-muted); font-weight: 400; }
1825
+ .polarity-opt.active[data-polarity="positive"] { background: #ecfdf5; color: #047857; border-color: #6ee7b7; }
1826
+ .polarity-opt.active[data-polarity="negative"] { background: #fef2f2; color: #b91c1c; border-color: #fca5a5; }
1827
+
1798
1828
  .judgment-form-row {
1799
1829
  display: flex;
1800
1830
  gap: 8px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",