principles-disciple 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +73 -0
- package/dist/commands/evolver.d.ts +9 -0
- package/dist/commands/evolver.js +26 -0
- package/dist/commands/pain.d.ts +5 -0
- package/dist/commands/pain.js +114 -0
- package/dist/commands/strategy.d.ts +3 -0
- package/dist/commands/strategy.js +29 -0
- package/dist/commands/thinking-os.d.ts +2 -0
- package/dist/commands/thinking-os.js +162 -0
- package/dist/commands/trust.d.ts +4 -0
- package/dist/commands/trust.js +95 -0
- package/dist/core/agent-loader.d.ts +44 -0
- package/dist/core/agent-loader.js +147 -0
- package/dist/core/config-service.d.ts +15 -0
- package/dist/core/config-service.js +26 -0
- package/dist/core/config.d.ts +103 -0
- package/dist/core/config.js +186 -0
- package/dist/core/detection-funnel.d.ts +33 -0
- package/dist/core/detection-funnel.js +100 -0
- package/dist/core/detection-service.d.ts +15 -0
- package/dist/core/detection-service.js +28 -0
- package/dist/core/dictionary-service.d.ts +15 -0
- package/dist/core/dictionary-service.js +26 -0
- package/dist/core/dictionary.d.ts +36 -0
- package/dist/core/dictionary.js +136 -0
- package/dist/core/event-log.d.ts +53 -0
- package/dist/core/event-log.js +196 -0
- package/dist/core/evolution-engine.d.ts +119 -0
- package/dist/core/evolution-engine.js +542 -0
- package/dist/core/evolution-types.d.ts +126 -0
- package/dist/core/evolution-types.js +56 -0
- package/dist/core/hygiene/tracker.d.ts +22 -0
- package/dist/core/hygiene/tracker.js +106 -0
- package/dist/core/init.d.ts +12 -0
- package/dist/core/init.js +117 -0
- package/dist/core/migration.d.ts +6 -0
- package/dist/core/migration.js +90 -0
- package/dist/core/pain.d.ts +4 -0
- package/dist/core/pain.js +70 -0
- package/dist/core/path-resolver.d.ts +43 -0
- package/dist/core/path-resolver.js +259 -0
- package/dist/core/paths.d.ts +60 -0
- package/dist/core/paths.js +67 -0
- package/dist/core/profile.d.ts +62 -0
- package/dist/core/profile.js +210 -0
- package/dist/core/risk-calculator.d.ts +7 -0
- package/dist/core/risk-calculator.js +39 -0
- package/dist/core/session-tracker.d.ts +76 -0
- package/dist/core/session-tracker.js +286 -0
- package/dist/core/system-logger.d.ts +8 -0
- package/dist/core/system-logger.js +31 -0
- package/dist/core/trust-engine.d.ts +91 -0
- package/dist/core/trust-engine.js +284 -0
- package/dist/core/workspace-context.d.ts +64 -0
- package/dist/core/workspace-context.js +134 -0
- package/dist/hooks/gate.d.ts +6 -0
- package/dist/hooks/gate.js +487 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/lifecycle.js +180 -0
- package/dist/hooks/llm.d.ts +4 -0
- package/dist/hooks/llm.js +153 -0
- package/dist/hooks/pain.d.ts +5 -0
- package/dist/hooks/pain.js +173 -0
- package/dist/hooks/prompt.d.ts +38 -0
- package/dist/hooks/prompt.js +285 -0
- package/dist/hooks/subagent.d.ts +2 -0
- package/dist/hooks/subagent.js +70 -0
- package/dist/i18n/commands.d.ts +26 -0
- package/dist/i18n/commands.js +88 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +204 -0
- package/dist/service/evolution-worker.d.ts +17 -0
- package/dist/service/evolution-worker.js +293 -0
- package/dist/tools/agent-spawn.d.ts +33 -0
- package/dist/tools/agent-spawn.js +170 -0
- package/dist/tools/critique-prompt.d.ts +14 -0
- package/dist/tools/critique-prompt.js +81 -0
- package/dist/tools/deep-reflect.d.ts +19 -0
- package/dist/tools/deep-reflect.js +174 -0
- package/dist/tools/model-index.d.ts +9 -0
- package/dist/tools/model-index.js +82 -0
- package/dist/types/event-types.d.ts +229 -0
- package/dist/types/event-types.js +73 -0
- package/dist/types/hygiene-types.d.ts +20 -0
- package/dist/types/hygiene-types.js +12 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/file-lock.d.ts +64 -0
- package/dist/utils/file-lock.js +270 -0
- package/dist/utils/glob-match.d.ts +28 -0
- package/dist/utils/glob-match.js +49 -0
- package/dist/utils/hashing.d.ts +9 -0
- package/dist/utils/hashing.js +25 -0
- package/dist/utils/io.d.ts +6 -0
- package/dist/utils/io.js +106 -0
- package/dist/utils/nlp.d.ts +9 -0
- package/dist/utils/nlp.js +59 -0
- package/dist/utils/plugin-logger.d.ts +39 -0
- package/dist/utils/plugin-logger.js +70 -0
- package/openclaw.plugin.json +46 -0
- package/package.json +63 -0
- package/templates/langs/en/core/AGENTS.md +206 -0
- package/templates/langs/en/core/BOOT.md +60 -0
- package/templates/langs/en/core/BOOTSTRAP.md +250 -0
- package/templates/langs/en/core/HEARTBEAT.md +74 -0
- package/templates/langs/en/core/IDENTITY.md +8 -0
- package/templates/langs/en/core/PRINCIPLES.md +10 -0
- package/templates/langs/en/core/SOUL.md +76 -0
- package/templates/langs/en/core/TOOLS.md +53 -0
- package/templates/langs/en/core/USER.md +10 -0
- package/templates/langs/en/pain/00_seed_samples.md +23 -0
- package/templates/langs/en/pain_dictionary.json +22 -0
- package/templates/langs/en/skills/admin/SKILL.md +40 -0
- package/templates/langs/en/skills/bootstrap-tools/SKILL.md +53 -0
- package/templates/langs/en/skills/deductive-audit/SKILL.md +36 -0
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +31 -0
- package/templates/langs/en/skills/evolve-system/SKILL.md +46 -0
- package/templates/langs/en/skills/evolve-task/SKILL.md +83 -0
- package/templates/langs/en/skills/feedback/SKILL.md +51 -0
- package/templates/langs/en/skills/init-strategy/SKILL.md +54 -0
- package/templates/langs/en/skills/inject-rule/SKILL.md +19 -0
- package/templates/langs/en/skills/manage-okr/SKILL.md +96 -0
- package/templates/langs/en/skills/pain/SKILL.md +19 -0
- package/templates/langs/en/skills/pd-daily/SKILL.md +199 -0
- package/templates/langs/en/skills/pd-grooming/SKILL.md +46 -0
- package/templates/langs/en/skills/pd-mentor/SKILL.md +230 -0
- package/templates/langs/en/skills/plan-script/SKILL.md +32 -0
- package/templates/langs/en/skills/profile/SKILL.md +24 -0
- package/templates/langs/en/skills/reflection/SKILL.md +40 -0
- package/templates/langs/en/skills/reflection-log/SKILL.md +37 -0
- package/templates/langs/en/skills/report/SKILL.md +13 -0
- package/templates/langs/en/skills/root-cause/SKILL.md +33 -0
- package/templates/langs/en/skills/triage/SKILL.md +29 -0
- package/templates/langs/en/skills/watch-evolution/SKILL.md +33 -0
- package/templates/langs/zh/core/AGENTS.md +207 -0
- package/templates/langs/zh/core/BOOT.md +60 -0
- package/templates/langs/zh/core/BOOTSTRAP.md +250 -0
- package/templates/langs/zh/core/HEARTBEAT.md +74 -0
- package/templates/langs/zh/core/IDENTITY.md +8 -0
- package/templates/langs/zh/core/SOUL.md +76 -0
- package/templates/langs/zh/core/TOOLS.md +53 -0
- package/templates/langs/zh/core/USER.md +10 -0
- package/templates/langs/zh/pain/00_seed_samples.md +24 -0
- package/templates/langs/zh/pain_dictionary.json +18 -0
- package/templates/langs/zh/skills/admin/SKILL.md +42 -0
- package/templates/langs/zh/skills/bootstrap-tools/SKILL.md +52 -0
- package/templates/langs/zh/skills/deductive-audit/SKILL.md +36 -0
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +31 -0
- package/templates/langs/zh/skills/evolve-system/SKILL.md +46 -0
- package/templates/langs/zh/skills/evolve-task/SKILL.md +83 -0
- package/templates/langs/zh/skills/feedback/SKILL.md +53 -0
- package/templates/langs/zh/skills/init-strategy/SKILL.md +54 -0
- package/templates/langs/zh/skills/inject-rule/SKILL.md +19 -0
- package/templates/langs/zh/skills/manage-okr/SKILL.md +109 -0
- package/templates/langs/zh/skills/pain/SKILL.md +19 -0
- package/templates/langs/zh/skills/pd-daily/SKILL.md +199 -0
- package/templates/langs/zh/skills/pd-grooming/SKILL.md +46 -0
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +230 -0
- package/templates/langs/zh/skills/plan-script/SKILL.md +32 -0
- package/templates/langs/zh/skills/profile/SKILL.md +24 -0
- package/templates/langs/zh/skills/reflection/SKILL.md +40 -0
- package/templates/langs/zh/skills/reflection-log/SKILL.md +37 -0
- package/templates/langs/zh/skills/report/SKILL.md +13 -0
- package/templates/langs/zh/skills/root-cause/SKILL.md +33 -0
- package/templates/langs/zh/skills/triage/SKILL.md +29 -0
- package/templates/langs/zh/skills/watch-evolution/SKILL.md +33 -0
- package/templates/pain_dictionary.json +36 -0
- package/templates/pain_settings.json +77 -0
- package/templates/workspace/.principles/00-kernel.md +51 -0
- package/templates/workspace/.principles/DECISION_POLICY.json +44 -0
- package/templates/workspace/.principles/PRINCIPLES.md +20 -0
- package/templates/workspace/.principles/PROFILE.json +52 -0
- package/templates/workspace/.principles/PROFILE.schema.json +56 -0
- package/templates/workspace/.principles/THINKING_OS.md +64 -0
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +7 -0
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +9 -0
- package/templates/workspace/.principles/models/_INDEX.md +27 -0
- package/templates/workspace/.principles/models/first_principles.md +62 -0
- package/templates/workspace/.principles/models/marketing_4p.md +52 -0
- package/templates/workspace/.principles/models/porter_five.md +63 -0
- package/templates/workspace/.principles/models/swot.md +60 -0
- package/templates/workspace/.principles/models/user_story_map.md +63 -0
- package/templates/workspace/.state/WORKBOARD.json +4 -0
- package/templates/workspace/AUDIT.md +15 -0
- package/templates/workspace/PLAN.md +2 -0
- package/templates/workspace/okr/RECOVERY_PROTOCOL.md +56 -0
- package/templates/workspace/okr/TASK_CHANGES.jsonl +6 -0
- package/templates/workspace/okr/WEEK_TASKS.json +6 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io.js';
|
|
4
|
+
import { matchesAnyPattern } from '../utils/glob-match.js';
|
|
5
|
+
import { normalizeProfile } from '../core/profile.js';
|
|
6
|
+
import { trackBlock, hasRecentThinking } from '../core/session-tracker.js';
|
|
7
|
+
import { assessRiskLevel, estimateLineChanges } from '../core/risk-calculator.js';
|
|
8
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
|
+
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
10
|
+
export function handleBeforeToolCall(event, ctx) {
|
|
11
|
+
const logger = ctx.logger || console;
|
|
12
|
+
// 1. Identify tool type
|
|
13
|
+
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'replace', 'insert', 'patch', 'edit_file', 'delete_file', 'move_file'];
|
|
14
|
+
const BASH_TOOLS = ['bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd'];
|
|
15
|
+
const AGENT_TOOLS = ['pd_spawn_agent', 'sessions_spawn'];
|
|
16
|
+
const isBash = BASH_TOOLS.includes(event.toolName);
|
|
17
|
+
const isWriteTool = WRITE_TOOLS.includes(event.toolName);
|
|
18
|
+
const isAgentTool = AGENT_TOOLS.includes(event.toolName);
|
|
19
|
+
// Profile loaded first for config-driven behavior (see below)
|
|
20
|
+
if (!ctx.workspaceDir || (!isWriteTool && !isBash && !isAgentTool)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
24
|
+
// 2. Load Profile
|
|
25
|
+
const profilePath = wctx.resolve('PROFILE');
|
|
26
|
+
let profile = {
|
|
27
|
+
risk_paths: [],
|
|
28
|
+
gate: { require_plan_for_risk_paths: true },
|
|
29
|
+
progressive_gate: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
plan_approvals: {
|
|
32
|
+
enabled: false,
|
|
33
|
+
max_lines_override: -1,
|
|
34
|
+
allowed_patterns: [],
|
|
35
|
+
allowed_operations: [],
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
edit_verification: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
max_file_size_bytes: 10 * 1024 * 1024,
|
|
41
|
+
fuzzy_match_enabled: true,
|
|
42
|
+
fuzzy_match_threshold: 0.8,
|
|
43
|
+
skip_large_file_action: 'warn',
|
|
44
|
+
},
|
|
45
|
+
thinking_checkpoint: {
|
|
46
|
+
enabled: false, // Default OFF
|
|
47
|
+
window_ms: 5 * 60 * 1000,
|
|
48
|
+
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_spawn_agent'],
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (fs.existsSync(profilePath)) {
|
|
52
|
+
try {
|
|
53
|
+
const rawProfile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
54
|
+
profile = normalizeProfile(rawProfile);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
logger?.error?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ═══ THINKING OS CHECKPOINT (P-10) — Config-gated ═══
|
|
61
|
+
// Only enforced when thinking_checkpoint.enabled = true in PROFILE.json
|
|
62
|
+
const isHighRisk = profile.thinking_checkpoint?.high_risk_tools?.includes(event.toolName) ?? false;
|
|
63
|
+
if (profile.thinking_checkpoint?.enabled && isHighRisk && ctx.sessionId) {
|
|
64
|
+
const windowMs = profile.thinking_checkpoint.window_ms ?? 5 * 60 * 1000;
|
|
65
|
+
const hasThinking = hasRecentThinking(ctx.sessionId, windowMs);
|
|
66
|
+
if (!hasThinking) {
|
|
67
|
+
logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
|
|
68
|
+
return {
|
|
69
|
+
block: true,
|
|
70
|
+
blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs / 60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Merge pluginConfig (OpenClaw UI settings)
|
|
75
|
+
const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
|
|
76
|
+
if (configRiskPaths.length > 0) {
|
|
77
|
+
profile.risk_paths = [...new Set([...profile.risk_paths, ...configRiskPaths])];
|
|
78
|
+
}
|
|
79
|
+
// 3. Resolve the target file path
|
|
80
|
+
let filePath = event.params.file_path || event.params.path || event.params.file || event.params.target;
|
|
81
|
+
// Heuristic for bash mutation detection
|
|
82
|
+
if (isBash && !filePath) {
|
|
83
|
+
const command = String(event.params.command || event.params.args || "");
|
|
84
|
+
const mutationMatch = command.match(/(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/);
|
|
85
|
+
if (mutationMatch) {
|
|
86
|
+
filePath = mutationMatch[1];
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const hasRiskPath = profile.risk_paths.some(rp => command.includes(rp));
|
|
90
|
+
const isMutation = /(?:>|>>|sed|rm|mv|mkdir|touch|cp|npm|yarn|pnpm|pip|cargo)/.test(command);
|
|
91
|
+
if (hasRiskPath && isMutation) {
|
|
92
|
+
filePath = command;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (typeof filePath !== 'string')
|
|
100
|
+
return;
|
|
101
|
+
const relPath = normalizePath(filePath, ctx.workspaceDir);
|
|
102
|
+
const risky = (isBash && filePath.includes(' '))
|
|
103
|
+
? profile.risk_paths.some(rp => filePath.includes(rp))
|
|
104
|
+
: isRisky(relPath, profile.risk_paths);
|
|
105
|
+
// ── PROGRESSIVE GATE LOGIC ──
|
|
106
|
+
if (profile.progressive_gate?.enabled) {
|
|
107
|
+
const trustEngine = wctx.trust;
|
|
108
|
+
const trustScore = trustEngine.getScore();
|
|
109
|
+
const stage = trustEngine.getStage();
|
|
110
|
+
const trustSettings = wctx.config.get('trust') || {
|
|
111
|
+
limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
|
|
112
|
+
};
|
|
113
|
+
const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
|
|
114
|
+
const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
|
|
115
|
+
logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
|
|
116
|
+
// Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
|
|
117
|
+
if (stage === 1) {
|
|
118
|
+
if (risky || riskLevel !== 'LOW') {
|
|
119
|
+
// Check if PLAN whitelist is enabled
|
|
120
|
+
if (profile.progressive_gate?.plan_approvals?.enabled) {
|
|
121
|
+
const planApprovals = profile.progressive_gate.plan_approvals;
|
|
122
|
+
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
123
|
+
// Must have READY plan
|
|
124
|
+
if (planStatus === 'READY') {
|
|
125
|
+
// Check operation type
|
|
126
|
+
if (planApprovals.allowed_operations?.includes(event.toolName)) {
|
|
127
|
+
// Check path pattern
|
|
128
|
+
if (matchesAnyPattern(relPath, planApprovals.allowed_patterns || [])) {
|
|
129
|
+
// Check line limit (if configured)
|
|
130
|
+
const maxLines = planApprovals.max_lines_override ?? -1;
|
|
131
|
+
if (maxLines === -1 || lineChanges <= maxLines) {
|
|
132
|
+
// Record PLAN approval event
|
|
133
|
+
wctx.eventLog.recordPlanApproval(ctx.sessionId, {
|
|
134
|
+
toolName: event.toolName,
|
|
135
|
+
filePath: relPath,
|
|
136
|
+
pattern: relPath,
|
|
137
|
+
planStatus
|
|
138
|
+
});
|
|
139
|
+
logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
|
|
140
|
+
return; // Allow the operation
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Block if not approved by whitelist
|
|
147
|
+
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Stage 2 (Editor): Block writes to risk paths. Block large changes.
|
|
151
|
+
if (stage === 2) {
|
|
152
|
+
if (risky) {
|
|
153
|
+
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName);
|
|
154
|
+
}
|
|
155
|
+
const stage2Limit = trustSettings.limits?.stage_2_max_lines ?? 50;
|
|
156
|
+
if (lineChanges > stage2Limit) {
|
|
157
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
|
|
161
|
+
if (stage === 3) {
|
|
162
|
+
if (risky) {
|
|
163
|
+
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
164
|
+
if (planStatus !== 'READY') {
|
|
165
|
+
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const stage3Limit = trustSettings.limits?.stage_3_max_lines ?? 300;
|
|
169
|
+
if (lineChanges > stage3Limit) {
|
|
170
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 3. Max allowed is ${stage3Limit}.`, wctx, event.toolName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Stage 4 (Architect): Full bypass
|
|
174
|
+
if (stage === 4) {
|
|
175
|
+
logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// ── EP SIMULATION MODE (M6验证) ──
|
|
179
|
+
// 记录EP系统的模拟决策,但不生效(仅用于对比分析)
|
|
180
|
+
try {
|
|
181
|
+
const epDecision = checkEvolutionGate(ctx.workspaceDir, {
|
|
182
|
+
toolName: event.toolName,
|
|
183
|
+
content: event.params?.content,
|
|
184
|
+
lineCount: lineChanges,
|
|
185
|
+
isRiskPath: risky,
|
|
186
|
+
});
|
|
187
|
+
const epLogEntry = {
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
toolName: event.toolName,
|
|
190
|
+
filePath: relPath,
|
|
191
|
+
trustEngine: { score: trustScore, stage, decision: 'allow' },
|
|
192
|
+
epSystem: { tier: epDecision.currentTier ?? 'UNKNOWN', allowed: epDecision.allowed, reason: epDecision.reason },
|
|
193
|
+
conflict: epDecision.allowed === false, // Trust允许但EP拒绝(任何阶段)
|
|
194
|
+
};
|
|
195
|
+
const epLogPath = path.join(ctx.workspaceDir, '.state', 'ep_simulation.jsonl');
|
|
196
|
+
// 安全创建目录(如果失败则跳过日志写入,但不影响 Trust Engine 决策)
|
|
197
|
+
let canWriteEpLog = true;
|
|
198
|
+
try {
|
|
199
|
+
fs.mkdirSync(path.dirname(epLogPath), { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
catch (mkdirErr) {
|
|
202
|
+
if (!mkdirErr || mkdirErr.code !== 'EEXIST') {
|
|
203
|
+
logger.warn(`[PD_EP_SIM] Failed to create log dir: ${mkdirErr?.message ?? String(mkdirErr)}, skipping log`);
|
|
204
|
+
canWriteEpLog = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (canWriteEpLog) {
|
|
208
|
+
fs.appendFileSync(epLogPath, JSON.stringify(epLogEntry) + '\n');
|
|
209
|
+
}
|
|
210
|
+
logger.info(`[PD_EP_SIM] Tier: ${epDecision.currentTier}, Allowed: ${epDecision.allowed}, Trust: ${trustScore} (Stage ${stage})`);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
// EP 模拟失败不应该影响 Trust Engine 决策
|
|
214
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
215
|
+
logger.warn(`[PD_EP_SIM] Simulation failed: ${errMsg}, continuing with Trust Engine`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// FALLBACK: Legacy Gate Logic
|
|
220
|
+
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
221
|
+
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
222
|
+
if (planStatus !== 'READY') {
|
|
223
|
+
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════
|
|
228
|
+
// P-03: Edit Tool Force Verification
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════
|
|
230
|
+
// After all gate checks, verify edit operations (enforces P-03)
|
|
231
|
+
if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
|
|
232
|
+
const verifyResult = handleEditVerification(event, wctx, ctx, {
|
|
233
|
+
enabled: profile.edit_verification.enabled,
|
|
234
|
+
max_file_size_bytes: profile.edit_verification.max_file_size_bytes,
|
|
235
|
+
fuzzy_match_enabled: profile.edit_verification.fuzzy_match_enabled,
|
|
236
|
+
fuzzy_match_threshold: profile.edit_verification.fuzzy_match_threshold,
|
|
237
|
+
skip_large_file_action: profile.edit_verification.skip_large_file_action,
|
|
238
|
+
});
|
|
239
|
+
if (verifyResult) {
|
|
240
|
+
return verifyResult; // Block or modify params
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function block(filePath, reason, wctx, toolName) {
|
|
245
|
+
const logger = console;
|
|
246
|
+
logger.error(`[PD_GATE] BLOCKED: ${filePath}. Reason: ${reason}`);
|
|
247
|
+
trackBlock(wctx.workspaceDir);
|
|
248
|
+
return {
|
|
249
|
+
block: true,
|
|
250
|
+
blockReason: `[Principles Disciple] Security Gate Blocked this action.\nFile: ${filePath}\nReason: ${reason}\n\nHint: You may need a READY plan or a higher trust score to perform this action.`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════
|
|
254
|
+
// P-03: Edit Tool Force Verification
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════
|
|
256
|
+
/**
|
|
257
|
+
* Normalize a line for fuzzy matching by collapsing whitespace
|
|
258
|
+
*/
|
|
259
|
+
function normalizeLine(line) {
|
|
260
|
+
return line.replace(/\s+/g, ' ').trim();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Find fuzzy match between oldText and current file content
|
|
264
|
+
* @param lines - File content split into lines
|
|
265
|
+
* @param oldLines - oldText split into lines
|
|
266
|
+
* @param threshold - Match threshold (0-1)
|
|
267
|
+
* @returns Match index or -1 if not found
|
|
268
|
+
*/
|
|
269
|
+
function findFuzzyMatch(lines, oldLines, threshold = 0.8) {
|
|
270
|
+
if (oldLines.length === 0)
|
|
271
|
+
return -1; // P2 fix: empty array boundary check
|
|
272
|
+
const normalizedLines = lines.map(normalizeLine);
|
|
273
|
+
const normalizedOldLines = oldLines.map(normalizeLine);
|
|
274
|
+
// Try to find matching sequence
|
|
275
|
+
for (let i = 0; i <= lines.length - oldLines.length; i++) {
|
|
276
|
+
let matchCount = 0;
|
|
277
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
278
|
+
if (normalizedLines[i + j] === normalizedOldLines[j]) {
|
|
279
|
+
matchCount++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Use threshold from config
|
|
283
|
+
if (matchCount >= oldLines.length * threshold) {
|
|
284
|
+
return i;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return -1;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Try to find a fuzzy match for oldText in the current content
|
|
291
|
+
* @param currentContent - Current file content
|
|
292
|
+
* @param oldText - Text to match
|
|
293
|
+
* @param threshold - Match threshold (0-1)
|
|
294
|
+
* @returns Object with found status and corrected text if found
|
|
295
|
+
*/
|
|
296
|
+
function tryFuzzyMatch(currentContent, oldText, threshold = 0.8) {
|
|
297
|
+
const lines = currentContent.split('\n');
|
|
298
|
+
const oldLines = oldText.split('\n');
|
|
299
|
+
const matchIndex = findFuzzyMatch(lines, oldLines, threshold);
|
|
300
|
+
if (matchIndex !== -1) {
|
|
301
|
+
// Found fuzzy match, extract actual text from file
|
|
302
|
+
const correctedText = lines.slice(matchIndex, matchIndex + oldLines.length).join('\n');
|
|
303
|
+
return { found: true, correctedText };
|
|
304
|
+
}
|
|
305
|
+
return { found: false };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Generate a helpful error message for edit verification failure
|
|
309
|
+
*/
|
|
310
|
+
function generateEditError(filePath, oldText, currentContent) {
|
|
311
|
+
const expectedSnippet = oldText.split('\n').slice(0, 3).join('\n').substring(0, 200);
|
|
312
|
+
const actualSnippet = currentContent.substring(0, 200);
|
|
313
|
+
return `[P-03 Violation] Edit verification failed
|
|
314
|
+
|
|
315
|
+
File: ${filePath}
|
|
316
|
+
|
|
317
|
+
The text you're trying to replace does not match the current file content.
|
|
318
|
+
|
|
319
|
+
Expected to find:
|
|
320
|
+
${expectedSnippet}${oldText.length > 200 ? '...' : ''}
|
|
321
|
+
|
|
322
|
+
Actual file contains:
|
|
323
|
+
${actualSnippet}${currentContent.length > 200 ? '...' : ''}
|
|
324
|
+
|
|
325
|
+
Possible reasons:
|
|
326
|
+
- File has been modified by another process
|
|
327
|
+
- Whitespace characters do not match (spaces, tabs, newlines)
|
|
328
|
+
- Context compression caused outdated information
|
|
329
|
+
|
|
330
|
+
Solution:
|
|
331
|
+
1. Use the 'read' tool to get the current file content
|
|
332
|
+
2. Update your edit command with the exact text from the file
|
|
333
|
+
3. Retry the edit operation
|
|
334
|
+
|
|
335
|
+
This is enforced by P-03 (精确匹配前验证原则).`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle edit tool verification before allowing the operation
|
|
339
|
+
* This enforces P-03 at the tool layer
|
|
340
|
+
*/
|
|
341
|
+
function handleEditVerification(event, wctx, ctx, config = {}) {
|
|
342
|
+
const logger = ctx.logger || console;
|
|
343
|
+
const maxSizeBytes = config.max_file_size_bytes ?? 10 * 1024 * 1024; // Default 10MB
|
|
344
|
+
const fuzzyMatchEnabled = config.fuzzy_match_enabled !== false;
|
|
345
|
+
const fuzzyMatchThreshold = config.fuzzy_match_threshold ?? 0.8;
|
|
346
|
+
const skipAction = config.skip_large_file_action ?? 'warn';
|
|
347
|
+
// 1. Extract parameters (handle both parameter naming conventions)
|
|
348
|
+
const filePath = event.params.file_path || event.params.path || event.params.file;
|
|
349
|
+
const oldText = event.params.oldText || event.params.old_string;
|
|
350
|
+
if (!filePath || !oldText) {
|
|
351
|
+
// Missing required parameters, let it fail naturally
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// 2. Resolve and read file
|
|
355
|
+
let absolutePath;
|
|
356
|
+
try {
|
|
357
|
+
absolutePath = wctx.resolve(filePath);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
// Path resolution error, let it fail naturally
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// 2.5. Skip verification for binary files
|
|
364
|
+
const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg',
|
|
365
|
+
'.pdf', '.zip', '.tar', '.gz', '.7z', '.rar',
|
|
366
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
367
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wav',
|
|
368
|
+
'.ttf', '.otf', '.woff', '.woff2',
|
|
369
|
+
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
|
|
370
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
371
|
+
if (BINARY_EXTENSIONS.includes(ext)) {
|
|
372
|
+
logger?.info?.(`[PD_GATE:EDIT_VERIFY] Skipping verification for binary file: ${path.basename(filePath)}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
// 2.6. Check file size before reading (P-03 improvement)
|
|
377
|
+
try {
|
|
378
|
+
const stats = fs.statSync(absolutePath);
|
|
379
|
+
const fileSizeBytes = stats.size;
|
|
380
|
+
const fileSizeMB = fileSizeBytes / (1024 * 1024);
|
|
381
|
+
if (fileSizeBytes > maxSizeBytes) {
|
|
382
|
+
const message = `[PD_GATE:EDIT_VERIFY] File size check: ${path.basename(filePath)} is ${fileSizeMB.toFixed(2)}MB (threshold: ${(maxSizeBytes / (1024 * 1024)).toFixed(2)}MB)`;
|
|
383
|
+
if (skipAction === 'block') {
|
|
384
|
+
logger?.warn?.(message + ' - BLOCKED');
|
|
385
|
+
return {
|
|
386
|
+
block: true,
|
|
387
|
+
blockReason: `${message}\n\nFile is too large for edit verification. Increase max_file_size_bytes in PROFILE.json or reduce file size.`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
logger?.warn?.(message + ' - SKIPPING verification');
|
|
392
|
+
return; // Skip verification but allow operation
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
logger?.info?.(`[PD_GATE:EDIT_VERIFY] File size check passed: ${path.basename(filePath)} (${fileSizeMB.toFixed(2)}MB)`);
|
|
396
|
+
}
|
|
397
|
+
catch (statError) {
|
|
398
|
+
// File stat error (e.g., permission denied)
|
|
399
|
+
const errStr = statError instanceof Error ? statError.message : String(statError);
|
|
400
|
+
const errCode = statError.code;
|
|
401
|
+
if (errCode === 'EACCES' || errCode === 'EPERM') {
|
|
402
|
+
logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied accessing file: ${path.basename(filePath)} (${errStr})`);
|
|
403
|
+
return {
|
|
404
|
+
block: true,
|
|
405
|
+
blockReason: `[P-03 Error] Permission denied: Cannot access file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
else if (errCode === 'ENOENT') {
|
|
409
|
+
logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
|
|
410
|
+
// File doesn't exist - let the edit operation proceed (it will create the file)
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Stat error: ${errStr}`);
|
|
415
|
+
// Let it fail naturally on read attempt
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// 3. Read current file content with improved error handling
|
|
419
|
+
let currentContent;
|
|
420
|
+
try {
|
|
421
|
+
currentContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
422
|
+
}
|
|
423
|
+
catch (readError) {
|
|
424
|
+
const errStr = readError instanceof Error ? readError.message : String(readError);
|
|
425
|
+
const errCode = readError.code;
|
|
426
|
+
if (errCode === 'EACCES' || errCode === 'EPERM') {
|
|
427
|
+
logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied reading file: ${path.basename(filePath)} (${errStr})`);
|
|
428
|
+
return {
|
|
429
|
+
block: true,
|
|
430
|
+
blockReason: `[P-03 Error] Permission denied: Cannot read file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
else if (errCode === 'ENOENT') {
|
|
434
|
+
logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
|
|
435
|
+
// File doesn't exist - let the edit operation proceed
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
else if (errStr.includes('UTF-8') || errStr.includes('encoding')) {
|
|
439
|
+
logger?.error?.(`[PD_GATE:EDIT_VERIFY] Encoding error reading file: ${path.basename(filePath)} (${errStr})`);
|
|
440
|
+
return {
|
|
441
|
+
block: true,
|
|
442
|
+
blockReason: `[P-03 Error] Encoding error: Cannot read file ${absolutePath}\n\nError: ${errStr}\n\nThe file appears to use an encoding other than UTF-8. Edit verification requires UTF-8 readable text files.\n\nSolution: Ensure the file is UTF-8 encoded text, or mark binary extensions to skip verification.`
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Read error: ${errStr}`);
|
|
447
|
+
// Let it fail naturally
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 4. Verify oldText exists in current content
|
|
452
|
+
if (!currentContent.includes(oldText)) {
|
|
453
|
+
logger?.info?.(`[PD_GATE:EDIT_VERIFY] Exact match failed for ${path.basename(filePath)}, trying fuzzy match`);
|
|
454
|
+
// 5. Try fuzzy matching (if enabled)
|
|
455
|
+
if (fuzzyMatchEnabled) {
|
|
456
|
+
const fuzzyResult = tryFuzzyMatch(currentContent, oldText, fuzzyMatchThreshold);
|
|
457
|
+
if (fuzzyResult.found && fuzzyResult.correctedText) {
|
|
458
|
+
logger?.info?.(`[PD_GATE:EDIT_VERIFY] Fuzzy match found for ${path.basename(filePath)}, auto-correcting oldText`);
|
|
459
|
+
// Return corrected parameters
|
|
460
|
+
return {
|
|
461
|
+
params: {
|
|
462
|
+
...event.params,
|
|
463
|
+
oldText: fuzzyResult.correctedText,
|
|
464
|
+
old_string: fuzzyResult.correctedText
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// 6. No match found, block the operation with helpful error
|
|
470
|
+
const errorMsg = generateEditError(absolutePath, oldText, currentContent);
|
|
471
|
+
logger?.error?.(`[PD_GATE:EDIT_VERIFY] Block edit on ${path.basename(filePath)}: oldText not found`);
|
|
472
|
+
return {
|
|
473
|
+
block: true,
|
|
474
|
+
blockReason: errorMsg
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// 7. Verification passed, allow edit to proceed
|
|
478
|
+
logger?.info?.(`[PD_GATE:EDIT_VERIFY] Verified edit on ${path.basename(filePath)}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
// Unexpected error - let it fail naturally
|
|
483
|
+
const errorStr = error instanceof Error ? error.message : String(error);
|
|
484
|
+
logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Unexpected error: ${errorStr}`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginHookBeforeResetEvent, PluginHookBeforeCompactionEvent, PluginHookAfterCompactionEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
|
|
2
|
+
export declare function handleBeforeReset(event: PluginHookBeforeResetEvent, ctx: PluginHookAgentContext): Promise<void>;
|
|
3
|
+
export declare function extractPainFromSessionFile(sessionFile: string, ctx: PluginHookAgentContext): Promise<void>;
|
|
4
|
+
export declare function handleBeforeCompaction(event: PluginHookBeforeCompactionEvent, ctx: PluginHookAgentContext): Promise<void>;
|
|
5
|
+
export declare function handleAfterCompaction(event: PluginHookAfterCompactionEvent, ctx: PluginHookAgentContext): Promise<void>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import { writePainFlag } from '../core/pain.js';
|
|
5
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
6
|
+
import { PD_DIRS } from '../core/paths.js';
|
|
7
|
+
export async function handleBeforeReset(event, ctx) {
|
|
8
|
+
if (!ctx.workspaceDir || !event.messages || event.messages.length === 0) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
12
|
+
// Auto-summarise pain points before the session is cleared
|
|
13
|
+
const painPoints = event.messages.filter((msg) => {
|
|
14
|
+
const m = msg;
|
|
15
|
+
return (m.role === 'assistant' &&
|
|
16
|
+
typeof m.content === 'string' &&
|
|
17
|
+
(m.content.includes('error') || m.content.includes('fail') || m.content.includes('blocked')));
|
|
18
|
+
});
|
|
19
|
+
if (painPoints.length > 0) {
|
|
20
|
+
const memoryPath = wctx.resolve('MEMORY_MD');
|
|
21
|
+
const summary = `\n## [${new Date().toISOString()}] Session Reset Summary (Reason: ${event.reason ?? 'Manual'})\n` +
|
|
22
|
+
`- Encountered ${painPoints.length} potential pain point(s) during this session.\n` +
|
|
23
|
+
`- Action: Consider running /reflection to solidify these into principles.\n`;
|
|
24
|
+
try {
|
|
25
|
+
fs.appendFileSync(memoryPath, summary, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch (_e) {
|
|
28
|
+
console.error(`[PD:Lifecycle] Failed to write session reset summary: ${String(_e)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function extractPainFromSessionFile(sessionFile, ctx) {
|
|
33
|
+
const painPoints = [];
|
|
34
|
+
const workspaceDir = ctx.workspaceDir;
|
|
35
|
+
if (!workspaceDir)
|
|
36
|
+
return;
|
|
37
|
+
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
38
|
+
if (!fs.existsSync(sessionFile)) {
|
|
39
|
+
if (ctx.logger?.debug)
|
|
40
|
+
ctx.logger.debug(`[Pain Extractor] Session file not found: ${sessionFile}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (ctx.logger)
|
|
44
|
+
ctx.logger.info(`[Pain Extractor] Scanning session transcript for pain signals: ${sessionFile}`);
|
|
45
|
+
const fileStream = fs.createReadStream(sessionFile);
|
|
46
|
+
const rl = readline.createInterface({
|
|
47
|
+
input: fileStream,
|
|
48
|
+
crlfDelay: Infinity
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
for await (const line of rl) {
|
|
52
|
+
try {
|
|
53
|
+
if (!line.trim())
|
|
54
|
+
continue;
|
|
55
|
+
const msg = JSON.parse(line);
|
|
56
|
+
if (msg.role !== 'assistant')
|
|
57
|
+
continue;
|
|
58
|
+
let text = '';
|
|
59
|
+
if (typeof msg.content === 'string') {
|
|
60
|
+
text = msg.content;
|
|
61
|
+
}
|
|
62
|
+
else if (Array.isArray(msg.content)) {
|
|
63
|
+
text = msg.content
|
|
64
|
+
.filter(c => c && c.type === 'text' && typeof c.text === 'string')
|
|
65
|
+
.map(c => c.text)
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
else if (msg.usage && msg.usage.outputText) {
|
|
69
|
+
text = msg.usage.outputText;
|
|
70
|
+
}
|
|
71
|
+
if (!text)
|
|
72
|
+
continue;
|
|
73
|
+
if (msg.openclawAbort?.aborted) {
|
|
74
|
+
const runIdSafe = msg.openclawAbort?.runId || 'unknown';
|
|
75
|
+
if (ctx.logger)
|
|
76
|
+
ctx.logger.info(`[Pain Extractor] Detected hard-abort snapshot (runId: ${runIdSafe})`);
|
|
77
|
+
painPoints.push(`[FATAL INTERCEPT] 动作被沙箱防御机制强制击落。大模型被击落前的思考流 (未遂动机): ${text.substring(0, 250)}...`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (msg.__openclaw?.truncated && msg.__openclaw?.reason === 'oversized') {
|
|
81
|
+
if (ctx.logger)
|
|
82
|
+
ctx.logger.info(`[Pain Extractor] Detected oversized data truncation placeholder`);
|
|
83
|
+
painPoints.push(`[COGNITIVE OVERLOAD] 大模型尝试读取极大体积的输入,已被底层守护程序抹除/折叠防爆。请反思是否读取了不当的文件或日志: ${text.substring(0, 150)}...`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const lower = text.toLowerCase();
|
|
87
|
+
if (lower.includes('i\'m sorry, but i\'m still getting') ||
|
|
88
|
+
lower.includes('i apologize for the confusion') ||
|
|
89
|
+
lower.includes('this is taking longer than expected')) {
|
|
90
|
+
if (ctx.logger?.debug)
|
|
91
|
+
ctx.logger.debug(`[Pain Extractor] Detected semantic confusion string.`);
|
|
92
|
+
painPoints.push(`[SEMANTIC CONFUSION] ${text.substring(0, 150)}...`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.error(`[PD:Lifecycle] Error parsing message: ${String(e)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
try {
|
|
102
|
+
rl.close();
|
|
103
|
+
fileStream.destroy();
|
|
104
|
+
}
|
|
105
|
+
catch (_e) {
|
|
106
|
+
// Ignore cleanup errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (painPoints.length > 0) {
|
|
110
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
111
|
+
const dailyLogPath = path.join(workspaceDir, PD_DIRS.MEMORY, `${dateStr}.md`);
|
|
112
|
+
const timestamp = new Date().toISOString();
|
|
113
|
+
let entry = `\n## [${timestamp}] Consolidated Pain (Pre-Compaction)\n\n`;
|
|
114
|
+
entry += `### Pain Signals extracted from session transcript\n`;
|
|
115
|
+
painPoints.slice(-5).forEach((p, idx) => {
|
|
116
|
+
entry += `- [Signal ${idx + 1}] ${p.replace(/\n/g, ' ')}\n`;
|
|
117
|
+
});
|
|
118
|
+
entry += `\n### Diagnosis (Pending)\n- Run /evolve-task to diagnose. Deep dive using memory_search if needed.\n`;
|
|
119
|
+
try {
|
|
120
|
+
const dir = path.dirname(dailyLogPath);
|
|
121
|
+
if (!fs.existsSync(dir))
|
|
122
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
123
|
+
fs.appendFileSync(dailyLogPath, entry, 'utf8');
|
|
124
|
+
const semanticPath = wctx.resolve('SEMANTIC_PAIN');
|
|
125
|
+
const semanticDir = path.dirname(semanticPath);
|
|
126
|
+
if (!fs.existsSync(semanticDir))
|
|
127
|
+
fs.mkdirSync(semanticDir, { recursive: true });
|
|
128
|
+
let semanticEntry = `\n### Sample ${timestamp}\n- Source: compaction\n\n\`\`\`\n${painPoints.join('\n---\n')}\n\`\`\`\n`;
|
|
129
|
+
fs.appendFileSync(semanticPath, semanticEntry, 'utf8');
|
|
130
|
+
const hasFatal = painPoints.some(p => p.includes('[FATAL INTERCEPT]'));
|
|
131
|
+
if (hasFatal) {
|
|
132
|
+
writePainFlag(workspaceDir, {
|
|
133
|
+
source: 'intercept_extraction',
|
|
134
|
+
score: '100',
|
|
135
|
+
time: new Date().toISOString(),
|
|
136
|
+
reason: 'Hard intercept detected in session history compaction.',
|
|
137
|
+
is_risky: 'true',
|
|
138
|
+
trigger_text_preview: painPoints.find(p => p.includes('[FATAL INTERCEPT]'))?.substring(0, 150) || 'Fatal intercept'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`[PD:Lifecycle] Failed to write pain signals: ${String(err)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function handleBeforeCompaction(event, ctx) {
|
|
148
|
+
if (!ctx.workspaceDir)
|
|
149
|
+
return;
|
|
150
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
151
|
+
const checkpointPath = path.join(ctx.workspaceDir, PD_DIRS.MEMORY, `${dateStr}.md`);
|
|
152
|
+
const log = `\n## [${new Date().toISOString()}] Pre-Compaction Checkpoint\n` +
|
|
153
|
+
`- Compacting session with ${event.messageCount} messages.\n` +
|
|
154
|
+
`- Ensuring critical state is flushed to disk.\n`;
|
|
155
|
+
try {
|
|
156
|
+
const dir = path.dirname(checkpointPath);
|
|
157
|
+
if (!fs.existsSync(dir))
|
|
158
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
159
|
+
fs.appendFileSync(checkpointPath, log, 'utf8');
|
|
160
|
+
}
|
|
161
|
+
catch (_e) {
|
|
162
|
+
console.error(`[PD:Lifecycle] Failed to write pre-compaction checkpoint: ${String(_e)}`);
|
|
163
|
+
}
|
|
164
|
+
if (event.sessionFile) {
|
|
165
|
+
await extractPainFromSessionFile(event.sessionFile, ctx);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function handleAfterCompaction(event, ctx) {
|
|
169
|
+
if (!ctx.workspaceDir)
|
|
170
|
+
return;
|
|
171
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
172
|
+
const checkpointPath = path.join(ctx.workspaceDir, PD_DIRS.MEMORY, `${dateStr}.md`);
|
|
173
|
+
const log = `- Post-Compaction Complete. Reduced active context to ${event.messageCount} messages.\n`;
|
|
174
|
+
try {
|
|
175
|
+
fs.appendFileSync(checkpointPath, log, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch (_e) {
|
|
178
|
+
console.error(`[PD:Lifecycle] Failed to write post-compaction checkpoint: ${String(_e)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|