principles-disciple 1.7.0 → 1.7.1
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/evolution-status.js +114 -118
- package/dist/commands/rollback.js +9 -3
- package/dist/commands/trust.js +64 -81
- package/dist/core/event-log.d.ts +6 -1
- package/dist/core/event-log.js +7 -0
- package/dist/core/session-tracker.d.ts +10 -2
- package/dist/core/session-tracker.js +60 -9
- package/dist/core/trust-engine.d.ts +4 -0
- package/dist/core/trust-engine.js +20 -25
- package/dist/hooks/gate.js +68 -53
- package/dist/hooks/llm.js +5 -2
- package/dist/hooks/subagent.js +42 -27
- package/dist/hooks/trajectory-collector.d.ts +32 -0
- package/dist/hooks/trajectory-collector.js +256 -0
- package/dist/index.js +22 -0
- package/dist/service/empathy-observer-manager.d.ts +2 -0
- package/dist/service/empathy-observer-manager.js +43 -1
- package/dist/service/evolution-worker.d.ts +19 -1
- package/dist/service/evolution-worker.js +116 -31
- package/dist/service/runtime-summary-service.d.ts +79 -0
- package/dist/service/runtime-summary-service.js +319 -0
- package/dist/types/event-types.d.ts +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,11 @@ const sessions = new Map();
|
|
|
6
6
|
let persistDir = null;
|
|
7
7
|
/** Debounce timer for persistence */
|
|
8
8
|
let persistTimer = null;
|
|
9
|
+
function logSessionTrackerWarning(message, error) {
|
|
10
|
+
const detail = error instanceof Error ? error.message : error ? String(error) : '';
|
|
11
|
+
const suffix = detail ? `: ${detail}` : '';
|
|
12
|
+
console.warn(`[PD:SessionTracker] ${message}${suffix}`);
|
|
13
|
+
}
|
|
9
14
|
/**
|
|
10
15
|
* Initialize persistence for session state.
|
|
11
16
|
* Call this once during plugin startup.
|
|
@@ -48,13 +53,13 @@ function loadAllSessions() {
|
|
|
48
53
|
}
|
|
49
54
|
sessions.set(state.sessionId, state);
|
|
50
55
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
56
|
+
catch (error) {
|
|
57
|
+
logSessionTrackerWarning(`Failed to load session snapshot ${file}`, error);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
catch (err) {
|
|
57
|
-
|
|
62
|
+
logSessionTrackerWarning('Failed to load persisted sessions', err);
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
@@ -69,8 +74,8 @@ function persistSession(state) {
|
|
|
69
74
|
try {
|
|
70
75
|
fs.writeFileSync(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
71
76
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
77
|
+
catch (error) {
|
|
78
|
+
logSessionTrackerWarning(`Failed to persist session ${state.sessionId}`, error);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
/**
|
|
@@ -108,6 +113,8 @@ function getOrCreateSession(sessionId, workspaceDir) {
|
|
|
108
113
|
cacheHits: 0,
|
|
109
114
|
stuckLoops: 0,
|
|
110
115
|
currentGfi: 0,
|
|
116
|
+
gfiBySource: {},
|
|
117
|
+
lastErrorSource: '',
|
|
111
118
|
lastErrorHash: '',
|
|
112
119
|
consecutiveErrors: 0,
|
|
113
120
|
dailyToolCalls: 0,
|
|
@@ -124,6 +131,12 @@ function getOrCreateSession(sessionId, workspaceDir) {
|
|
|
124
131
|
}
|
|
125
132
|
return state;
|
|
126
133
|
}
|
|
134
|
+
function ensureGfiLedger(state) {
|
|
135
|
+
if (!state.gfiBySource || typeof state.gfiBySource !== 'object') {
|
|
136
|
+
state.gfiBySource = {};
|
|
137
|
+
}
|
|
138
|
+
return state.gfiBySource;
|
|
139
|
+
}
|
|
127
140
|
export function trackToolRead(sessionId, filePath, workspaceDir) {
|
|
128
141
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
129
142
|
const normalizedPath = path.posix.normalize(filePath.replace(/\\/g, '/'));
|
|
@@ -165,12 +178,14 @@ export function trackLlmOutput(sessionId, usage, config, workspaceDir) {
|
|
|
165
178
|
/**
|
|
166
179
|
* Tracks physical friction based on tool execution failures.
|
|
167
180
|
*/
|
|
168
|
-
export function trackFriction(sessionId, deltaF, hash, workspaceDir) {
|
|
181
|
+
export function trackFriction(sessionId, deltaF, hash, workspaceDir, options) {
|
|
169
182
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
183
|
+
const ledger = ensureGfiLedger(state);
|
|
170
184
|
if (hash && hash === state.lastErrorHash) {
|
|
171
185
|
state.consecutiveErrors++;
|
|
172
186
|
}
|
|
173
187
|
else {
|
|
188
|
+
state.lastErrorSource = options?.source || (hash ? `unattributed:${hash}` : 'unattributed:unknown');
|
|
174
189
|
state.lastErrorHash = hash;
|
|
175
190
|
state.consecutiveErrors = 1;
|
|
176
191
|
}
|
|
@@ -178,6 +193,8 @@ export function trackFriction(sessionId, deltaF, hash, workspaceDir) {
|
|
|
178
193
|
const multiplier = Math.pow(1.5, state.consecutiveErrors - 1);
|
|
179
194
|
const addedFriction = deltaF * multiplier;
|
|
180
195
|
state.currentGfi = (state.currentGfi || 0) + addedFriction;
|
|
196
|
+
const sourceKey = options?.source || (hash ? `unattributed:${hash}` : 'unattributed:unknown');
|
|
197
|
+
ledger[sourceKey] = (ledger[sourceKey] || 0) + addedFriction;
|
|
181
198
|
state.lastActivityAt = Date.now();
|
|
182
199
|
SystemLogger.log(state.workspaceDir, 'GFI_INC', `Friction added: +${addedFriction.toFixed(1)} (Base: ${deltaF}, Mult: ${multiplier.toFixed(2)}). Total GFI: ${state.currentGfi.toFixed(1)}`);
|
|
183
200
|
// Update daily stats
|
|
@@ -190,12 +207,36 @@ export function trackFriction(sessionId, deltaF, hash, workspaceDir) {
|
|
|
190
207
|
/**
|
|
191
208
|
* Resets the friction index upon successful action.
|
|
192
209
|
*/
|
|
193
|
-
export function resetFriction(sessionId, workspaceDir) {
|
|
210
|
+
export function resetFriction(sessionId, workspaceDir, options) {
|
|
194
211
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
212
|
+
const ledger = ensureGfiLedger(state);
|
|
213
|
+
if (options?.source) {
|
|
214
|
+
const sourceKey = options.source;
|
|
215
|
+
const currentSource = ledger[sourceKey] || 0;
|
|
216
|
+
const requestedAmount = Number.isFinite(options.amount) ? Number(options.amount) : currentSource;
|
|
217
|
+
const amountToRemove = Math.max(0, Math.min(currentSource, requestedAmount));
|
|
218
|
+
if (amountToRemove > 0) {
|
|
219
|
+
ledger[sourceKey] = Math.max(0, currentSource - amountToRemove);
|
|
220
|
+
if (ledger[sourceKey] === 0) {
|
|
221
|
+
delete ledger[sourceKey];
|
|
222
|
+
}
|
|
223
|
+
state.currentGfi = Math.max(0, (state.currentGfi || 0) - amountToRemove);
|
|
224
|
+
SystemLogger.log(state.workspaceDir, 'GFI_SLICE_RESET', `Friction slice reset for ${sourceKey}: -${amountToRemove.toFixed(1)}. Total GFI: ${state.currentGfi.toFixed(1)}`);
|
|
225
|
+
if (state.lastErrorSource === sourceKey) {
|
|
226
|
+
state.consecutiveErrors = 0;
|
|
227
|
+
state.lastErrorHash = '';
|
|
228
|
+
state.lastErrorSource = '';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
schedulePersistence(state);
|
|
232
|
+
return state;
|
|
233
|
+
}
|
|
195
234
|
if (state.currentGfi > 0) {
|
|
196
235
|
SystemLogger.log(state.workspaceDir, 'GFI_RESET', `Friction reset to 0 (Was: ${state.currentGfi.toFixed(1)}). Action successful.`);
|
|
197
236
|
}
|
|
198
237
|
state.currentGfi = 0;
|
|
238
|
+
state.gfiBySource = {};
|
|
239
|
+
state.lastErrorSource = '';
|
|
199
240
|
state.consecutiveErrors = 0;
|
|
200
241
|
state.lastErrorHash = '';
|
|
201
242
|
// Schedule persistence
|
|
@@ -248,6 +289,16 @@ export function clearInjectedProbationIds(sessionId, workspaceDir) {
|
|
|
248
289
|
export function getSession(sessionId) {
|
|
249
290
|
return sessions.get(sessionId);
|
|
250
291
|
}
|
|
292
|
+
export function listSessions(workspaceDir) {
|
|
293
|
+
return [...sessions.values()]
|
|
294
|
+
.filter((state) => !workspaceDir || !state.workspaceDir || state.workspaceDir === workspaceDir)
|
|
295
|
+
.map((state) => ({
|
|
296
|
+
...state,
|
|
297
|
+
toolReadsByFile: { ...state.toolReadsByFile },
|
|
298
|
+
gfiBySource: state.gfiBySource ? { ...state.gfiBySource } : undefined,
|
|
299
|
+
injectedProbationIds: state.injectedProbationIds ? [...state.injectedProbationIds] : undefined,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
251
302
|
export function clearSession(sessionId) {
|
|
252
303
|
sessions.delete(sessionId);
|
|
253
304
|
}
|
|
@@ -264,8 +315,8 @@ export function garbageCollectSessions() {
|
|
|
264
315
|
try {
|
|
265
316
|
fs.unlinkSync(sessionPath);
|
|
266
317
|
}
|
|
267
|
-
catch {
|
|
268
|
-
|
|
318
|
+
catch (error) {
|
|
319
|
+
logSessionTrackerWarning(`Failed to delete session snapshot for ${id}`, error);
|
|
269
320
|
}
|
|
270
321
|
}
|
|
271
322
|
}
|
|
@@ -17,6 +17,8 @@ export interface TrustScorecard {
|
|
|
17
17
|
reason: string;
|
|
18
18
|
timestamp: string;
|
|
19
19
|
}>;
|
|
20
|
+
frozen?: boolean;
|
|
21
|
+
reward_policy?: 'frozen_all_positive' | 'frozen_atomic_positive_keep_plan_ready';
|
|
20
22
|
}
|
|
21
23
|
export type TrustStage = 1 | 2 | 3 | 4;
|
|
22
24
|
export declare const EXPLORATORY_TOOLS: string[];
|
|
@@ -29,6 +31,7 @@ export declare class TrustEngine {
|
|
|
29
31
|
private get config();
|
|
30
32
|
private get trustSettings();
|
|
31
33
|
private loadScorecard;
|
|
34
|
+
private applyLegacyFreezeMetadata;
|
|
32
35
|
private saveScorecard;
|
|
33
36
|
getScore(): number;
|
|
34
37
|
getScorecard(): TrustScorecard;
|
|
@@ -45,6 +48,7 @@ export declare class TrustEngine {
|
|
|
45
48
|
toolName?: string;
|
|
46
49
|
error?: string;
|
|
47
50
|
}): void;
|
|
51
|
+
private touchScorecard;
|
|
48
52
|
private updateScore;
|
|
49
53
|
resetTrust(newScore?: number): void;
|
|
50
54
|
getStatusSummary(): {
|
|
@@ -29,6 +29,7 @@ export const CONSTRUCTIVE_TOOLS = [
|
|
|
29
29
|
'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
|
|
30
30
|
'pd_run_worker', 'sessions_spawn', 'evolve-task', 'init-strategy'
|
|
31
31
|
];
|
|
32
|
+
const LEGACY_TRUST_REWARD_POLICY = 'frozen_all_positive';
|
|
32
33
|
export class TrustEngine {
|
|
33
34
|
scorecard;
|
|
34
35
|
workspaceDir;
|
|
@@ -69,6 +70,7 @@ export class TrustEngine {
|
|
|
69
70
|
data.history = [];
|
|
70
71
|
if (data.exploratory_failure_streak === undefined)
|
|
71
72
|
data.exploratory_failure_streak = 0;
|
|
73
|
+
this.applyLegacyFreezeMetadata(data);
|
|
72
74
|
return data;
|
|
73
75
|
}
|
|
74
76
|
catch (e) {
|
|
@@ -77,7 +79,7 @@ export class TrustEngine {
|
|
|
77
79
|
}
|
|
78
80
|
const now = new Date();
|
|
79
81
|
const coldStartEnd = new Date(now.getTime() + settings.cold_start.cold_start_period_ms);
|
|
80
|
-
|
|
82
|
+
const scorecard = {
|
|
81
83
|
trust_score: settings.cold_start.initial_trust,
|
|
82
84
|
success_streak: 0,
|
|
83
85
|
failure_streak: 0,
|
|
@@ -88,6 +90,12 @@ export class TrustEngine {
|
|
|
88
90
|
first_activity_at: now.toISOString(),
|
|
89
91
|
history: []
|
|
90
92
|
};
|
|
93
|
+
this.applyLegacyFreezeMetadata(scorecard);
|
|
94
|
+
return scorecard;
|
|
95
|
+
}
|
|
96
|
+
applyLegacyFreezeMetadata(scorecard) {
|
|
97
|
+
scorecard.frozen = true;
|
|
98
|
+
scorecard.reward_policy = LEGACY_TRUST_REWARD_POLICY;
|
|
91
99
|
}
|
|
92
100
|
saveScorecard() {
|
|
93
101
|
const scorecardPath = resolvePdPath(this.workspaceDir, 'AGENT_SCORECARD');
|
|
@@ -120,39 +128,19 @@ export class TrustEngine {
|
|
|
120
128
|
return new Date() < new Date(this.scorecard.cold_start_end);
|
|
121
129
|
}
|
|
122
130
|
recordSuccess(reason, context, isSubagent = false) {
|
|
123
|
-
const settings = this.trustSettings;
|
|
124
|
-
const rewards = settings.rewards;
|
|
125
131
|
const toolName = context?.toolName;
|
|
126
132
|
// 1. Check if this is an exploratory tool success
|
|
127
133
|
const isExploratory = toolName && EXPLORATORY_TOOLS.includes(toolName);
|
|
128
134
|
if (reason === 'tool_success' && isExploratory) {
|
|
129
|
-
// Exploratory tools don't grant trust points, but they:
|
|
130
|
-
// 1. Reset the exploratory failure streak
|
|
131
|
-
// 2. Prove the agent isn't stuck (no delta, no success_streak increment)
|
|
132
135
|
this.scorecard.exploratory_failure_streak = 0;
|
|
133
|
-
this.
|
|
136
|
+
this.touchScorecard();
|
|
134
137
|
return;
|
|
135
138
|
}
|
|
136
|
-
let
|
|
137
|
-
|
|
138
|
-
delta = rewards.subagent_success;
|
|
139
|
-
}
|
|
140
|
-
else if (reason === 'tool_success') {
|
|
141
|
-
delta = rewards.tool_success_reward ?? 0.2;
|
|
142
|
-
}
|
|
143
|
-
else if (reason === 'plan_ready') {
|
|
144
|
-
delta = 5; // Strategic reward
|
|
145
|
-
}
|
|
146
|
-
this.scorecard.success_streak++;
|
|
139
|
+
// Phase 1 freeze: do not let atomic successes inflate legacy trust.
|
|
140
|
+
this.scorecard.success_streak = 0;
|
|
147
141
|
this.scorecard.failure_streak = 0; // Reset failure streak on constructive success
|
|
148
142
|
this.scorecard.exploratory_failure_streak = 0;
|
|
149
|
-
|
|
150
|
-
delta += rewards.streak_bonus;
|
|
151
|
-
}
|
|
152
|
-
if (this.scorecard.trust_score < settings.stages.stage_1_observer) {
|
|
153
|
-
delta += rewards.recovery_boost;
|
|
154
|
-
}
|
|
155
|
-
this.updateScore(delta, reason, 'success', context);
|
|
143
|
+
this.touchScorecard();
|
|
156
144
|
}
|
|
157
145
|
recordFailure(type, context) {
|
|
158
146
|
const settings = this.trustSettings;
|
|
@@ -204,8 +192,14 @@ export class TrustEngine {
|
|
|
204
192
|
delta = penalties.max_penalty;
|
|
205
193
|
this.updateScore(delta, `Failure: ${toolName || type}`, 'failure', context);
|
|
206
194
|
}
|
|
195
|
+
touchScorecard() {
|
|
196
|
+
this.applyLegacyFreezeMetadata(this.scorecard);
|
|
197
|
+
this.scorecard.last_updated = new Date().toISOString();
|
|
198
|
+
this.saveScorecard();
|
|
199
|
+
}
|
|
207
200
|
updateScore(delta, reason, type, context) {
|
|
208
201
|
const oldScore = this.scorecard.trust_score;
|
|
202
|
+
this.applyLegacyFreezeMetadata(this.scorecard);
|
|
209
203
|
this.scorecard.trust_score += delta;
|
|
210
204
|
// Floor score: never drop below 30 (prevents Trust collapse from cascades)
|
|
211
205
|
if (this.scorecard.trust_score < 30)
|
|
@@ -255,6 +249,7 @@ export class TrustEngine {
|
|
|
255
249
|
this.scorecard.first_activity_at = now.toISOString();
|
|
256
250
|
this.scorecard.cold_start_end = coldStartEnd.toISOString();
|
|
257
251
|
this.scorecard.history.push({ type: 'success', delta: 0, reason: 'Manual trust reset (Spiritual Cleanse)', timestamp: now.toISOString() });
|
|
252
|
+
this.applyLegacyFreezeMetadata(this.scorecard);
|
|
258
253
|
this.saveScorecard();
|
|
259
254
|
}
|
|
260
255
|
getStatusSummary() {
|
package/dist/hooks/gate.js
CHANGED
|
@@ -37,7 +37,7 @@ const BASH_TOOLS_SET = new Set([
|
|
|
37
37
|
/**
|
|
38
38
|
* 分析 Bash 命令风险等级
|
|
39
39
|
*/
|
|
40
|
-
function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
|
|
40
|
+
function analyzeBashCommand(command, safePatterns, dangerousPatterns, logger) {
|
|
41
41
|
let normalizedCmd = command.trim().toLowerCase();
|
|
42
42
|
// P2 fix: Unicode de-obfuscation — convert Cyrillic/Unicode lookalikes to ASCII equivalents
|
|
43
43
|
// Common Cyrillic lookalikes that could bypass detection: аеорсух (Cyrillic) → aeopcyx (Latin)
|
|
@@ -72,8 +72,10 @@ function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
|
|
|
72
72
|
return 'dangerous';
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
catch {
|
|
76
|
-
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger?.warn?.(`[PD_GATE] Invalid dangerous bash regex "${pattern}": ${String(error)}. Failing closed.`);
|
|
77
|
+
return 'dangerous';
|
|
78
|
+
// Fail-closed: 无效的危险模式正则视为匹配危险命令
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
}
|
|
@@ -87,8 +89,8 @@ function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
|
|
|
87
89
|
break;
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
92
|
+
catch (error) {
|
|
93
|
+
logger?.warn?.(`[PD_GATE] Invalid safe bash regex "${pattern}": ${String(error)}. Ignoring safe override.`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
if (!isSafe) {
|
|
@@ -187,7 +189,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
187
189
|
// TIER 3: Bash 命令 - 根据内容判断
|
|
188
190
|
if (BASH_TOOLS_SET.has(event.toolName)) {
|
|
189
191
|
const command = String(event.params.command || event.params.args || '');
|
|
190
|
-
const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || []);
|
|
192
|
+
const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || [], logger);
|
|
191
193
|
if (bashRisk === 'dangerous') {
|
|
192
194
|
// 危险命令 - 直接拦截
|
|
193
195
|
logger?.warn?.(`[PD:GFI_GATE] Dangerous bash command blocked: ${command.substring(0, 50)}...`);
|
|
@@ -378,6 +380,46 @@ GFI: ${currentGfi}/100
|
|
|
378
380
|
matchesAnyPattern(relPath, planApprovals.allowed_patterns || []) &&
|
|
379
381
|
((planApprovals.max_lines_override ?? -1) === -1 || lineChanges <= (planApprovals.max_lines_override ?? -1)));
|
|
380
382
|
logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
|
|
383
|
+
// ── EP SIMULATION MODE (M6验证) ──
|
|
384
|
+
// 记录EP系统的模拟决策,但不生效(仅用于对比分析)
|
|
385
|
+
// BUGFIX #90: 移到所有Stage检查之前,确保所有Stage都触发EP simulation记录
|
|
386
|
+
try {
|
|
387
|
+
const epDecision = checkEvolutionGate(ctx.workspaceDir, {
|
|
388
|
+
toolName: event.toolName,
|
|
389
|
+
content: event.params?.content,
|
|
390
|
+
lineCount: lineChanges,
|
|
391
|
+
isRiskPath: risky,
|
|
392
|
+
});
|
|
393
|
+
const epLogEntry = {
|
|
394
|
+
timestamp: new Date().toISOString(),
|
|
395
|
+
toolName: event.toolName,
|
|
396
|
+
filePath: relPath,
|
|
397
|
+
trustEngine: { score: trustScore, stage, decision: 'allow' },
|
|
398
|
+
epSystem: { tier: epDecision.currentTier ?? 'UNKNOWN', allowed: epDecision.allowed, reason: epDecision.reason },
|
|
399
|
+
conflict: epDecision.allowed === false, // Trust允许但EP拒绝(任何阶段)
|
|
400
|
+
};
|
|
401
|
+
const epLogPath = path.join(ctx.workspaceDir, '.state', 'ep_simulation.jsonl');
|
|
402
|
+
// 安全创建目录(如果失败则跳过日志写入,但不影响 Trust Engine 决策)
|
|
403
|
+
let canWriteEpLog = true;
|
|
404
|
+
try {
|
|
405
|
+
fs.mkdirSync(path.dirname(epLogPath), { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
catch (mkdirErr) {
|
|
408
|
+
if (!mkdirErr || mkdirErr.code !== 'EEXIST') {
|
|
409
|
+
logger.warn(`[PD_EP_SIM] Failed to create log dir: ${mkdirErr?.message ?? String(mkdirErr)}, skipping log`);
|
|
410
|
+
canWriteEpLog = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (canWriteEpLog) {
|
|
414
|
+
fs.appendFileSync(epLogPath, JSON.stringify(epLogEntry) + '\n');
|
|
415
|
+
}
|
|
416
|
+
logger.info(`[PD_EP_SIM] Tier: ${epDecision.currentTier}, Allowed: ${epDecision.allowed}, Trust: ${trustScore} (Stage ${stage})`);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
// EP 模拟失败不应该影响 Trust Engine 决策
|
|
420
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
421
|
+
logger.warn(`[PD_EP_SIM] Simulation failed: ${errMsg}, continuing with Trust Engine`);
|
|
422
|
+
}
|
|
381
423
|
// Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
|
|
382
424
|
if (stage === 1) {
|
|
383
425
|
if (canUsePlanApproval) {
|
|
@@ -400,17 +442,17 @@ GFI: ${currentGfi}/100
|
|
|
400
442
|
}
|
|
401
443
|
if (risky || riskLevel !== 'LOW') {
|
|
402
444
|
// Block if not approved by whitelist
|
|
403
|
-
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
|
|
445
|
+
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName, ctx.sessionId);
|
|
404
446
|
}
|
|
405
447
|
}
|
|
406
448
|
// Stage 2 (Editor): Block writes to risk paths. Block large changes.
|
|
407
449
|
if (stage === 2) {
|
|
408
450
|
if (risky) {
|
|
409
|
-
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName);
|
|
451
|
+
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName, ctx.sessionId);
|
|
410
452
|
}
|
|
411
453
|
const stage2Limit = trustSettings.limits?.stage_2_max_lines ?? 50;
|
|
412
454
|
if (lineChanges > stage2Limit) {
|
|
413
|
-
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName);
|
|
455
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName, ctx.sessionId);
|
|
414
456
|
}
|
|
415
457
|
}
|
|
416
458
|
// Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
|
|
@@ -418,12 +460,12 @@ GFI: ${currentGfi}/100
|
|
|
418
460
|
if (risky) {
|
|
419
461
|
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
420
462
|
if (planStatus !== 'READY') {
|
|
421
|
-
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName);
|
|
463
|
+
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName, ctx.sessionId);
|
|
422
464
|
}
|
|
423
465
|
}
|
|
424
466
|
const stage3Limit = trustSettings.limits?.stage_3_max_lines ?? 300;
|
|
425
467
|
if (lineChanges > stage3Limit) {
|
|
426
|
-
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 3. Max allowed is ${stage3Limit}.`, wctx, event.toolName);
|
|
468
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 3. Max allowed is ${stage3Limit}.`, wctx, event.toolName, ctx.sessionId);
|
|
427
469
|
}
|
|
428
470
|
}
|
|
429
471
|
// Stage 4 (Architect): Full bypass
|
|
@@ -446,52 +488,13 @@ GFI: ${currentGfi}/100
|
|
|
446
488
|
}
|
|
447
489
|
return;
|
|
448
490
|
}
|
|
449
|
-
// ── EP SIMULATION MODE (M6验证) ──
|
|
450
|
-
// 记录EP系统的模拟决策,但不生效(仅用于对比分析)
|
|
451
|
-
try {
|
|
452
|
-
const epDecision = checkEvolutionGate(ctx.workspaceDir, {
|
|
453
|
-
toolName: event.toolName,
|
|
454
|
-
content: event.params?.content,
|
|
455
|
-
lineCount: lineChanges,
|
|
456
|
-
isRiskPath: risky,
|
|
457
|
-
});
|
|
458
|
-
const epLogEntry = {
|
|
459
|
-
timestamp: new Date().toISOString(),
|
|
460
|
-
toolName: event.toolName,
|
|
461
|
-
filePath: relPath,
|
|
462
|
-
trustEngine: { score: trustScore, stage, decision: 'allow' },
|
|
463
|
-
epSystem: { tier: epDecision.currentTier ?? 'UNKNOWN', allowed: epDecision.allowed, reason: epDecision.reason },
|
|
464
|
-
conflict: epDecision.allowed === false, // Trust允许但EP拒绝(任何阶段)
|
|
465
|
-
};
|
|
466
|
-
const epLogPath = path.join(ctx.workspaceDir, '.state', 'ep_simulation.jsonl');
|
|
467
|
-
// 安全创建目录(如果失败则跳过日志写入,但不影响 Trust Engine 决策)
|
|
468
|
-
let canWriteEpLog = true;
|
|
469
|
-
try {
|
|
470
|
-
fs.mkdirSync(path.dirname(epLogPath), { recursive: true });
|
|
471
|
-
}
|
|
472
|
-
catch (mkdirErr) {
|
|
473
|
-
if (!mkdirErr || mkdirErr.code !== 'EEXIST') {
|
|
474
|
-
logger.warn(`[PD_EP_SIM] Failed to create log dir: ${mkdirErr?.message ?? String(mkdirErr)}, skipping log`);
|
|
475
|
-
canWriteEpLog = false;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (canWriteEpLog) {
|
|
479
|
-
fs.appendFileSync(epLogPath, JSON.stringify(epLogEntry) + '\n');
|
|
480
|
-
}
|
|
481
|
-
logger.info(`[PD_EP_SIM] Tier: ${epDecision.currentTier}, Allowed: ${epDecision.allowed}, Trust: ${trustScore} (Stage ${stage})`);
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
// EP 模拟失败不应该影响 Trust Engine 决策
|
|
485
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
486
|
-
logger.warn(`[PD_EP_SIM] Simulation failed: ${errMsg}, continuing with Trust Engine`);
|
|
487
|
-
}
|
|
488
491
|
}
|
|
489
492
|
else {
|
|
490
493
|
// FALLBACK: Legacy Gate Logic
|
|
491
494
|
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
492
495
|
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
493
496
|
if (planStatus !== 'READY') {
|
|
494
|
-
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName);
|
|
497
|
+
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName, ctx.sessionId);
|
|
495
498
|
}
|
|
496
499
|
}
|
|
497
500
|
}
|
|
@@ -512,10 +515,22 @@ GFI: ${currentGfi}/100
|
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
517
|
}
|
|
515
|
-
function block(filePath, reason, wctx, toolName) {
|
|
518
|
+
function block(filePath, reason, wctx, toolName, sessionId) {
|
|
516
519
|
const logger = console;
|
|
517
520
|
logger.error(`[PD_GATE] BLOCKED: ${filePath}. Reason: ${reason}`);
|
|
518
|
-
|
|
521
|
+
if (sessionId) {
|
|
522
|
+
trackBlock(sessionId);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
wctx.eventLog.recordGateBlock(sessionId, {
|
|
526
|
+
toolName,
|
|
527
|
+
filePath,
|
|
528
|
+
reason,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
logger.warn(`[PD_GATE] Failed to record gate block event: ${String(error)}`);
|
|
533
|
+
}
|
|
519
534
|
return {
|
|
520
535
|
block: true,
|
|
521
536
|
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.`,
|
package/dist/hooks/llm.js
CHANGED
|
@@ -237,7 +237,7 @@ export function handleLlmOutput(event, ctx) {
|
|
|
237
237
|
const calibratedScore = Math.round(weightedScore * calibrationFactor);
|
|
238
238
|
const boundedScore = applyRateLimit(ctx.sessionId, event.runId, calibratedScore, config);
|
|
239
239
|
if (boundedScore > 0) {
|
|
240
|
-
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir);
|
|
240
|
+
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir, { source: 'user_empathy' });
|
|
241
241
|
try {
|
|
242
242
|
wctx.trajectory?.recordPainEvent?.({
|
|
243
243
|
sessionId: ctx.sessionId,
|
|
@@ -298,7 +298,10 @@ export function handleLlmOutput(event, ctx) {
|
|
|
298
298
|
const rolledBackScore = eventLog.rollbackEmpathyEvent(eventId, ctx.sessionId, 'Natural language rollback request detected', 'natural_language');
|
|
299
299
|
if (rolledBackScore > 0) {
|
|
300
300
|
// Reset GFI after successful rollback
|
|
301
|
-
resetFriction(ctx.sessionId
|
|
301
|
+
resetFriction(ctx.sessionId, ctx.workspaceDir, {
|
|
302
|
+
source: 'user_empathy',
|
|
303
|
+
amount: rolledBackScore,
|
|
304
|
+
});
|
|
302
305
|
}
|
|
303
306
|
}
|
|
304
307
|
}
|
package/dist/hooks/subagent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writePainFlag } from '../core/pain.js';
|
|
2
2
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
3
3
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
4
|
+
import { acquireQueueLock, EVOLUTION_QUEUE_LOCK_SUFFIX } from '../service/evolution-worker.js';
|
|
4
5
|
import * as fs from 'fs';
|
|
5
6
|
function emitSubagentPainEvent(wctx, payload) {
|
|
6
7
|
try {
|
|
@@ -61,38 +62,49 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
61
62
|
}, true);
|
|
62
63
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
63
64
|
if (fs.existsSync(queuePath)) {
|
|
65
|
+
const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
|
|
66
|
+
const releaseLock = acquireQueueLock(lockPath, console);
|
|
67
|
+
if (!releaseLock) {
|
|
68
|
+
console.warn('[PD:Subagent] Failed to acquire queue lock, skipping queue update');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
try {
|
|
65
72
|
const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
66
73
|
let changed = false;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
const resolveTaskTime = (task) => {
|
|
75
|
+
const raw = task?.enqueued_at || task?.timestamp;
|
|
76
|
+
const ts = new Date(raw).getTime();
|
|
77
|
+
return Number.isFinite(ts) ? ts : Number.MAX_SAFE_INTEGER;
|
|
78
|
+
};
|
|
79
|
+
// Resolve the queue entry by its position, not by id. Historical pain
|
|
80
|
+
// records may legitimately share the same id in legacy data.
|
|
81
|
+
let oldestTaskIndex = -1;
|
|
82
|
+
let oldestTaskTime = Number.MAX_SAFE_INTEGER;
|
|
83
|
+
queue.forEach((task, index) => {
|
|
84
|
+
if (task?.status !== 'in_progress')
|
|
85
|
+
return;
|
|
86
|
+
const taskTime = resolveTaskTime(task);
|
|
87
|
+
if (taskTime < oldestTaskTime) {
|
|
88
|
+
oldestTaskTime = taskTime;
|
|
89
|
+
oldestTaskIndex = index;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (oldestTaskIndex !== -1) {
|
|
93
|
+
queue[oldestTaskIndex].status = 'completed';
|
|
94
|
+
queue[oldestTaskIndex].completed_at = new Date().toISOString();
|
|
95
|
+
changed = true;
|
|
96
|
+
// Clean up the .pain_flag if it was queued, to reset the environment
|
|
97
|
+
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
98
|
+
if (fs.existsSync(painFlagPath)) {
|
|
99
|
+
try {
|
|
100
|
+
const painData = fs.readFileSync(painFlagPath, 'utf8');
|
|
101
|
+
if (painData.includes('status: queued')) {
|
|
102
|
+
fs.unlinkSync(painFlagPath);
|
|
94
103
|
}
|
|
95
104
|
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error(`[PD:Subagent] Failed to cleanup pain flag: ${String(e)}`);
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
if (changed) {
|
|
@@ -102,6 +114,9 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
102
114
|
catch (e) {
|
|
103
115
|
console.error(`[PD:Subagent] Failed to update evolution queue: ${String(e)}`);
|
|
104
116
|
}
|
|
117
|
+
finally {
|
|
118
|
+
releaseLock();
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory Collector - 行为进化引擎 Phase 0 数据收集
|
|
3
|
+
*
|
|
4
|
+
* 收集工具调用和 LLM 输出到 memory/trajectories/ 目录
|
|
5
|
+
* 用于分析工具使用模式、识别原则应用案例、评估行为质量
|
|
6
|
+
*/
|
|
7
|
+
import type { PluginHookAfterToolCallEvent, PluginHookToolContext, PluginHookLlmOutputEvent, PluginHookAgentContext, PluginHookBeforeMessageWriteEvent } from '../openclaw-sdk.js';
|
|
8
|
+
/**
|
|
9
|
+
* 工具调用完成后的处理
|
|
10
|
+
* 记录:工具名、参数、结果、错误、执行时间
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleAfterToolCall(event: PluginHookAfterToolCallEvent, ctx: PluginHookToolContext & {
|
|
13
|
+
workspaceDir?: string;
|
|
14
|
+
}): void;
|
|
15
|
+
/**
|
|
16
|
+
* LLM 输出处理
|
|
17
|
+
* 记录:provider、model、输出长度、token 使用量
|
|
18
|
+
*/
|
|
19
|
+
export declare function handleLlmOutput(event: PluginHookLlmOutputEvent, ctx: PluginHookAgentContext & {
|
|
20
|
+
workspaceDir?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
/**
|
|
23
|
+
* 消息写入前的处理
|
|
24
|
+
* 记录:用户/助手消息内容
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleBeforeMessageWrite(event: PluginHookBeforeMessageWriteEvent, ctx: PluginHookAgentContext & {
|
|
27
|
+
workspaceDir?: string;
|
|
28
|
+
}): void;
|
|
29
|
+
/**
|
|
30
|
+
* 轨迹汇总统计(供 cron 任务调用)
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeTrajectoryStats(workspaceDir: string): object;
|