principles-disciple 1.145.2 → 1.146.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.
- package/dist/core/pain-diagnostic-gate.d.ts +11 -0
- package/dist/core/pain-diagnostic-gate.js +11 -0
- package/dist/hooks/after-tool-call-helpers.d.ts +1 -0
- package/dist/hooks/after-tool-call-helpers.js +7 -15
- package/dist/hooks/gate-block-helper.js +51 -7
- package/dist/hooks/llm.js +57 -23
- package/dist/hooks/pain.js +110 -47
- package/dist/hooks/prompt.js +206 -75
- package/dist/hooks/raw-observation-adapter.d.ts +1 -1
- package/dist/hooks/raw-observation-adapter.js +1 -1
- package/dist/hooks/trigger-cooldown-tracker.d.ts +12 -0
- package/dist/hooks/trigger-cooldown-tracker.js +25 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/langs/en/skills/pd-cli-operator/SKILL.md +20 -11
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +10 -0
- package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +20 -11
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +10 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pain Diagnostic Gate — PRI-446 thin adapter
|
|
3
3
|
*
|
|
4
|
+
* @deprecated PRI-454 — Gate A (PainDiagnosticGate) is superseded by Gate B
|
|
5
|
+
* (TriggerController + EvidenceTriage). This module remains as the rollback
|
|
6
|
+
* path when `painEvidenceAdmission` or `painEvidenceAdmissionDefault` flags
|
|
7
|
+
* are OFF. Do not add new callers. New admission logic must use
|
|
8
|
+
* `evaluateTriggerController` from runtime-v2/evidence-triage.
|
|
9
|
+
*
|
|
10
|
+
* Disposition: Archive (do not delete) per PRI-454 plan step 6.
|
|
11
|
+
* Removal conditions: Both flags confirmed ON in production for 30 days,
|
|
12
|
+
* and all 5 MVP paths verified on Gate B. See
|
|
13
|
+
* docs/plans/2026-06-pain-evidence-admission-track.md.
|
|
14
|
+
*
|
|
4
15
|
* The pure decision logic (threshold tree, cooldown comparison, episode-key
|
|
5
16
|
* construction) now lives in principles-core
|
|
6
17
|
* (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pain Diagnostic Gate — PRI-446 thin adapter
|
|
3
3
|
*
|
|
4
|
+
* @deprecated PRI-454 — Gate A (PainDiagnosticGate) is superseded by Gate B
|
|
5
|
+
* (TriggerController + EvidenceTriage). This module remains as the rollback
|
|
6
|
+
* path when `painEvidenceAdmission` or `painEvidenceAdmissionDefault` flags
|
|
7
|
+
* are OFF. Do not add new callers. New admission logic must use
|
|
8
|
+
* `evaluateTriggerController` from runtime-v2/evidence-triage.
|
|
9
|
+
*
|
|
10
|
+
* Disposition: Archive (do not delete) per PRI-454 plan step 6.
|
|
11
|
+
* Removal conditions: Both flags confirmed ON in production for 30 days,
|
|
12
|
+
* and all 5 MVP paths verified on Gate B. See
|
|
13
|
+
* docs/plans/2026-06-pain-evidence-admission-track.md.
|
|
14
|
+
*
|
|
4
15
|
* The pure decision logic (threshold tree, cooldown comparison, episode-key
|
|
5
16
|
* construction) now lives in principles-core
|
|
6
17
|
* (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
|
|
@@ -92,5 +92,6 @@ export declare function emitPainIfAdmitted(wctx: WorkspaceContext, event: Plugin
|
|
|
92
92
|
export { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
93
93
|
/**
|
|
94
94
|
* Reset trigger cooldown state (for tests).
|
|
95
|
+
* PRI-454: Delegates to shared cooldown Map reset.
|
|
95
96
|
*/
|
|
96
97
|
export declare function resetTriggerCooldownForTest(): void;
|
|
@@ -19,7 +19,7 @@ import { denoiseError, computeHash } from '../utils/hashing.js';
|
|
|
19
19
|
import { SystemLogger } from '../core/system-logger.js';
|
|
20
20
|
import { getEvolutionLogger, createTraceId } from '../core/evolution-logger.js';
|
|
21
21
|
import { recordEvolutionSuccess, recordEvolutionFailure } from '../core/evolution-engine.js';
|
|
22
|
-
import {
|
|
22
|
+
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed, resetSharedCooldownForTest } from './trigger-cooldown-tracker.js';
|
|
23
23
|
import { sanitizeForEvidence, sanitizeToolParamsForEvidence } from './message-sanitize.js';
|
|
24
24
|
import { resolveSourceKind, buildToolFailureObservation } from './raw-observation-adapter.js';
|
|
25
25
|
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
@@ -313,15 +313,6 @@ export function handleProbationFeedback(sessionId, toolName, workspaceDir, wctx,
|
|
|
313
313
|
}
|
|
314
314
|
// ── Stage 6: Pain Admission ─────────────────────────────────────────────────
|
|
315
315
|
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file', 'replace'];
|
|
316
|
-
/**
|
|
317
|
-
* Cooldown map for trigger controller decisions.
|
|
318
|
-
*
|
|
319
|
-
* PRI-363: This replaces the hidden map in PainDiagnosticGate.
|
|
320
|
-
* Core trigger-controller is stateless; plugin layer owns cooldown state.
|
|
321
|
-
*
|
|
322
|
-
* EP-05: Loop state freshness — each check reads fresh state from this map.
|
|
323
|
-
*/
|
|
324
|
-
const TRIGGER_COOLDOWN_MAP = new Map();
|
|
325
316
|
/**
|
|
326
317
|
* Evaluate whether a tool failure should trigger pain diagnosis.
|
|
327
318
|
*
|
|
@@ -346,8 +337,8 @@ export function evaluatePainAdmissionForToolCall(event, observation, outcome, la
|
|
|
346
337
|
};
|
|
347
338
|
}
|
|
348
339
|
const failureSource = outcome.failureSource ?? 'tool_failure';
|
|
349
|
-
// Check cooldown before calling trigger controller
|
|
350
|
-
const cooldownActive =
|
|
340
|
+
// Check cooldown before calling trigger controller (PRI-454: shared Map)
|
|
341
|
+
const cooldownActive = isSharedCooldownActive(failureSource, sessionId, latestFailureState?.lastErrorHash);
|
|
351
342
|
// PRI-360 S1: Build RawObservation for unified source mapping
|
|
352
343
|
const rawObs = {
|
|
353
344
|
observedAt: new Date().toISOString(),
|
|
@@ -386,9 +377,9 @@ export function evaluatePainAdmissionForToolCall(event, observation, outcome, la
|
|
|
386
377
|
tool: event.toolName,
|
|
387
378
|
path: observation.relPath,
|
|
388
379
|
}));
|
|
389
|
-
// If trigger controller says yes, mark cooldown and admit
|
|
380
|
+
// If trigger controller says yes, mark cooldown and admit (PRI-454: shared Map)
|
|
390
381
|
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
391
|
-
|
|
382
|
+
markSharedEpisodeAsDiagnosed(failureSource, sessionId, latestFailureState?.lastErrorHash);
|
|
392
383
|
return {
|
|
393
384
|
admitted: true,
|
|
394
385
|
stage: 'trigger_admitted',
|
|
@@ -507,9 +498,10 @@ export function emitPainIfAdmitted(wctx, event, observation, outcome, admission,
|
|
|
507
498
|
export { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
508
499
|
/**
|
|
509
500
|
* Reset trigger cooldown state (for tests).
|
|
501
|
+
* PRI-454: Delegates to shared cooldown Map reset.
|
|
510
502
|
*/
|
|
511
503
|
export function resetTriggerCooldownForTest() {
|
|
512
|
-
|
|
504
|
+
resetSharedCooldownForTest();
|
|
513
505
|
}
|
|
514
506
|
// ── Source Classification ────────────────────────────────────────────────────
|
|
515
507
|
// classifyToolFailureSource logic is now in resolveSourceKind (PRI-360 S1)
|
|
@@ -15,6 +15,8 @@ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
|
15
15
|
import { emitPainDetectedEvent } from './pain.js';
|
|
16
16
|
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
17
17
|
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
18
|
+
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
19
|
+
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
18
20
|
import { isRisky } from '../utils/io.js';
|
|
19
21
|
import { normalizeProfile } from '../core/profile.js';
|
|
20
22
|
import { SystemLogger } from '../core/system-logger.js';
|
|
@@ -83,10 +85,14 @@ export function recordGateBlockAndReturn(wctx, blockCtx, logger) {
|
|
|
83
85
|
// evolution.jsonl + trajectory.db (with canonicalPainId for dedup). No separate
|
|
84
86
|
// legacy recordPainEvent call needed — avoids double-write to trajectory.db.
|
|
85
87
|
const gatePainId = `gate_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
88
|
+
// PRI-454: Dual-gate migration. When both flags ON → Gate B (TriggerController).
|
|
89
|
+
// When either OFF → Gate A (PainDiagnosticGate, rollback).
|
|
90
|
+
const triageFlag = loadFeatureFlagFromConfig(wctx.workspaceDir, 'painEvidenceAdmission');
|
|
91
|
+
const defaultFlag = loadFeatureFlagFromConfig(wctx.workspaceDir, 'painEvidenceAdmissionDefault');
|
|
92
|
+
const useGateB = triageFlag.enabled && defaultFlag.enabled;
|
|
93
|
+
// PEAT-B1: Evidence triage (runs when Gate B is active)
|
|
94
|
+
const session = getSession(sessionId);
|
|
95
|
+
if (useGateB) {
|
|
90
96
|
// Load profile with 1MB size guard, matching pain.ts pattern
|
|
91
97
|
const profilePath = wctx.resolve('PROFILE');
|
|
92
98
|
let profile = normalizeProfile({});
|
|
@@ -111,16 +117,54 @@ export function recordGateBlockAndReturn(wctx, blockCtx, logger) {
|
|
|
111
117
|
// weight, NOT the action risk severity. The rulehost principle already determined
|
|
112
118
|
// this action was important enough to block — that is the real signal.
|
|
113
119
|
const isUnsafe = isRisky(filePath, profile.risk_paths);
|
|
120
|
+
// PRI-454 P2-1: Pass consecutiveErrors and isRisky to match Gate A's
|
|
121
|
+
// upgrade logic. Rule 3 (consecutiveErrors >= 4 → admit) was being
|
|
122
|
+
// dropped, so non-risky repeated gate blocks never triggered diagnosis.
|
|
114
123
|
const triage = evaluateEvidenceTriage('rulehost_block', GATE_BLOCK_PAIN_SCORE, {
|
|
115
124
|
isUnsafeHighConfidence: isUnsafe,
|
|
125
|
+
isRisky: isUnsafe,
|
|
126
|
+
consecutiveErrors: session?.consecutiveErrors,
|
|
116
127
|
});
|
|
117
128
|
if (triage.decision !== 'admit') {
|
|
118
|
-
triageAdmitted = false;
|
|
119
129
|
logger.info?.(`[PD_GATE] Triage ${triage.decision}: ${triage.reason}`);
|
|
120
130
|
}
|
|
131
|
+
else {
|
|
132
|
+
// PRI-454: Gate B path — TriggerController owns admission
|
|
133
|
+
const errorHash = `${toolName}:${filePath}:${reason}`;
|
|
134
|
+
const cooldownActive = isSharedCooldownActive('rulehost_block', sessionId, errorHash);
|
|
135
|
+
const triggerDecision = evaluateTriggerController({
|
|
136
|
+
triageResult: triage,
|
|
137
|
+
isOwnerManual: false,
|
|
138
|
+
isCooldownActive: cooldownActive,
|
|
139
|
+
isValid: true,
|
|
140
|
+
score: GATE_BLOCK_PAIN_SCORE,
|
|
141
|
+
sessionId,
|
|
142
|
+
});
|
|
143
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
144
|
+
markSharedEpisodeAsDiagnosed('rulehost_block', sessionId, errorHash);
|
|
145
|
+
void emitPainDetectedEvent(wctx, {
|
|
146
|
+
ts: new Date().toISOString(),
|
|
147
|
+
type: 'pain_detected',
|
|
148
|
+
data: {
|
|
149
|
+
painId: gatePainId,
|
|
150
|
+
painType: 'user_frustration',
|
|
151
|
+
source: 'gate_blocked',
|
|
152
|
+
reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
|
|
153
|
+
score: GATE_BLOCK_PAIN_SCORE,
|
|
154
|
+
sessionId,
|
|
155
|
+
agentId: 'main',
|
|
156
|
+
},
|
|
157
|
+
}).catch((emitErr) => {
|
|
158
|
+
logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
logger.info?.(`[PD_GATE] Gate B skipped: ${triggerDecision.reason}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
121
165
|
}
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
else {
|
|
167
|
+
// PRI-454: Gate A path (rollback when either flag is OFF)
|
|
124
168
|
const gate = evaluatePainDiagnosticGate({
|
|
125
169
|
source: 'gate_blocked',
|
|
126
170
|
score: GATE_BLOCK_PAIN_SCORE,
|
package/dist/hooks/llm.js
CHANGED
|
@@ -13,6 +13,8 @@ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
|
13
13
|
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
14
14
|
import { resolveSourceKind } from './raw-observation-adapter.js';
|
|
15
15
|
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
16
|
+
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
17
|
+
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
16
18
|
function clamp(value, min, max) {
|
|
17
19
|
return Math.max(min, Math.min(max, value));
|
|
18
20
|
}
|
|
@@ -241,25 +243,11 @@ export function handleLlmOutput(event, ctx) {
|
|
|
241
243
|
matchedReason = `Accumulated GFI (${state.currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Source: empathy keyword friction.`;
|
|
242
244
|
}
|
|
243
245
|
if (painScore >= painTriggerThreshold) {
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const rawObs = {
|
|
250
|
-
observedAt: new Date().toISOString(),
|
|
251
|
-
workspaceId: ctx.workspaceDir,
|
|
252
|
-
sessionId: ctx.sessionId,
|
|
253
|
-
detectionSource: source,
|
|
254
|
-
isGfiTriggered,
|
|
255
|
-
};
|
|
256
|
-
const sourceKind = resolveSourceKind(rawObs);
|
|
257
|
-
const triage = evaluateEvidenceTriage(sourceKind, painScore);
|
|
258
|
-
if (triage.decision !== 'admit') {
|
|
259
|
-
triageAdmitted = false;
|
|
260
|
-
ctx.logger?.info?.(`[PD:LLM] Triage ${triage.decision}: ${triage.reason}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
246
|
+
// PRI-454: Dual-gate migration. When both flags ON → Gate B (TriggerController).
|
|
247
|
+
// When either OFF → Gate A (PainDiagnosticGate, rollback).
|
|
248
|
+
const triageFlag = loadFeatureFlagFromConfig(ctx.workspaceDir, 'painEvidenceAdmission');
|
|
249
|
+
const defaultFlag = loadFeatureFlagFromConfig(ctx.workspaceDir, 'painEvidenceAdmissionDefault');
|
|
250
|
+
const useGateB = triageFlag.enabled && defaultFlag.enabled;
|
|
263
251
|
eventLog.recordPainSignal(ctx.sessionId, {
|
|
264
252
|
score: painScore,
|
|
265
253
|
source: source,
|
|
@@ -278,7 +266,56 @@ export function handleLlmOutput(event, ctx) {
|
|
|
278
266
|
origin: 'system_infer',
|
|
279
267
|
canonicalPainId: painId,
|
|
280
268
|
});
|
|
281
|
-
if (
|
|
269
|
+
if (useGateB) {
|
|
270
|
+
// PRI-454: Gate B path — TriggerController owns admission
|
|
271
|
+
const rawObs = {
|
|
272
|
+
observedAt: new Date().toISOString(),
|
|
273
|
+
workspaceId: ctx.workspaceDir,
|
|
274
|
+
sessionId: ctx.sessionId,
|
|
275
|
+
detectionSource: source,
|
|
276
|
+
isGfiTriggered,
|
|
277
|
+
};
|
|
278
|
+
const sourceKind = resolveSourceKind(rawObs);
|
|
279
|
+
const triage = evaluateEvidenceTriage(sourceKind, painScore);
|
|
280
|
+
if (triage.decision !== 'admit') {
|
|
281
|
+
ctx.logger?.info?.(`[PD:LLM] Triage ${triage.decision}: ${triage.reason}`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const cooldownActive = isSharedCooldownActive(sourceKind, ctx.sessionId, source);
|
|
285
|
+
const triggerDecision = evaluateTriggerController({
|
|
286
|
+
triageResult: triage,
|
|
287
|
+
isOwnerManual: false,
|
|
288
|
+
isCooldownActive: cooldownActive,
|
|
289
|
+
isValid: true,
|
|
290
|
+
score: painScore,
|
|
291
|
+
sessionId: ctx.sessionId,
|
|
292
|
+
});
|
|
293
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
294
|
+
markSharedEpisodeAsDiagnosed(sourceKind, ctx.sessionId, source);
|
|
295
|
+
const evidence = buildTrajectoryEvidence(wctx, ctx.sessionId || 'unknown');
|
|
296
|
+
emitPainDetectedEvent(wctx, {
|
|
297
|
+
ts: new Date().toISOString(),
|
|
298
|
+
type: 'pain_detected',
|
|
299
|
+
data: {
|
|
300
|
+
painId,
|
|
301
|
+
painType: 'user_frustration',
|
|
302
|
+
source,
|
|
303
|
+
reason: `${matchedReason}; trigger=${triggerDecision.outcome}`,
|
|
304
|
+
score: painScore,
|
|
305
|
+
sessionId: ctx.sessionId || 'unknown',
|
|
306
|
+
agentId: ctx.agentId,
|
|
307
|
+
provenance: 'openclaw_context_bound',
|
|
308
|
+
evidence,
|
|
309
|
+
},
|
|
310
|
+
}, { recordObservability: false });
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
ctx.logger?.info?.(`[PD:LLM] Gate B skipped: ${triggerDecision.reason}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// PRI-454: Gate A path (rollback when either flag is OFF)
|
|
282
319
|
const gate = evaluatePainDiagnosticGate({
|
|
283
320
|
source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
|
|
284
321
|
score: painScore,
|
|
@@ -314,9 +351,6 @@ export function handleLlmOutput(event, ctx) {
|
|
|
314
351
|
ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
|
|
315
352
|
}
|
|
316
353
|
}
|
|
317
|
-
else {
|
|
318
|
-
ctx.logger?.info?.(`[PD:LLM] Triage evidence-only: pain signal recorded without diagnosis or gate evaluation`);
|
|
319
|
-
}
|
|
320
354
|
}
|
|
321
355
|
// ═══ Thinking OS: Mental Model Usage Tracking ═══
|
|
322
356
|
trackThinkingModelUsage({
|
package/dist/hooks/pain.js
CHANGED
|
@@ -24,7 +24,11 @@ import { getEvolutionLogger, createTraceId } from '../core/evolution-logger.js';
|
|
|
24
24
|
import { resolveWorkspaceDirForRuntimeV2 } from '../utils/workspace-resolver.js';
|
|
25
25
|
import { PainToPrincipleService, PrincipleTreeLedgerAdapter } from '@principles/core/runtime-v2';
|
|
26
26
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
27
|
-
import { loadPdConfigForPlugin } from '../core/pd-config-loader.js';
|
|
27
|
+
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
28
|
+
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
29
|
+
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
30
|
+
import { buildManualPainObservation, resolveSourceKind } from './raw-observation-adapter.js';
|
|
31
|
+
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
28
32
|
import { classifyToolCallOutcome, buildToolCallObservation, handleFrictionTrackingForFailure, handleFrictionTrackingForSuccess, recordHygieneTracking, handleProbationFeedback, evaluatePainAdmissionForToolCall, emitPainIfAdmitted, } from './after-tool-call-helpers.js';
|
|
29
33
|
import { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
30
34
|
export { buildTrajectoryEvidence };
|
|
@@ -241,61 +245,120 @@ function handleManualPain(event, ctx, wctx, workspaceDir, sessionId) {
|
|
|
241
245
|
toolName: event.toolName,
|
|
242
246
|
sessionId,
|
|
243
247
|
});
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
SystemLogger.log(workspaceDir, 'PAYLOAD_SERIALIZE_FAILED', String(e));
|
|
267
|
-
payload = JSON.stringify({ reason: gate.reason, detail: '(log serialization failed)' });
|
|
268
|
-
}
|
|
269
|
-
SystemLogger.log(workspaceDir, 'PAIN_GATE_REJECTED', payload);
|
|
270
|
-
// PEAT-B2: Record trigger decision even when cooldown blocks manual pain
|
|
248
|
+
// PRI-454: Dual-gate migration. When both flags ON → Gate B (TriggerController).
|
|
249
|
+
// When either OFF → Gate A (PainDiagnosticGate, rollback).
|
|
250
|
+
const triageFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmission');
|
|
251
|
+
const defaultFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmissionDefault');
|
|
252
|
+
const useGateB = triageFlag.enabled && defaultFlag.enabled;
|
|
253
|
+
if (useGateB) {
|
|
254
|
+
// PRI-454: Gate B path — manual pain is owner-reported.
|
|
255
|
+
// isOwnerManual: true → evaluateTriggerController returns manual_owner_admitted
|
|
256
|
+
// (bypasses triage and cooldown per PRODUCT_IDENTITY: owner-governed).
|
|
257
|
+
const rawObs = buildManualPainObservation({ sessionId });
|
|
258
|
+
const sourceKind = resolveSourceKind(rawObs); // → 'owner_reported'
|
|
259
|
+
const triage = evaluateEvidenceTriage(sourceKind, 100); // → 'admit'
|
|
260
|
+
const cooldownActive = isSharedCooldownActive(sourceKind, sessionId, 'manual_pain');
|
|
261
|
+
const triggerDecision = evaluateTriggerController({
|
|
262
|
+
triageResult: triage,
|
|
263
|
+
isOwnerManual: true,
|
|
264
|
+
isCooldownActive: cooldownActive,
|
|
265
|
+
isValid: true,
|
|
266
|
+
score: 100,
|
|
267
|
+
sessionId,
|
|
268
|
+
});
|
|
269
|
+
// PEAT-B2: Record trigger decision
|
|
271
270
|
const painTriageFlag = loadPdConfigForPlugin(workspaceDir);
|
|
272
271
|
if (painTriageFlag.effective) {
|
|
273
272
|
SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
|
|
274
|
-
outcome:
|
|
275
|
-
sourceKind:
|
|
276
|
-
reason:
|
|
277
|
-
nextAction:
|
|
273
|
+
outcome: triggerDecision.outcome,
|
|
274
|
+
sourceKind: triggerDecision.sourceKind,
|
|
275
|
+
reason: triggerDecision.reason,
|
|
276
|
+
nextAction: triggerDecision.nextAction,
|
|
278
277
|
isOwnerManual: true,
|
|
279
278
|
sessionId,
|
|
280
279
|
score: 100,
|
|
281
280
|
}));
|
|
282
281
|
}
|
|
283
|
-
|
|
282
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
283
|
+
markSharedEpisodeAsDiagnosed(sourceKind, sessionId, 'manual_pain');
|
|
284
|
+
emitPainDetectedEvent(wctx, {
|
|
285
|
+
ts: new Date().toISOString(),
|
|
286
|
+
type: 'pain_detected',
|
|
287
|
+
data: {
|
|
288
|
+
painId,
|
|
289
|
+
painType: 'user_frustration',
|
|
290
|
+
source: event.toolName,
|
|
291
|
+
reason: `User intervention: ${reason}`,
|
|
292
|
+
score: 100,
|
|
293
|
+
sessionId,
|
|
294
|
+
traceId,
|
|
295
|
+
agentId: ctx.agentId,
|
|
296
|
+
provenance: (sessionId && sessionId !== 'unknown') ? 'openclaw_context_bound' : 'owner_reported_no_host_trace',
|
|
297
|
+
evidence: buildTrajectoryEvidence(wctx, sessionId),
|
|
298
|
+
},
|
|
299
|
+
}, { recordObservability: false });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
SystemLogger.log(workspaceDir, 'MANUAL_PAIN_SKIPPED', triggerDecision.reason);
|
|
303
|
+
}
|
|
284
304
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
painType: 'user_frustration',
|
|
291
|
-
source: event.toolName,
|
|
292
|
-
reason: `User intervention: ${reason}`,
|
|
305
|
+
else {
|
|
306
|
+
// PRI-454: Gate A path (rollback when either flag is OFF)
|
|
307
|
+
const session = getSession(sessionId);
|
|
308
|
+
const gate = evaluatePainDiagnosticGate({
|
|
309
|
+
source: 'manual',
|
|
293
310
|
score: 100,
|
|
311
|
+
currentGfi: session?.currentGfi ?? 0,
|
|
294
312
|
sessionId,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
313
|
+
});
|
|
314
|
+
if (!gate.shouldDiagnose) {
|
|
315
|
+
SystemLogger.log(workspaceDir, 'MANUAL_PAIN_SKIPPED', `Manual pain within cooldown: ${gate.detail}`);
|
|
316
|
+
let payload;
|
|
317
|
+
try {
|
|
318
|
+
payload = JSON.stringify({
|
|
319
|
+
reason: gate.reason,
|
|
320
|
+
detail: gate.detail,
|
|
321
|
+
source: 'manual',
|
|
322
|
+
sessionId,
|
|
323
|
+
gfi: 0,
|
|
324
|
+
score: 100,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
SystemLogger.log(workspaceDir, 'PAYLOAD_SERIALIZE_FAILED', String(e));
|
|
329
|
+
payload = JSON.stringify({ reason: gate.reason, detail: '(log serialization failed)' });
|
|
330
|
+
}
|
|
331
|
+
SystemLogger.log(workspaceDir, 'PAIN_GATE_REJECTED', payload);
|
|
332
|
+
// PEAT-B2: Record trigger decision even when cooldown blocks manual pain
|
|
333
|
+
const painTriageFlag = loadPdConfigForPlugin(workspaceDir);
|
|
334
|
+
if (painTriageFlag.effective) {
|
|
335
|
+
SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
|
|
336
|
+
outcome: 'cooldown_skipped',
|
|
337
|
+
sourceKind: 'owner_reported',
|
|
338
|
+
reason: `Manual pain within cooldown: ${gate.detail}`,
|
|
339
|
+
nextAction: 'wait_for_cooldown_or_manual_retrigger',
|
|
340
|
+
isOwnerManual: true,
|
|
341
|
+
sessionId,
|
|
342
|
+
score: 100,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
emitPainDetectedEvent(wctx, {
|
|
348
|
+
ts: new Date().toISOString(),
|
|
349
|
+
type: 'pain_detected',
|
|
350
|
+
data: {
|
|
351
|
+
painId,
|
|
352
|
+
painType: 'user_frustration',
|
|
353
|
+
source: event.toolName,
|
|
354
|
+
reason: `User intervention: ${reason}`,
|
|
355
|
+
score: 100,
|
|
356
|
+
sessionId,
|
|
357
|
+
traceId,
|
|
358
|
+
agentId: ctx.agentId,
|
|
359
|
+
provenance: (sessionId && sessionId !== 'unknown') ? 'openclaw_context_bound' : 'owner_reported_no_host_trace',
|
|
360
|
+
evidence: buildTrajectoryEvidence(wctx, sessionId),
|
|
361
|
+
},
|
|
362
|
+
}, { recordObservability: false });
|
|
363
|
+
}
|
|
301
364
|
}
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -15,6 +15,11 @@ import { matchEmpathyKeywords, loadKeywordStore, saveKeywordStore, getKeywordSto
|
|
|
15
15
|
import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
|
|
16
16
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
17
17
|
import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
|
|
18
|
+
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
19
|
+
import { isSharedCooldownActive, markSharedEpisodeAsDiagnosed } from './trigger-cooldown-tracker.js';
|
|
20
|
+
import { buildEmpathyObservation, resolveSourceKind } from './raw-observation-adapter.js';
|
|
21
|
+
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
22
|
+
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
18
23
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
19
24
|
import { detectCorrectionCue as coreDetectCorrectionCue, escapeXml, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
|
|
20
25
|
import { sanitizeForEvidence } from './message-sanitize.js';
|
|
@@ -310,19 +315,11 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
310
315
|
if (currentGfi >= highGfiThreshold) {
|
|
311
316
|
const gfiPainScore = Math.min(Math.round(currentGfi), 60);
|
|
312
317
|
logger?.info?.(`[PD:Empathy] GFI-TRIGGERED: currentGfi=${currentGfi.toFixed(1)} >= highGfi=${highGfiThreshold}, emitting pain signal (score=${gfiPainScore})`);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
sessionId,
|
|
319
|
-
errorHash: 'empathy_gfi_threshold',
|
|
320
|
-
thresholds: {
|
|
321
|
-
painTrigger,
|
|
322
|
-
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
323
|
-
semanticPain: Math.max(painTrigger, 60),
|
|
324
|
-
},
|
|
325
|
-
});
|
|
318
|
+
// PRI-454: Dual-gate migration. When both flags ON → Gate B (TriggerController).
|
|
319
|
+
// When either OFF → Gate A (PainDiagnosticGate, rollback).
|
|
320
|
+
const triageFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmission');
|
|
321
|
+
const defaultFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmissionDefault');
|
|
322
|
+
const useGateB = triageFlag.enabled && defaultFlag.enabled;
|
|
326
323
|
wctx.eventLog.recordPainSignal(sessionId, {
|
|
327
324
|
score: gfiPainScore,
|
|
328
325
|
source: 'user_empathy',
|
|
@@ -350,34 +347,104 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
350
347
|
origin: 'system_infer',
|
|
351
348
|
canonicalPainId: gfiPainId,
|
|
352
349
|
});
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
365
|
-
score: gfiPainScore,
|
|
366
|
-
sessionId,
|
|
367
|
-
agentId: 'main',
|
|
368
|
-
provenance: 'openclaw_context_bound',
|
|
369
|
-
evidence,
|
|
370
|
-
},
|
|
371
|
-
}, { recordObservability: false });
|
|
372
|
-
logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
|
|
350
|
+
if (useGateB) {
|
|
351
|
+
// PRI-454: Gate B path — TriggerController owns admission
|
|
352
|
+
const rawObs = buildEmpathyObservation({
|
|
353
|
+
detectionSource: 'user_empathy',
|
|
354
|
+
isGfiTriggered: true,
|
|
355
|
+
sessionId,
|
|
356
|
+
});
|
|
357
|
+
const sourceKind = resolveSourceKind(rawObs);
|
|
358
|
+
const triage = evaluateEvidenceTriage(sourceKind, gfiPainScore);
|
|
359
|
+
if (triage.decision !== 'admit') {
|
|
360
|
+
logger?.info?.(`[PD:Empathy] Triage ${triage.decision}: ${triage.reason}`);
|
|
373
361
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
362
|
+
else {
|
|
363
|
+
const cooldownActive = isSharedCooldownActive(sourceKind, sessionId, 'empathy_gfi_threshold');
|
|
364
|
+
const triggerDecision = evaluateTriggerController({
|
|
365
|
+
triageResult: triage,
|
|
366
|
+
isOwnerManual: false,
|
|
367
|
+
isCooldownActive: cooldownActive,
|
|
368
|
+
isValid: true,
|
|
369
|
+
score: gfiPainScore,
|
|
370
|
+
sessionId,
|
|
371
|
+
});
|
|
372
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
373
|
+
markSharedEpisodeAsDiagnosed(sourceKind, sessionId, 'empathy_gfi_threshold');
|
|
374
|
+
logger?.info?.(`[PD:Empathy] Gate B approved, calling emitPainDetectedEvent...`);
|
|
375
|
+
try {
|
|
376
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
377
|
+
await emitPainDetectedEvent(wctx, {
|
|
378
|
+
ts: new Date().toISOString(),
|
|
379
|
+
type: 'pain_detected',
|
|
380
|
+
data: {
|
|
381
|
+
painId: gfiPainId,
|
|
382
|
+
painType: 'user_frustration',
|
|
383
|
+
source: 'user_empathy',
|
|
384
|
+
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
385
|
+
score: gfiPainScore,
|
|
386
|
+
sessionId,
|
|
387
|
+
agentId: 'main',
|
|
388
|
+
provenance: 'openclaw_context_bound',
|
|
389
|
+
evidence,
|
|
390
|
+
},
|
|
391
|
+
}, { recordObservability: false });
|
|
392
|
+
logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
|
|
393
|
+
}
|
|
394
|
+
catch (emitErr) {
|
|
395
|
+
console.error(`[PD:Empathy] FAILED to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
396
|
+
logger?.warn?.(`[PD:Empathy] Failed to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
logger?.info?.(`[PD:Empathy] Gate B skipped: ${triggerDecision.reason}`);
|
|
401
|
+
}
|
|
377
402
|
}
|
|
378
403
|
}
|
|
379
404
|
else {
|
|
380
|
-
|
|
405
|
+
// PRI-454: Gate A path (rollback when either flag is OFF)
|
|
406
|
+
const gate = evaluatePainDiagnosticGate({
|
|
407
|
+
source: 'user_empathy',
|
|
408
|
+
score: gfiPainScore,
|
|
409
|
+
currentGfi,
|
|
410
|
+
consecutiveErrors: currentSession?.consecutiveErrors ?? 0,
|
|
411
|
+
sessionId,
|
|
412
|
+
errorHash: 'empathy_gfi_threshold',
|
|
413
|
+
thresholds: {
|
|
414
|
+
painTrigger,
|
|
415
|
+
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
416
|
+
semanticPain: Math.max(painTrigger, 60),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
if (gate.shouldDiagnose) {
|
|
420
|
+
logger?.info?.(`[PD:Empathy] Gate approved, calling emitPainDetectedEvent...`);
|
|
421
|
+
try {
|
|
422
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
423
|
+
await emitPainDetectedEvent(wctx, {
|
|
424
|
+
ts: new Date().toISOString(),
|
|
425
|
+
type: 'pain_detected',
|
|
426
|
+
data: {
|
|
427
|
+
painId: gfiPainId,
|
|
428
|
+
painType: 'user_frustration',
|
|
429
|
+
source: 'user_empathy',
|
|
430
|
+
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
431
|
+
score: gfiPainScore,
|
|
432
|
+
sessionId,
|
|
433
|
+
agentId: 'main',
|
|
434
|
+
provenance: 'openclaw_context_bound',
|
|
435
|
+
evidence,
|
|
436
|
+
},
|
|
437
|
+
}, { recordObservability: false });
|
|
438
|
+
logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
|
|
439
|
+
}
|
|
440
|
+
catch (emitErr) {
|
|
441
|
+
console.error(`[PD:Empathy] FAILED to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
442
|
+
logger?.warn?.(`[PD:Empathy] Failed to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
logger?.info?.(`[PD:Empathy] GFI-triggered gate rejected: ${gate.detail}`);
|
|
447
|
+
}
|
|
381
448
|
}
|
|
382
449
|
}
|
|
383
450
|
// Trigger asynchronous background Empathy Observer deep analysis (Zero Latency)
|
|
@@ -438,45 +505,109 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
438
505
|
const freshGfi = freshSession?.currentGfi ?? 0;
|
|
439
506
|
if (freshGfi >= highGfiThreshold) {
|
|
440
507
|
const freshGfiPainScore = Math.min(Math.round(freshGfi), 60);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
508
|
+
// PRI-454: Dual-gate migration. When both flags ON → Gate B (TriggerController).
|
|
509
|
+
// When either OFF → Gate A (PainDiagnosticGate, rollback).
|
|
510
|
+
const triageFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmission');
|
|
511
|
+
const defaultFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmissionDefault');
|
|
512
|
+
const useGateB = triageFlag.enabled && defaultFlag.enabled;
|
|
513
|
+
if (useGateB) {
|
|
514
|
+
// PRI-454: Gate B path — TriggerController owns admission
|
|
515
|
+
const rawObs = buildEmpathyObservation({
|
|
516
|
+
detectionSource: 'user_empathy',
|
|
517
|
+
isGfiTriggered: true,
|
|
518
|
+
sessionId,
|
|
519
|
+
});
|
|
520
|
+
const sourceKind = resolveSourceKind(rawObs);
|
|
521
|
+
const triage = evaluateEvidenceTriage(sourceKind, freshGfiPainScore);
|
|
522
|
+
if (triage.decision !== 'admit') {
|
|
523
|
+
logger?.info?.(`[PD:Empathy] Observer triage ${triage.decision}: ${triage.reason}`);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const cooldownActive = isSharedCooldownActive(sourceKind, sessionId, 'empathy_gfi_observer');
|
|
527
|
+
const triggerDecision = evaluateTriggerController({
|
|
528
|
+
triageResult: triage,
|
|
529
|
+
isOwnerManual: false,
|
|
530
|
+
isCooldownActive: cooldownActive,
|
|
531
|
+
isValid: true,
|
|
532
|
+
score: freshGfiPainScore,
|
|
533
|
+
sessionId,
|
|
534
|
+
});
|
|
535
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
536
|
+
markSharedEpisodeAsDiagnosed(sourceKind, sessionId, 'empathy_gfi_observer');
|
|
537
|
+
logger?.info?.(`[PD:Empathy] Gate B approved (observer), calling emitPainDetectedEvent...`);
|
|
538
|
+
try {
|
|
539
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
540
|
+
// PRI-453: Reuse observerPainId for lineage consistency —
|
|
541
|
+
// the same id is used as canonicalPainId in recordPainEvent
|
|
542
|
+
// and as painId in the emitted event, so EvidenceChainConsoleModel
|
|
543
|
+
// can JOIN pain_events.canonical_pain_id = tasks.input_ref.
|
|
544
|
+
await emitPainDetectedEvent(wctx, {
|
|
545
|
+
ts: new Date().toISOString(),
|
|
546
|
+
type: 'pain_detected',
|
|
547
|
+
data: {
|
|
548
|
+
painId: observerPainId,
|
|
549
|
+
painType: 'user_frustration',
|
|
550
|
+
source: 'user_empathy',
|
|
551
|
+
reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
|
|
552
|
+
score: freshGfiPainScore,
|
|
553
|
+
sessionId,
|
|
554
|
+
agentId: 'main',
|
|
555
|
+
provenance: 'openclaw_context_bound',
|
|
556
|
+
evidence,
|
|
557
|
+
},
|
|
558
|
+
}, { recordObservability: false });
|
|
559
|
+
}
|
|
560
|
+
catch (emitErr) {
|
|
561
|
+
logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
logger?.info?.(`[PD:Empathy] Gate B skipped (observer): ${triggerDecision.reason}`);
|
|
566
|
+
}
|
|
477
567
|
}
|
|
478
|
-
|
|
479
|
-
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// PRI-454: Gate A path (rollback when either flag is OFF)
|
|
571
|
+
const gate = evaluatePainDiagnosticGate({
|
|
572
|
+
source: 'user_empathy',
|
|
573
|
+
score: freshGfiPainScore,
|
|
574
|
+
currentGfi: freshGfi,
|
|
575
|
+
consecutiveErrors: freshSession?.consecutiveErrors ?? 0,
|
|
576
|
+
sessionId,
|
|
577
|
+
errorHash: 'empathy_gfi_threshold',
|
|
578
|
+
thresholds: {
|
|
579
|
+
painTrigger,
|
|
580
|
+
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
581
|
+
semanticPain: Math.max(painTrigger, 60),
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
if (gate.shouldDiagnose) {
|
|
585
|
+
logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
|
|
586
|
+
try {
|
|
587
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
588
|
+
// PRI-453: Reuse observerPainId for lineage consistency —
|
|
589
|
+
// the same id is used as canonicalPainId in recordPainEvent
|
|
590
|
+
// and as painId in the emitted event, so EvidenceChainConsoleModel
|
|
591
|
+
// can JOIN pain_events.canonical_pain_id = tasks.input_ref.
|
|
592
|
+
await emitPainDetectedEvent(wctx, {
|
|
593
|
+
ts: new Date().toISOString(),
|
|
594
|
+
type: 'pain_detected',
|
|
595
|
+
data: {
|
|
596
|
+
painId: observerPainId,
|
|
597
|
+
painType: 'user_frustration',
|
|
598
|
+
source: 'user_empathy',
|
|
599
|
+
reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
|
|
600
|
+
score: freshGfiPainScore,
|
|
601
|
+
sessionId,
|
|
602
|
+
agentId: 'main',
|
|
603
|
+
provenance: 'openclaw_context_bound',
|
|
604
|
+
evidence,
|
|
605
|
+
},
|
|
606
|
+
}, { recordObservability: false });
|
|
607
|
+
}
|
|
608
|
+
catch (emitErr) {
|
|
609
|
+
logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
|
|
610
|
+
}
|
|
480
611
|
}
|
|
481
612
|
}
|
|
482
613
|
}
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
* ERR checklist:
|
|
13
13
|
* - ERR-011: re-export adapter, not a local re-definition of migrated logic.
|
|
14
14
|
*/
|
|
15
|
-
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, } from '@principles/core/runtime-v2';
|
|
15
|
+
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, buildEmpathyObservation, buildManualPainObservation, } from '@principles/core/runtime-v2';
|
|
16
16
|
export type { RawObservation } from './raw-observation-types.js';
|
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* ERR checklist:
|
|
13
13
|
* - ERR-011: re-export adapter, not a local re-definition of migrated logic.
|
|
14
14
|
*/
|
|
15
|
-
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, } from '@principles/core/runtime-v2';
|
|
15
|
+
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, buildEmpathyObservation, buildManualPainObservation, } from '@principles/core/runtime-v2';
|
|
@@ -28,3 +28,15 @@ export declare function clearCooldownState(cooldownMap: Map<string, number>): vo
|
|
|
28
28
|
* Get the cooldown timestamp for a given episode (for tests).
|
|
29
29
|
*/
|
|
30
30
|
export declare function getCooldownTimestamp(source: string, sessionId: string | undefined, errorHash: string | undefined, cooldownMap: ReadonlyMap<string, number>): number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Check whether cooldown is currently active using the shared Map.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isSharedCooldownActive(source: string, sessionId: string | undefined, errorHash: string | undefined): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Mark an episode as diagnosed using the shared Map.
|
|
37
|
+
*/
|
|
38
|
+
export declare function markSharedEpisodeAsDiagnosed(source: string, sessionId: string | undefined, errorHash: string | undefined): void;
|
|
39
|
+
/**
|
|
40
|
+
* Clear all shared cooldown state (for tests).
|
|
41
|
+
*/
|
|
42
|
+
export declare function resetSharedCooldownForTest(): void;
|
|
@@ -53,3 +53,28 @@ export function getCooldownTimestamp(source, sessionId, errorHash, cooldownMap)
|
|
|
53
53
|
const episodeKey = buildEpisodeKey(source, sessionId, errorHash);
|
|
54
54
|
return cooldownMap.get(episodeKey);
|
|
55
55
|
}
|
|
56
|
+
// ── Shared Cooldown Map (PRI-454) ───────────────────────────────────────────
|
|
57
|
+
//
|
|
58
|
+
// Module-level singleton Map shared across all Gate B paths.
|
|
59
|
+
// Replaces the per-file TRIGGER_COOLDOWN_MAP in after-tool-call-helpers.ts.
|
|
60
|
+
// All paths (gate-block, llm, empathy, manual pain) use this single Map
|
|
61
|
+
// to ensure unified cooldown state.
|
|
62
|
+
const SHARED_COOLDOWN_MAP = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Check whether cooldown is currently active using the shared Map.
|
|
65
|
+
*/
|
|
66
|
+
export function isSharedCooldownActive(source, sessionId, errorHash) {
|
|
67
|
+
return isCooldownActive(source, sessionId, errorHash, SHARED_COOLDOWN_MAP);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Mark an episode as diagnosed using the shared Map.
|
|
71
|
+
*/
|
|
72
|
+
export function markSharedEpisodeAsDiagnosed(source, sessionId, errorHash) {
|
|
73
|
+
markEpisodeAsDiagnosed(source, sessionId, errorHash, SHARED_COOLDOWN_MAP);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clear all shared cooldown state (for tests).
|
|
77
|
+
*/
|
|
78
|
+
export function resetSharedCooldownForTest() {
|
|
79
|
+
SHARED_COOLDOWN_MAP.clear();
|
|
80
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.146.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pd-cli-operator
|
|
3
|
-
description: Use this skill when operating the Principles Disciple `pd` CLI: probing runtime health, manually recording pain, inspecting tasks/runs/candidates/artifacts, intaking candidates, viewing flow policy, or
|
|
3
|
+
description: Use this skill when operating the Principles Disciple `pd` CLI: probing runtime health, manually recording pain, inspecting tasks/runs/candidates/artifacts, intaking candidates, viewing flow policy, or tracing pain-to-ledger chains. It routes CLI commands safely and forbids direct `.state/.pain_flag` writes.
|
|
4
4
|
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -36,6 +36,25 @@ Flow policy:
|
|
|
36
36
|
pd runtime flow show --workspace "<workspace>" --json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Trace Pain-To-Ledger Chain (PRI-455 promoted)
|
|
40
|
+
|
|
41
|
+
Trace the full evidence chain from pain signal to principle ledger:
|
|
42
|
+
```bash
|
|
43
|
+
pd trace show --pain-id "<painId>" --workspace "<workspace>" --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Activation Management (PRI-455 promoted)
|
|
47
|
+
|
|
48
|
+
List active activations:
|
|
49
|
+
```bash
|
|
50
|
+
pd activation list --workspace "<workspace>" --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Deactivate (rollback) an activation:
|
|
54
|
+
```bash
|
|
55
|
+
pd activation deactivate --activation-id "<activationId>" --workspace "<workspace>" --json
|
|
56
|
+
```
|
|
57
|
+
|
|
39
58
|
## Inspect Runtime V2 Objects
|
|
40
59
|
|
|
41
60
|
Tasks and runs:
|
|
@@ -55,13 +74,3 @@ Manual intake:
|
|
|
55
74
|
```bash
|
|
56
75
|
pd candidate intake --candidate-id "<candidateId>" --workspace "<workspace>" --json
|
|
57
76
|
```
|
|
58
|
-
|
|
59
|
-
## Legacy Admin
|
|
60
|
-
|
|
61
|
-
Only when explicitly cleaning old state:
|
|
62
|
-
```bash
|
|
63
|
-
pd legacy cleanup --workspace "<workspace>" --dry-run
|
|
64
|
-
pd legacy cleanup --workspace "<workspace>" --apply
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Never use legacy cleanup as a diagnosis trigger.
|
|
@@ -38,6 +38,16 @@ Flow policy:
|
|
|
38
38
|
pd runtime flow show --workspace "<workspace>" --json
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
Trace pain-to-ledger chain (PRI-455 promoted):
|
|
42
|
+
```bash
|
|
43
|
+
pd trace show --pain-id "<painId>" --workspace "<workspace>" --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Activation status (PRI-455 promoted):
|
|
47
|
+
```bash
|
|
48
|
+
pd activation list --workspace "<workspace>" --json
|
|
49
|
+
```
|
|
50
|
+
|
|
41
51
|
Candidates:
|
|
42
52
|
```bash
|
|
43
53
|
pd candidate list --workspace "<workspace>" --json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pd-cli-operator
|
|
3
|
-
description: 操作 Principles Disciple `pd` CLI 时使用:运行时健康检查、手动记录痛苦、查看 task/run/candidate/artifact、intake candidate、查看 flow policy
|
|
3
|
+
description: 操作 Principles Disciple `pd` CLI 时使用:运行时健康检查、手动记录痛苦、查看 task/run/candidate/artifact、intake candidate、查看 flow policy、追踪痛苦到原则的证据链。本技能提供安全 CLI 路由,并禁止直接写 `.state/.pain_flag`。
|
|
4
4
|
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -36,6 +36,25 @@ pd runtime probe --runtime pi-ai --workspace "<workspace>" --json
|
|
|
36
36
|
pd runtime flow show --workspace "<workspace>" --json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## 追踪痛苦到原则链(PRI-455 提升)
|
|
40
|
+
|
|
41
|
+
追踪从痛苦信号到原则账本的完整证据链:
|
|
42
|
+
```bash
|
|
43
|
+
pd trace show --pain-id "<painId>" --workspace "<workspace>" --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 激活管理(PRI-455 提升)
|
|
47
|
+
|
|
48
|
+
列出当前激活:
|
|
49
|
+
```bash
|
|
50
|
+
pd activation list --workspace "<workspace>" --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
停用(回滚)激活:
|
|
54
|
+
```bash
|
|
55
|
+
pd activation deactivate --activation-id "<activationId>" --workspace "<workspace>" --json
|
|
56
|
+
```
|
|
57
|
+
|
|
39
58
|
## 查看 Runtime V2 对象
|
|
40
59
|
|
|
41
60
|
Tasks 和 runs:
|
|
@@ -55,13 +74,3 @@ pd artifact show --artifact-id "<artifactId>" --workspace "<workspace>" --json
|
|
|
55
74
|
```bash
|
|
56
75
|
pd candidate intake --candidate-id "<candidateId>" --workspace "<workspace>" --json
|
|
57
76
|
```
|
|
58
|
-
|
|
59
|
-
## Legacy 管理
|
|
60
|
-
|
|
61
|
-
只有在明确清理旧状态时使用:
|
|
62
|
-
```bash
|
|
63
|
-
pd legacy cleanup --workspace "<workspace>" --dry-run
|
|
64
|
-
pd legacy cleanup --workspace "<workspace>" --apply
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
不要把 legacy cleanup 当作诊断触发入口。
|
|
@@ -38,6 +38,16 @@ pd runtime probe --runtime pi-ai --workspace "<workspace>" --json
|
|
|
38
38
|
pd runtime flow show --workspace "<workspace>" --json
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
追踪痛苦到原则链(PRI-455 提升):
|
|
42
|
+
```bash
|
|
43
|
+
pd trace show --pain-id "<painId>" --workspace "<workspace>" --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
激活状态(PRI-455 提升):
|
|
47
|
+
```bash
|
|
48
|
+
pd activation list --workspace "<workspace>" --json
|
|
49
|
+
```
|
|
50
|
+
|
|
41
51
|
候选原则:
|
|
42
52
|
```bash
|
|
43
53
|
pd candidate list --workspace "<workspace>" --json
|