@bolloon/bolloon-agent 0.1.34 → 0.1.36
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/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +144 -0
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +687 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +117 -0
- package/dist/bootstrap/context-collector.js +312 -0
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +171 -0
- package/dist/bootstrap/project-context.js +158 -0
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +49 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +125 -28
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/input-scanner.js +223 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +3810 -3830
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +60 -49
- package/dist/web/server.js +827 -124
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +33 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +481 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/validate-system-prompt.ts +142 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +741 -30
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +135 -0
- package/src/bootstrap/context-collector.ts +371 -0
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +289 -0
- package/src/bootstrap/project-context.ts +171 -0
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +47 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +135 -29
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/input-scanner.ts +287 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +105 -2187
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +60 -49
- package/src/web/server.ts +894 -148
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const web_search = {
|
|
2
|
+
id: 'web_search',
|
|
3
|
+
name: 'web_search',
|
|
4
|
+
oneLine: '搜索 Web (返回前 10 条结果).',
|
|
5
|
+
description: '当需要当前信息或信息可能自知识截止以来已发生变化时使用. 1-6 词查询最佳. 默认改写; 引用应是个罕见的例外.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'当前状态 (谁担任某职位、什么政策生效)',
|
|
8
|
+
'时效性信息 (股价、新闻)',
|
|
9
|
+
'不识别的实体 (默认搜索)',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'永恒信息/成熟技术事实 (直接答)',
|
|
13
|
+
'已故人物 (不会变)',
|
|
14
|
+
],
|
|
15
|
+
parameters: [
|
|
16
|
+
{ name: 'query', type: 'string', required: true, description: '搜索查询 (1-6 词最佳)' },
|
|
17
|
+
],
|
|
18
|
+
callExample: `[TOOL:web_search]
|
|
19
|
+
[P:query]S&P 500 current price
|
|
20
|
+
[ENDTOOL]`,
|
|
21
|
+
layerId: 'tool.web_search',
|
|
22
|
+
};
|
|
23
|
+
export const web_fetch = {
|
|
24
|
+
id: 'web_fetch',
|
|
25
|
+
name: 'web_fetch',
|
|
26
|
+
oneLine: '获取指定 URL 网页的内容.',
|
|
27
|
+
description: '此函数只能获取用户直接提供的或由 web_search 和 web_fetch 工具的结果中返回的 EXACT URL. 此工具无法访问需要身份验证的内容, 例如私有 Google Docs 或登录墙后的页面. URL 必须包含协议.',
|
|
28
|
+
whenToUse: [
|
|
29
|
+
'web_search 摘要不够, 需要读完整文章',
|
|
30
|
+
'用户提供 URL 让你读',
|
|
31
|
+
],
|
|
32
|
+
whenNotToUse: [
|
|
33
|
+
'需要身份验证的页面',
|
|
34
|
+
'web_search 已经足够时',
|
|
35
|
+
],
|
|
36
|
+
parameters: [
|
|
37
|
+
{ name: 'url', type: 'string', required: true, description: '完整 URL (含协议, https://...)', format: 'uri' },
|
|
38
|
+
{ name: 'allowed_domains', type: 'array', required: false, description: '仅允许的域名白名单 (如 ["example.com","*.github.com"])', items: { name: 'd', type: 'string', required: true, description: '域名' } },
|
|
39
|
+
{ name: 'blocked_domains', type: 'array', required: false, description: '黑名单域名', items: { name: 'd', type: 'string', required: true, description: '域名' } },
|
|
40
|
+
{ name: 'html_extraction_method', type: 'string', required: false, description: 'HTML 提取方法 (e.g. "text"/"markdown")' },
|
|
41
|
+
{ name: 'is_zdr', type: 'boolean', required: false, description: '是否 Zero Data Retention 请求 (true 时不记录 URL)' },
|
|
42
|
+
{ name: 'text_content_token_limit', type: 'integer', required: false, description: 'token 限制', minimum: 0 },
|
|
43
|
+
{ name: 'web_fetch_pdf_extract_text', type: 'boolean', required: false, description: '是否提取 PDF 文本' },
|
|
44
|
+
{ name: 'web_fetch_rate_limit_key', type: 'string', required: false, description: '速率限制键' },
|
|
45
|
+
{ name: 'web_fetch_rate_limit_dark_launch', type: 'boolean', required: false, description: '速率限制暗启动模式' },
|
|
46
|
+
],
|
|
47
|
+
callExample: `[TOOL:web_fetch]
|
|
48
|
+
[P:url]https://example.com/article
|
|
49
|
+
[ENDTOOL]`,
|
|
50
|
+
layerId: 'tool.manifest',
|
|
51
|
+
};
|
|
@@ -64,6 +64,29 @@ export class P2PDirect extends EventEmitter {
|
|
|
64
64
|
conn.on('data', (chunk) => {
|
|
65
65
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
66
|
console.log(`[P2PDirect:${this.name}] 收到数据 from ${remotePubKeyHex.substring(0, 12)}... (${buf.length} bytes)`);
|
|
67
|
+
// P-Action 3: untrusted-input scanner (默认 silence-on-fail + log-only)
|
|
68
|
+
// 包装层 跨机器 @-mention 代发 (已 WORKING) 不能 block-on-suspicion
|
|
69
|
+
try {
|
|
70
|
+
// 动态 import 避免循环依赖 + 启动时加载
|
|
71
|
+
// (lazy require to keep cold-start fast)
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
73
|
+
const { scanInput, writeScanAudit, shouldHardBlock } = require('../security/input-scanner.js');
|
|
74
|
+
const result = scanInput(buf, { source: 'p2p' });
|
|
75
|
+
if (shouldHardBlock(result)) {
|
|
76
|
+
console.warn(`[P2PDirect:${this.name}] 阻断恶意消息 from ${remotePubKeyHex.substring(0, 12)}... (verdict=${result.verdict}, threats=${result.threats.length})`);
|
|
77
|
+
writeScanAudit(result, { fromPublicKey: remotePubKeyHex, blocked: true }).catch(() => { });
|
|
78
|
+
return; // 不 emit('data'), 跨机中转不传播
|
|
79
|
+
}
|
|
80
|
+
if (result.verdict !== 'pass') {
|
|
81
|
+
// log-only: 仍然 emit('data'), 但写 audit 留痕
|
|
82
|
+
writeScanAudit(result, { fromPublicKey: remotePubKeyHex, blocked: false }).catch(() => { });
|
|
83
|
+
console.log(`[P2PDirect:${this.name}] 输入扫描 verdict=${result.verdict} (${result.threats.length} threats) from ${remotePubKeyHex.substring(0, 12)}... (audit-only)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// scanner 自身挂掉 → 静默, 不阻断
|
|
88
|
+
console.warn(`[P2PDirect:${this.name}] input scanner failed (silent, fail-safe):`, err);
|
|
89
|
+
}
|
|
67
90
|
this.emit('data', {
|
|
68
91
|
data: buf,
|
|
69
92
|
fromPublicKey: remotePubKeyHex,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* source-intent-broadcaster.ts — 包装 P2PDirect, 收发 SourceIntentMsg
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* const p2p = new P2PDirect({ name: 'bolloon', role: 'source-agent' });
|
|
6
|
+
* await p2p.start();
|
|
7
|
+
* const sb = new SourceIntentBroadcaster(p2p, { agent: 'agent-A@mac1' });
|
|
8
|
+
* await sb.start();
|
|
9
|
+
*
|
|
10
|
+
* // 我方要改 src/foo.ts 行 42-50
|
|
11
|
+
* const conflict = await sb.reserve({ taskId, file: 'src/foo.ts', lines: [42, 50] });
|
|
12
|
+
* if (conflict) {
|
|
13
|
+
* // 对方已 reserve, 选: 让出 / 改别的 / 强制
|
|
14
|
+
* } else {
|
|
15
|
+
* sb.writeFileAndCommit(...);
|
|
16
|
+
* await sb.broadcastCommitIntent({ taskId, file, lines, sha, diffHash });
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* 主题: 复用现有 'bolloon-agent-harness' topic (P2PDirect 已 join),
|
|
20
|
+
* 上层加 'source-intent:' 前缀避免和别的协议混.
|
|
21
|
+
*/
|
|
22
|
+
import { EventEmitter } from 'events';
|
|
23
|
+
import { ReserveLock, } from './source-intent.js';
|
|
24
|
+
export class SourceIntentBroadcaster extends EventEmitter {
|
|
25
|
+
p2p;
|
|
26
|
+
agent;
|
|
27
|
+
topic;
|
|
28
|
+
waitMs;
|
|
29
|
+
lock = new ReserveLock();
|
|
30
|
+
started = false;
|
|
31
|
+
dataHandler = null;
|
|
32
|
+
constructor(p2p, opts) {
|
|
33
|
+
super();
|
|
34
|
+
this.p2p = p2p;
|
|
35
|
+
this.agent = opts.agent;
|
|
36
|
+
this.topic = opts.topic || 'bolloon-agent-harness';
|
|
37
|
+
this.waitMs = opts.waitMs ?? 200;
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
if (this.started)
|
|
41
|
+
return;
|
|
42
|
+
// 确保 p2p 已 join topic
|
|
43
|
+
await this.p2p.joinTopic(this.topic);
|
|
44
|
+
this.dataHandler = (data, from) => {
|
|
45
|
+
try {
|
|
46
|
+
const text = data.toString('utf8');
|
|
47
|
+
// 协议前缀 'source-intent:'
|
|
48
|
+
if (!text.startsWith('source-intent:'))
|
|
49
|
+
return;
|
|
50
|
+
const json = text.slice('source-intent:'.length);
|
|
51
|
+
const msg = JSON.parse(json);
|
|
52
|
+
this.onMessage(msg, from);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// 静默忽略非本协议消息
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
this.p2p.on('data', this.dataHandler);
|
|
59
|
+
this.started = true;
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (!this.started)
|
|
63
|
+
return;
|
|
64
|
+
if (this.dataHandler)
|
|
65
|
+
this.p2p.off('data', this.dataHandler);
|
|
66
|
+
this.started = false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 申请 reserve (file, lines).
|
|
70
|
+
* 返回 {ok:true} → 成功 (没有重叠)
|
|
71
|
+
* 返回 {ok:false, existing} → 本地 lock 已有重叠, LLM 决定让出 / 改方向 / 强制
|
|
72
|
+
*/
|
|
73
|
+
async reserve(args) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const msg = {
|
|
76
|
+
type: 'reserve',
|
|
77
|
+
taskId: args.taskId,
|
|
78
|
+
agent: this.agent,
|
|
79
|
+
file: args.file,
|
|
80
|
+
lines: args.lines,
|
|
81
|
+
expiresAt: now + ReserveLock.TTL_MS,
|
|
82
|
+
ts: now,
|
|
83
|
+
};
|
|
84
|
+
// 1. 先查本地 lock: 本端/远端任何 reserve 重叠都立即冲突 (避免 [5,15] 漏检 [1,10])
|
|
85
|
+
const existing = this.lock.isReserved(args.file, args.lines);
|
|
86
|
+
if (existing) {
|
|
87
|
+
return { ok: false, existing: { agent: existing.agent, lines: existing.lines } };
|
|
88
|
+
}
|
|
89
|
+
// 2. 写本地 lock
|
|
90
|
+
this.lock.add({
|
|
91
|
+
taskId: msg.taskId,
|
|
92
|
+
agent: msg.agent,
|
|
93
|
+
file: msg.file,
|
|
94
|
+
lines: msg.lines,
|
|
95
|
+
expiresAt: msg.expiresAt,
|
|
96
|
+
ts: msg.ts,
|
|
97
|
+
});
|
|
98
|
+
// 3. 广播
|
|
99
|
+
this.broadcastMsg(msg);
|
|
100
|
+
// 4. 等待 waitMs 看远端是否 ack 同一区间
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
this.off('remoteConflict', onRemote);
|
|
104
|
+
resolve({ ok: true });
|
|
105
|
+
}, this.waitMs);
|
|
106
|
+
const onRemote = (existing) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve({ ok: false, existing });
|
|
109
|
+
};
|
|
110
|
+
this.once('remoteConflict', onRemote);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** 释放我方 reserve */
|
|
114
|
+
release(args) {
|
|
115
|
+
const msg = {
|
|
116
|
+
type: 'release',
|
|
117
|
+
taskId: args.taskId,
|
|
118
|
+
agent: this.agent,
|
|
119
|
+
file: args.file,
|
|
120
|
+
lines: args.lines,
|
|
121
|
+
ts: Date.now(),
|
|
122
|
+
};
|
|
123
|
+
this.lock.release(args.file, args.lines);
|
|
124
|
+
this.broadcastMsg(msg);
|
|
125
|
+
}
|
|
126
|
+
/** commit 后广播, 供对方做轻量审计 (diffHash 验真伪) */
|
|
127
|
+
async broadcastCommitIntent(args) {
|
|
128
|
+
const msg = {
|
|
129
|
+
type: 'commit-intent',
|
|
130
|
+
taskId: args.taskId,
|
|
131
|
+
agent: this.agent,
|
|
132
|
+
file: args.file,
|
|
133
|
+
lines: args.lines,
|
|
134
|
+
sha: args.sha,
|
|
135
|
+
diffHash: args.diffHash,
|
|
136
|
+
ts: Date.now(),
|
|
137
|
+
};
|
|
138
|
+
this.broadcastMsg(msg);
|
|
139
|
+
}
|
|
140
|
+
/** 取所有 live reserves (给 pi-ai 注入到 LLM 系统 prompt) */
|
|
141
|
+
liveReserves() {
|
|
142
|
+
return this.lock.live();
|
|
143
|
+
}
|
|
144
|
+
// ---- 内部 ----
|
|
145
|
+
broadcastMsg(msg) {
|
|
146
|
+
const text = 'source-intent:' + JSON.stringify(msg);
|
|
147
|
+
this.p2p.broadcast(Buffer.from(text, 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
onMessage(msg, from) {
|
|
150
|
+
// 忽略自己发出的消息 (P2PDirect 自身会发到自己的 loopback)
|
|
151
|
+
if (msg.agent === this.agent)
|
|
152
|
+
return;
|
|
153
|
+
switch (msg.type) {
|
|
154
|
+
case 'reserve': {
|
|
155
|
+
// 检查本地是否已 reserve 重叠区间
|
|
156
|
+
const existing = this.lock.isReserved(msg.file, msg.lines);
|
|
157
|
+
if (existing) {
|
|
158
|
+
// 我方有重叠, 对方也 reserve → 触发 conflict
|
|
159
|
+
this.emit('remoteConflict', { agent: msg.agent, lines: msg.lines });
|
|
160
|
+
// 自己也回一个 ack 让对方知道我方已 reserve
|
|
161
|
+
const ack = {
|
|
162
|
+
type: 'ack',
|
|
163
|
+
taskId: existing.taskId,
|
|
164
|
+
agent: this.agent,
|
|
165
|
+
file: existing.file,
|
|
166
|
+
lines: existing.lines,
|
|
167
|
+
ts: Date.now(),
|
|
168
|
+
};
|
|
169
|
+
this.broadcastMsg(ack);
|
|
170
|
+
}
|
|
171
|
+
// 记入 lock (远端的 reserve 我方也要避开, LLM 看 prompt 知道)
|
|
172
|
+
this.lock.add({
|
|
173
|
+
taskId: msg.taskId,
|
|
174
|
+
agent: msg.agent,
|
|
175
|
+
file: msg.file,
|
|
176
|
+
lines: msg.lines,
|
|
177
|
+
expiresAt: msg.expiresAt,
|
|
178
|
+
ts: msg.ts,
|
|
179
|
+
});
|
|
180
|
+
this.emit('remoteReserve', msg);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'ack': {
|
|
184
|
+
// 远端 ack 我方已 reserve 同一区间 → 立即触发 conflict
|
|
185
|
+
const local = this.lock.isReserved(msg.file, msg.lines);
|
|
186
|
+
if (local) {
|
|
187
|
+
this.emit('remoteConflict', { agent: msg.agent, lines: msg.lines });
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'release': {
|
|
192
|
+
this.lock.release(msg.file, msg.lines);
|
|
193
|
+
this.emit('remoteRelease', msg);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'commit-intent': {
|
|
197
|
+
this.lock.release(msg.file, msg.lines);
|
|
198
|
+
this.emit('remoteCommit', msg);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* source-intent.ts — 行级 P2P 协作: 类型 + reserve.lock
|
|
3
|
+
*
|
|
4
|
+
* 为什么: 两台机上的智能体要协作维护同一份源码仓库, 粗粒度 task 协调不够
|
|
5
|
+
* (同一文件不同行会冲突). 用行级 reserve 让 LLM 在改前先广播"我要改 X 行",
|
|
6
|
+
* 对方看到重叠可让出/改方向/强制 merge.
|
|
7
|
+
*
|
|
8
|
+
* 数据流 (P2P RPC 消息, 走现有 P2PDirect.broadcast):
|
|
9
|
+
* reserve → 我方申请改 file:[start,end]
|
|
10
|
+
* ack → 对方也在这区间, 互相知道, 等 commit-intent 时看谁先到
|
|
11
|
+
* release → 我方放弃 (LLM 决定改别的行)
|
|
12
|
+
* commit-intent → 我方 commit 后广播, 附 diffHash, 对方用 diffHash 验真伪
|
|
13
|
+
*
|
|
14
|
+
* 兜底:
|
|
15
|
+
* - TTL 5min, reserve 不 release 就过期
|
|
16
|
+
* - 200ms 等待 + 检冲突, 极端 race 双方都收到 conflict, LLM 自己重选
|
|
17
|
+
* - diffHash 不去判语义, 行级 merge 后 lefthook 失败 → revert
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
/**
|
|
21
|
+
* 检测两个 LineRange 是否重叠.
|
|
22
|
+
* 规则: 任一区间的一端落在对方区间内 → 重叠.
|
|
23
|
+
* 不重叠示例: [1,5] vs [6,10] (边界相邻不算重叠).
|
|
24
|
+
*/
|
|
25
|
+
export function rangesOverlap(a, b) {
|
|
26
|
+
return a[0] <= b[1] && b[0] <= a[1];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ReserveLock — 本地内存表, 记录"我方 + 远端"所有 live reserves.
|
|
30
|
+
*
|
|
31
|
+
* 事件:
|
|
32
|
+
* 'added' 新增 reserve (我方或远端)
|
|
33
|
+
* 'removed' reserve 被释放 / 过期
|
|
34
|
+
* 'conflict' 检测到重叠 (a,b)
|
|
35
|
+
*/
|
|
36
|
+
export class ReserveLock extends EventEmitter {
|
|
37
|
+
/** key = `${file}:${start}-${end}` */
|
|
38
|
+
byKey = new Map();
|
|
39
|
+
/** TTL: 5 min, 兜底防止对方死掉/网络断后 reserve 永远不释放 */
|
|
40
|
+
static TTL_MS = 5 * 60 * 1000;
|
|
41
|
+
/** 清理过期 reserve, 触发 'removed' 事件 */
|
|
42
|
+
sweep(now = Date.now()) {
|
|
43
|
+
for (const [k, r] of this.byKey) {
|
|
44
|
+
if (r.expiresAt <= now) {
|
|
45
|
+
this.byKey.delete(k);
|
|
46
|
+
this.emit('removed', r);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** 我方/远端 reserve, 仅清自己区间后写入 (不清全表, 避免过期 reserve 干扰 add 测试) */
|
|
51
|
+
add(r) {
|
|
52
|
+
const key = keyOf(r.file, r.lines);
|
|
53
|
+
const prev = this.byKey.get(key);
|
|
54
|
+
this.byKey.set(key, r);
|
|
55
|
+
if (prev) {
|
|
56
|
+
// 同一区间已被对方 reserve, 我方再 reserve → 触发冲突
|
|
57
|
+
this.emit('conflict', { a: prev, b: r });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.emit('added', r);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** 释放指定 reserve (我方 LLM 决定让出时调用) */
|
|
64
|
+
release(file, lines) {
|
|
65
|
+
const key = keyOf(file, lines);
|
|
66
|
+
const r = this.byKey.get(key);
|
|
67
|
+
if (!r)
|
|
68
|
+
return null;
|
|
69
|
+
this.byKey.delete(key);
|
|
70
|
+
this.emit('removed', r);
|
|
71
|
+
return r;
|
|
72
|
+
}
|
|
73
|
+
/** 查询某 (file, lines) 是否已被 reserve (含我方) */
|
|
74
|
+
isReserved(file, lines, now = Date.now()) {
|
|
75
|
+
this.sweep(now);
|
|
76
|
+
for (const r of this.byKey.values()) {
|
|
77
|
+
if (r.file === file && rangesOverlap(r.lines, lines)) {
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** 取所有 live reserves (给 pi-ai 注入到系统 prompt) */
|
|
84
|
+
live(now = Date.now()) {
|
|
85
|
+
this.sweep(now);
|
|
86
|
+
return [...this.byKey.values()];
|
|
87
|
+
}
|
|
88
|
+
/** 调试: dump 全部 */
|
|
89
|
+
dump() {
|
|
90
|
+
return [...this.byKey.values()];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function keyOf(file, lines) {
|
|
94
|
+
return `${file}:${lines[0]}-${lines[1]}`;
|
|
95
|
+
}
|
|
96
|
+
/** 算 diffHash (git diff 的 sha256 前 16 字符) */
|
|
97
|
+
export function diffHashOf(diffText) {
|
|
98
|
+
const crypto = require('crypto');
|
|
99
|
+
return crypto.createHash('sha256').update(diffText, 'utf8').digest('hex').slice(0, 16);
|
|
100
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Scan — 类 B 自迭代入口
|
|
3
|
+
*
|
|
4
|
+
* 扫描 ~/.bolloon/human-values/usage.jsonl + judgments.json,
|
|
5
|
+
* 生成"自适应建议" (只读, 不改库). 用户在 UI 接受/拒绝后
|
|
6
|
+
* 才会真正生效, 写入 evolution.jsonl 留作审计.
|
|
7
|
+
*
|
|
8
|
+
* 设计原则 (类 B 边界):
|
|
9
|
+
* - 不自动改库 (避免 AI 越权)
|
|
10
|
+
* - 所有建议可逆 (接受/拒绝都行, 接受后写 evolution.jsonl 留痕)
|
|
11
|
+
* - 失败静默 (主对话不阻塞)
|
|
12
|
+
* - 单次扫描纯本地 + 内存计算, 无 LLM 调用 (避免 LLM 幻觉污染)
|
|
13
|
+
*
|
|
14
|
+
* 触发时机:
|
|
15
|
+
* - 用户主动: UI "📊 自适应" tab 点 "重新扫描"
|
|
16
|
+
* - 被动: 每天首次进入判断力 modal 时自动跑一次 (缓存 24h)
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { loadAllJudgments } from './human-value-store.js';
|
|
22
|
+
// 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
|
|
23
|
+
// 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
|
|
24
|
+
function getUsageLogPath() {
|
|
25
|
+
return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/usage.jsonl';
|
|
26
|
+
}
|
|
27
|
+
function getEvolutionLogPath() {
|
|
28
|
+
return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* P-Action 1: next-action hint (4 仓库都强调的错误信息 = 修复指令 思想)
|
|
32
|
+
* - deusyu: "Lint error messages should embed fix instructions, turning them into
|
|
33
|
+
* a self-correction loop the agent can execute."
|
|
34
|
+
* - walkinglabs lecture 10: "POST /api/reset-password returned 500. Check that
|
|
35
|
+
* the email service config exists..." 而非 "Test failed"
|
|
36
|
+
* - 马书 Capybara v8 4-class behavior mitigation
|
|
37
|
+
* - china-qijizhifeng Agent Debugger LLM 分析
|
|
38
|
+
*
|
|
39
|
+
* 在 adaptive-scan 启发式产出 hint, 不调 LLM (保持类 B 失败静默 + 无 LLM 不变量)
|
|
40
|
+
*/
|
|
41
|
+
export function suggestionHint(kind, action, metrics) {
|
|
42
|
+
switch (kind) {
|
|
43
|
+
case 'stale':
|
|
44
|
+
return action === 'deprecate'
|
|
45
|
+
? `此判断力 30+ 天未使用 (last ${metrics.daysSinceLastUse}d ago, total ${metrics.totalUsage}×). 建议: 接受废弃前, 跑一次 'npm run judgments:search "<topic>"' 确认是否真的不再相关; 若仍相关, 用 boost + active 标签标记, 不要 deprecate.`
|
|
46
|
+
: `Stale judgment 建议: review 后 决定 boost / deprecate. 7d 用量 ${metrics.usage7d}× 30d 用量 ${metrics.usage30d}×.`;
|
|
47
|
+
case 'rising':
|
|
48
|
+
return `🔥 30 天内用量翻倍 (7d=${metrics.usage7d}×, 30d=${metrics.usage30d}×). 建议: 接受 boost 提升该 judgment 的注入优先级; 同时检查 conflicts.jsonl 看是否与其他判断力冲突.`;
|
|
49
|
+
case 'unused':
|
|
50
|
+
return `此判断力从未被注入 (total ${metrics.totalUsage}× = 0). 建议: (1) 跑 'npm run judgments:debug <id>' 确认检测钩子是否正常工作; (2) 若是 false positive, 接受 reject; (3) 若需要激活, 接受 review + 触发 D 路径蒸馏.`;
|
|
51
|
+
case 'causal_conflict':
|
|
52
|
+
return `检测到与另一判断力因果冲突. 建议: 接受 review 后, 优先保留因果强度 (causal power) 高的那条; 或在 persona.json 加一条 human override. 详细因果矩阵: '~/.bolloon/human-values/causal-matrix.json'.`;
|
|
53
|
+
case 'low_causal_power':
|
|
54
|
+
return `此判断力的因果强度持续低 (causal power < 0.3). 建议: 接受 review 后考虑 deprecate; 跑 'npm run causal:audit <id>' 看是哪条 related event 拉低了分数.`;
|
|
55
|
+
default:
|
|
56
|
+
return `建议 review: ${action} 这条 judgment. 7d 用量 ${metrics.usage7d}× 30d 用量 ${metrics.usage30d}×.`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
async function readUsageLog() {
|
|
61
|
+
try {
|
|
62
|
+
const content = await fs.readFile(getUsageLogPath(), 'utf-8');
|
|
63
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
64
|
+
return lines
|
|
65
|
+
.map((l) => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(l);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
.filter((e) => Boolean(e) && Array.isArray(e?.usedIds));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function countByJudgment(entries) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const cutoff7d = now - 7 * DAY_MS;
|
|
82
|
+
const cutoff30d = now - 30 * DAY_MS;
|
|
83
|
+
const map = new Map();
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
const ts = new Date(e.ts).getTime();
|
|
86
|
+
for (const id of e.usedIds) {
|
|
87
|
+
const cur = map.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
|
|
88
|
+
cur.total++;
|
|
89
|
+
if (ts >= cutoff7d)
|
|
90
|
+
cur.last7d++;
|
|
91
|
+
if (ts >= cutoff30d)
|
|
92
|
+
cur.last30d++;
|
|
93
|
+
if (cur.lastUseTs === null || ts > cur.lastUseTs)
|
|
94
|
+
cur.lastUseTs = ts;
|
|
95
|
+
map.set(id, cur);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return map;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 主扫描函数: 纯本地计算, 无 LLM
|
|
102
|
+
*/
|
|
103
|
+
export async function runAdaptiveScan() {
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const nowTs = now.getTime();
|
|
106
|
+
const [judgments, entries] = await Promise.all([
|
|
107
|
+
loadAllJudgments(),
|
|
108
|
+
readUsageLog(),
|
|
109
|
+
]);
|
|
110
|
+
const usage = countByJudgment(entries);
|
|
111
|
+
const suggestions = [];
|
|
112
|
+
for (const j of judgments) {
|
|
113
|
+
if ((j.status ?? 'active') !== 'active')
|
|
114
|
+
continue;
|
|
115
|
+
const id = j.id;
|
|
116
|
+
const u = usage.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
|
|
117
|
+
const daysSinceLastUse = u.lastUseTs === null
|
|
118
|
+
? Math.floor((nowTs - new Date(j.timestamp).getTime()) / DAY_MS)
|
|
119
|
+
: Math.floor((nowTs - u.lastUseTs) / DAY_MS);
|
|
120
|
+
const metrics = {
|
|
121
|
+
usage7d: u.last7d,
|
|
122
|
+
usage30d: u.last30d,
|
|
123
|
+
daysSinceLastUse,
|
|
124
|
+
totalUsage: u.total,
|
|
125
|
+
};
|
|
126
|
+
// rising: 7 天频率 > 30 天均值的 1.5 倍, 且至少用过 2 次 (避免噪声)
|
|
127
|
+
if (u.last30d > 0 && u.last7d >= 2) {
|
|
128
|
+
const dailyAvg30 = u.last30d / 30;
|
|
129
|
+
const dailyRate7 = u.last7d / 7;
|
|
130
|
+
if (dailyRate7 > dailyAvg30 * 1.5) {
|
|
131
|
+
suggestions.push({
|
|
132
|
+
key: `rising:${id}`,
|
|
133
|
+
kind: 'rising',
|
|
134
|
+
judgmentId: id,
|
|
135
|
+
decision: j.decision,
|
|
136
|
+
reason: `7 天使用率 (${dailyRate7.toFixed(2)}/d) 是 30 天均值的 ${(dailyRate7 / dailyAvg30).toFixed(1)} 倍`,
|
|
137
|
+
action: 'boost',
|
|
138
|
+
hint: suggestionHint('rising', 'boost', metrics),
|
|
139
|
+
metrics,
|
|
140
|
+
scannedAt: now.toISOString(),
|
|
141
|
+
});
|
|
142
|
+
continue; // 不再归入 unused/stale
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// stale: 90 天未使用 + 总使用 < 3 (低频 + 长期不用)
|
|
146
|
+
if (u.total < 3 && daysSinceLastUse >= 90) {
|
|
147
|
+
suggestions.push({
|
|
148
|
+
key: `stale:${id}`,
|
|
149
|
+
kind: 'stale',
|
|
150
|
+
judgmentId: id,
|
|
151
|
+
decision: j.decision,
|
|
152
|
+
reason: `已 ${daysSinceLastUse} 天未使用, 总使用仅 ${u.total} 次`,
|
|
153
|
+
action: 'deprecate',
|
|
154
|
+
hint: suggestionHint('stale', 'deprecate', metrics),
|
|
155
|
+
metrics,
|
|
156
|
+
scannedAt: now.toISOString(),
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// unused: 30 天未使用 + 总使用 < 5 (中频但近期没在用)
|
|
161
|
+
if (u.total < 5 && daysSinceLastUse >= 30) {
|
|
162
|
+
suggestions.push({
|
|
163
|
+
key: `unused:${id}`,
|
|
164
|
+
kind: 'unused',
|
|
165
|
+
judgmentId: id,
|
|
166
|
+
decision: j.decision,
|
|
167
|
+
reason: `已 ${daysSinceLastUse} 天未使用, 总使用 ${u.total} 次 (可能不再相关)`,
|
|
168
|
+
action: 'review',
|
|
169
|
+
hint: suggestionHint('unused', 'review', metrics),
|
|
170
|
+
metrics,
|
|
171
|
+
scannedAt: now.toISOString(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 按 action 重要性排序: rising > stale > unused
|
|
176
|
+
const order = { rising: 0, stale: 1, unused: 2, causal_conflict: 3, low_causal_power: 4 };
|
|
177
|
+
// 阶段 2: causal_conflict — judgment 库内自动检测冲突对
|
|
178
|
+
const { runConflictDetection } = await import('./causal-judge.js');
|
|
179
|
+
const conflictResult = await runConflictDetection();
|
|
180
|
+
for (const a of judgments) {
|
|
181
|
+
if ((a.status ?? 'active') !== 'active')
|
|
182
|
+
continue;
|
|
183
|
+
if (!Array.isArray(a.conflictWith) || a.conflictWith.length === 0)
|
|
184
|
+
continue;
|
|
185
|
+
// 列出每对冲突 (limit 每个 judgment 最多 3 对, 避免 UI 刷屏)
|
|
186
|
+
const conflicts = a.conflictWith.slice(0, 3);
|
|
187
|
+
for (const otherId of conflicts) {
|
|
188
|
+
const other = judgments.find((j) => j.id === otherId);
|
|
189
|
+
if (!other)
|
|
190
|
+
continue;
|
|
191
|
+
const c = await import('./causal-judge.js');
|
|
192
|
+
const det = c.detectConflict(a, other);
|
|
193
|
+
suggestions.push({
|
|
194
|
+
key: `causal_conflict:${a.id}:${other.id}`,
|
|
195
|
+
kind: 'causal_conflict',
|
|
196
|
+
judgmentId: a.id,
|
|
197
|
+
decision: `${a.decision} ↔ ${other.decision}`,
|
|
198
|
+
reason: det.isConflict ? det.reason : '库内已标冲突, 需 LLM 复核',
|
|
199
|
+
action: 'review',
|
|
200
|
+
hint: suggestionHint('causal_conflict', 'review', { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 }),
|
|
201
|
+
metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
|
|
202
|
+
scannedAt: now.toISOString(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// 静默 conflictResult.detected 计数 (已通过 import 触发了)
|
|
207
|
+
void conflictResult;
|
|
208
|
+
// 阶段 2: low_causal_power — 留空 (依赖 do-calculus 跑过后的低边际贡献记录)
|
|
209
|
+
// 当前不主动跑 (cost 高), 仅在 UI 手动触发后, 类 B 下次扫描时捡起来
|
|
210
|
+
// 此处不实现, 留作下个迭代
|
|
211
|
+
suggestions.sort((a, b) => order[a.kind] - order[b.kind]);
|
|
212
|
+
return {
|
|
213
|
+
scannedAt: now.toISOString(),
|
|
214
|
+
judgmentsTotal: judgments.length,
|
|
215
|
+
usageEntriesScanned: entries.length,
|
|
216
|
+
suggestions,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export async function logEvolution(entry) {
|
|
220
|
+
try {
|
|
221
|
+
// P-Action 3: 摄入点扫描 (只跳 prompt-injection, 不扫 PII — judgment 决策文本含人话是合法的)
|
|
222
|
+
const { scanInput, writeScanAudit, shouldHardBlock } = await import('../security/input-scanner.js');
|
|
223
|
+
const fieldsToScan = [
|
|
224
|
+
entry.suggestion?.decision ?? '',
|
|
225
|
+
entry.suggestion?.reason ?? '',
|
|
226
|
+
].join('\n');
|
|
227
|
+
const result = scanInput(fieldsToScan, { source: 'judgment', scanPii: false });
|
|
228
|
+
if (shouldHardBlock(result)) {
|
|
229
|
+
console.warn(`[adaptive-scan] logEvolution 阻断恶意 entry: verdict=${result.verdict}, threats=${result.threats.length}`);
|
|
230
|
+
writeScanAudit(result, { evolutionKey: entry.suggestion?.key, blocked: true }).catch(() => { });
|
|
231
|
+
return; // 不写磁盘
|
|
232
|
+
}
|
|
233
|
+
if (result.verdict !== 'pass') {
|
|
234
|
+
writeScanAudit(result, { evolutionKey: entry.suggestion?.key, blocked: false }).catch(() => { });
|
|
235
|
+
}
|
|
236
|
+
await fs.mkdir(path.dirname(getEvolutionLogPath()), { recursive: true });
|
|
237
|
+
await fs.appendFile(getEvolutionLogPath(), JSON.stringify(entry) + '\n', 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
console.warn('[adaptive-scan] logEvolution failed:', err);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function readEvolutionLog(limit = 50) {
|
|
244
|
+
try {
|
|
245
|
+
const content = await fs.readFile(getEvolutionLogPath(), 'utf-8');
|
|
246
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
247
|
+
return lines
|
|
248
|
+
.slice(-limit)
|
|
249
|
+
.reverse()
|
|
250
|
+
.map((l) => {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(l);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
.filter((e) => Boolean(e));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// ============================================================
|
|
265
|
+
// 缓存: 避免每次打开 modal 都扫一次
|
|
266
|
+
// ============================================================
|
|
267
|
+
let lastScan = null;
|
|
268
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
269
|
+
export async function getCachedScan(force = false) {
|
|
270
|
+
if (!force && lastScan && Date.now() - lastScan.at < CACHE_TTL_MS) {
|
|
271
|
+
return lastScan.result;
|
|
272
|
+
}
|
|
273
|
+
const result = await runAdaptiveScan();
|
|
274
|
+
lastScan = { at: Date.now(), result };
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
export function clearAdaptiveScanCache() {
|
|
278
|
+
lastScan = null;
|
|
279
|
+
}
|