principles-disciple 1.92.0 → 1.94.0

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.
@@ -10,11 +10,17 @@
10
10
  * had their own block persistence implementations.
11
11
  */
12
12
 
13
+ import * as fs from 'fs';
13
14
  import { getSession, trackBlock } from '../core/session-tracker.js';
14
15
  import type { WorkspaceContext } from '../core/workspace-context.js';
15
16
  import type { PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
16
17
  import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
17
18
  import { emitPainDetectedEvent } from './pain.js';
19
+ import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
20
+ import { evaluateEvidenceTriage } from './triage-adapter.js';
21
+ import { isRisky } from '../utils/io.js';
22
+ import { normalizeProfile } from '../core/profile.js';
23
+ import { SystemLogger } from '../core/system-logger.js';
18
24
  import {
19
25
  TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS,
20
26
  TRAJECTORY_GATE_BLOCK_MAX_RETRIES
@@ -114,38 +120,75 @@ export function recordGateBlockAndReturn(
114
120
  origin: 'system_infer',
115
121
  });
116
122
 
117
- const session = getSession(sessionId);
118
- const gate = evaluatePainDiagnosticGate({
119
- source: 'gate_blocked',
120
- score: GATE_BLOCK_PAIN_SCORE,
121
- currentGfi: session?.currentGfi ?? 0,
122
- consecutiveErrors: session?.consecutiveErrors ?? 0,
123
- sessionId,
124
- errorHash: `${toolName}:${filePath}:${reason}`,
125
- thresholds: {
126
- painTrigger: wctx.config.get('thresholds.pain_trigger') || 40,
127
- highSeverity: wctx.config.get('severity_thresholds.high') || 70,
128
- },
129
- });
123
+ // PEAT-B1: Evidence triage (feature-flagged)
124
+ let triageAdmitted = true;
125
+ const gateBlockTriageFlag = loadFeatureFlagFromConfig(wctx.workspaceDir, 'painEvidenceAdmission');
126
+ if (gateBlockTriageFlag.enabled) {
127
+ // Load profile with 1MB size guard, matching pain.ts pattern
128
+ const profilePath = wctx.resolve('PROFILE');
129
+ let profile = normalizeProfile({});
130
+ if (fs.existsSync(profilePath)) {
131
+ try {
132
+ const content = fs.readFileSync(profilePath, 'utf8');
133
+ if (content.length > 1024 * 1024) {
134
+ logger.warn?.('[PD_GATE] PROFILE.json exceeds 1 MB, skipping');
135
+ SystemLogger.log(wctx.workspaceDir, 'PROFILE_PARSE_WARN', 'PROFILE.json exceeds 1 MB, skipping — fallback to non-risky');
136
+ } else {
137
+ profile = normalizeProfile(JSON.parse(content));
138
+ }
139
+ } catch (e) {
140
+ logger.warn?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
141
+ SystemLogger.log(wctx.workspaceDir, 'PROFILE_PARSE_WARN', `Failed to parse PROFILE.json: ${String(e)} — fallback to non-risky`);
142
+ }
143
+ }
144
+ // Real judgment for rulehost blocks: if a principle blocked an action on a risky path,
145
+ // it IS a high-confidence unsafe action. The pain score (45) is the evidence friction
146
+ // weight, NOT the action risk severity. The rulehost principle already determined
147
+ // this action was important enough to block — that is the real signal.
148
+ const isUnsafe = isRisky(filePath, profile.risk_paths);
149
+ const triage = evaluateEvidenceTriage('rulehost_block', GATE_BLOCK_PAIN_SCORE, {
150
+ isUnsafeHighConfidence: isUnsafe,
151
+ });
152
+ if (triage.decision !== 'admit') {
153
+ triageAdmitted = false;
154
+ logger.info?.(`[PD_GATE] Triage ${triage.decision}: ${triage.reason}`);
155
+ }
156
+ }
130
157
 
131
- if (gate.shouldDiagnose) {
132
- void emitPainDetectedEvent(wctx, {
133
- ts: new Date().toISOString(),
134
- type: 'pain_detected',
135
- data: {
136
- painId: `gate_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
137
- painType: 'user_frustration',
138
- source: 'gate_blocked',
139
- reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
140
- score: GATE_BLOCK_PAIN_SCORE,
141
- sessionId,
142
- agentId: 'main',
158
+ const session = getSession(sessionId);
159
+ if (triageAdmitted) {
160
+ const gate = evaluatePainDiagnosticGate({
161
+ source: 'gate_blocked',
162
+ score: GATE_BLOCK_PAIN_SCORE,
163
+ currentGfi: session?.currentGfi ?? 0,
164
+ consecutiveErrors: session?.consecutiveErrors ?? 0,
165
+ sessionId,
166
+ errorHash: `${toolName}:${filePath}:${reason}`,
167
+ thresholds: {
168
+ painTrigger: wctx.config.get('thresholds.pain_trigger') || 40,
169
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
143
170
  },
144
- }).catch((emitErr) => {
145
- logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
146
171
  });
147
- } else {
148
- logger.info?.(`[PD_GATE] Gate block recorded without Runtime V2 diagnosis: ${gate.detail}`);
172
+
173
+ if (gate.shouldDiagnose) {
174
+ void emitPainDetectedEvent(wctx, {
175
+ ts: new Date().toISOString(),
176
+ type: 'pain_detected',
177
+ data: {
178
+ painId: `gate_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
179
+ painType: 'user_frustration',
180
+ source: 'gate_blocked',
181
+ reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
182
+ score: GATE_BLOCK_PAIN_SCORE,
183
+ sessionId,
184
+ agentId: 'main',
185
+ },
186
+ }).catch((emitErr) => {
187
+ logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
188
+ });
189
+ } else {
190
+ logger.info?.(`[PD_GATE] Gate block recorded without Runtime V2 diagnosis: ${gate.detail}`);
191
+ }
149
192
  }
150
193
  }
151
194
 
package/src/hooks/llm.ts CHANGED
@@ -11,6 +11,8 @@ import { sanitizeAssistantText } from './message-sanitize.js';
11
11
  import { atomicWriteFileSync } from '../utils/io.js';
12
12
  import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
13
13
  import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
14
+ import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
15
+ import { resolveSourceKindFromLlmDetection, evaluateEvidenceTriage } from './triage-adapter.js';
14
16
 
15
17
  export interface EmpathySignal {
16
18
  detected: boolean;
@@ -240,26 +242,26 @@ export function handleLlmOutput(
240
242
  // GFI-triggered pain: when accumulated friction crosses highGfi threshold,
241
243
  // emit pain signal even if L1 detection didn't fire.
242
244
  const highGfiThreshold = Math.max(config.get('severity_thresholds.high') || 70, painTriggerThreshold + 30);
245
+ let isGfiTriggered = false;
243
246
  if (state.currentGfi >= highGfiThreshold && painScore < painTriggerThreshold) {
244
247
  painScore = Math.min(state.currentGfi, 60);
245
248
  source = 'user_empathy';
249
+ isGfiTriggered = true;
246
250
  matchedReason = `Accumulated GFI (${state.currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Source: empathy keyword friction.`;
247
251
  }
248
252
 
249
253
  if (painScore >= painTriggerThreshold) {
250
- const gate = evaluatePainDiagnosticGate({
251
- source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
252
- score: painScore,
253
- currentGfi: state.currentGfi,
254
- consecutiveErrors: state.consecutiveErrors,
255
- sessionId: ctx.sessionId || 'unknown',
256
- errorHash: source,
257
- thresholds: {
258
- painTrigger: painTriggerThreshold,
259
- highSeverity: config.get('severity_thresholds.high') || 70,
260
- semanticPain: Math.max(painTriggerThreshold, 60),
261
- },
262
- });
254
+ // PEAT-B1: Evidence triage (feature-flagged)
255
+ let triageAdmitted = true;
256
+ const llmTriageFlag = loadFeatureFlagFromConfig(ctx.workspaceDir!, 'painEvidenceAdmission');
257
+ if (llmTriageFlag.enabled) {
258
+ const sourceKind = resolveSourceKindFromLlmDetection(source, isGfiTriggered);
259
+ const triage = evaluateEvidenceTriage(sourceKind, painScore);
260
+ if (triage.decision !== 'admit') {
261
+ triageAdmitted = false;
262
+ ctx.logger?.info?.(`[PD:LLM] Triage ${triage.decision}: ${triage.reason}`);
263
+ }
264
+ }
263
265
 
264
266
  eventLog.recordPainSignal(ctx.sessionId, {
265
267
  score: painScore,
@@ -268,25 +270,43 @@ export function handleLlmOutput(
268
270
  isRisky: false
269
271
  });
270
272
 
271
- if (gate.shouldDiagnose) {
272
- const evidence = buildTrajectoryEvidence(wctx, ctx.sessionId || 'unknown');
273
- emitPainDetectedEvent(wctx, {
274
- ts: new Date().toISOString(),
275
- type: 'pain_detected',
276
- data: {
277
- painId: `llm_${Date.now()}`,
278
- painType: 'user_frustration' as const,
279
- source,
280
- reason: `${matchedReason}; diagnosticGate=${gate.reason}`,
281
- score: painScore,
282
- sessionId: ctx.sessionId || 'unknown',
283
- agentId: ctx.agentId,
284
- provenance: 'openclaw_context_bound',
285
- evidence,
273
+ if (triageAdmitted) {
274
+ const gate = evaluatePainDiagnosticGate({
275
+ source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
276
+ score: painScore,
277
+ currentGfi: state.currentGfi,
278
+ consecutiveErrors: state.consecutiveErrors,
279
+ sessionId: ctx.sessionId || 'unknown',
280
+ errorHash: source,
281
+ thresholds: {
282
+ painTrigger: painTriggerThreshold,
283
+ highSeverity: config.get('severity_thresholds.high') || 70,
284
+ semanticPain: Math.max(painTriggerThreshold, 60),
286
285
  },
287
286
  });
287
+
288
+ if (gate.shouldDiagnose) {
289
+ const evidence = buildTrajectoryEvidence(wctx, ctx.sessionId || 'unknown');
290
+ emitPainDetectedEvent(wctx, {
291
+ ts: new Date().toISOString(),
292
+ type: 'pain_detected',
293
+ data: {
294
+ painId: `llm_${Date.now()}`,
295
+ painType: 'user_frustration' as const,
296
+ source,
297
+ reason: `${matchedReason}; diagnosticGate=${gate.reason}`,
298
+ score: painScore,
299
+ sessionId: ctx.sessionId || 'unknown',
300
+ agentId: ctx.agentId,
301
+ provenance: 'openclaw_context_bound',
302
+ evidence,
303
+ },
304
+ });
305
+ } else {
306
+ ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
307
+ }
288
308
  } else {
289
- ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
309
+ ctx.logger?.info?.(`[PD:LLM] Triage evidence-only: pain signal recorded without diagnosis or gate evaluation`);
290
310
  }
291
311
  }
292
312