principles-disciple 1.7.8 → 1.8.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/core/config.d.ts +2 -0
- package/dist/core/session-tracker.d.ts +3 -1
- package/dist/core/session-tracker.js +12 -3
- package/dist/hooks/llm.d.ts +1 -0
- package/dist/hooks/llm.js +17 -70
- package/dist/hooks/progressive-trust-gate.d.ts +1 -0
- package/dist/hooks/progressive-trust-gate.js +45 -0
- package/dist/hooks/prompt.d.ts +2 -0
- package/dist/hooks/prompt.js +27 -6
- package/dist/hooks/subagent.js +2 -2
- package/dist/http/principles-console-route.js +114 -0
- package/dist/service/empathy-observer-manager.d.ts +46 -10
- package/dist/service/empathy-observer-manager.js +249 -64
- package/dist/service/evolution-worker.js +1 -0
- package/dist/service/health-query-service.d.ts +170 -0
- package/dist/service/health-query-service.js +662 -0
- package/dist/service/nocturnal-runtime.d.ts +2 -2
- package/dist/service/nocturnal-runtime.js +75 -4
- package/dist/service/nocturnal-service.js +2 -2
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.d.ts +48 -0
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.js +480 -0
- package/dist/service/subagent-workflow/index.d.ts +4 -0
- package/dist/service/subagent-workflow/index.js +3 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.d.ts +77 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.js +75 -0
- package/dist/service/subagent-workflow/types.d.ts +259 -0
- package/dist/service/subagent-workflow/types.js +11 -0
- package/dist/service/subagent-workflow/workflow-store.d.ts +26 -0
- package/dist/service/subagent-workflow/workflow-store.js +165 -0
- package/dist/tools/deep-reflect.js +2 -2
- package/openclaw.plugin.json +6 -1
- package/package.json +3 -3
package/dist/core/config.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface PainSettings {
|
|
|
78
78
|
deep_reflection?: DeepReflectionSettings;
|
|
79
79
|
empathy_engine?: {
|
|
80
80
|
enabled?: boolean;
|
|
81
|
+
/** Shadow mode: also run EmpathyObserverWorkflowManager alongside legacy path */
|
|
82
|
+
helper_empathy_enabled?: boolean;
|
|
81
83
|
dedupe_window_ms?: number;
|
|
82
84
|
penalties?: {
|
|
83
85
|
mild?: number;
|
|
@@ -8,6 +8,8 @@ export interface TokenUsage {
|
|
|
8
8
|
}
|
|
9
9
|
export interface SessionState {
|
|
10
10
|
sessionId: string;
|
|
11
|
+
sessionKey?: string;
|
|
12
|
+
trigger?: string;
|
|
11
13
|
workspaceDir?: string;
|
|
12
14
|
toolReadsByFile: Record<string, number>;
|
|
13
15
|
llmTurns: number;
|
|
@@ -40,7 +42,7 @@ export declare function initPersistence(stateDir: string): void;
|
|
|
40
42
|
*/
|
|
41
43
|
export declare function flushAllSessions(): void;
|
|
42
44
|
export declare function trackToolRead(sessionId: string, filePath: string, workspaceDir?: string): SessionState;
|
|
43
|
-
export declare function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined, config?: PainConfig, workspaceDir?: string): SessionState;
|
|
45
|
+
export declare function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined, config?: PainConfig, workspaceDir?: string, sessionKey?: string, trigger?: string): SessionState;
|
|
44
46
|
/**
|
|
45
47
|
* Tracks physical friction based on tool execution failures.
|
|
46
48
|
*/
|
|
@@ -111,11 +111,13 @@ export function flushAllSessions() {
|
|
|
111
111
|
persistSession(state);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
function getOrCreateSession(sessionId, workspaceDir) {
|
|
114
|
+
function getOrCreateSession(sessionId, workspaceDir, sessionKey, trigger) {
|
|
115
115
|
let state = sessions.get(sessionId);
|
|
116
116
|
if (!state) {
|
|
117
117
|
state = {
|
|
118
118
|
sessionId,
|
|
119
|
+
sessionKey,
|
|
120
|
+
trigger,
|
|
119
121
|
workspaceDir,
|
|
120
122
|
toolReadsByFile: {},
|
|
121
123
|
llmTurns: 0,
|
|
@@ -143,6 +145,13 @@ function getOrCreateSession(sessionId, workspaceDir) {
|
|
|
143
145
|
if (workspaceDir && !state.workspaceDir) {
|
|
144
146
|
state.workspaceDir = workspaceDir;
|
|
145
147
|
}
|
|
148
|
+
// Update sessionKey and trigger if provided (they may be more recent)
|
|
149
|
+
if (sessionKey && !state.sessionKey) {
|
|
150
|
+
state.sessionKey = sessionKey;
|
|
151
|
+
}
|
|
152
|
+
if (trigger && !state.trigger) {
|
|
153
|
+
state.trigger = trigger;
|
|
154
|
+
}
|
|
146
155
|
return state;
|
|
147
156
|
}
|
|
148
157
|
function ensureGfiLedger(state) {
|
|
@@ -158,8 +167,8 @@ export function trackToolRead(sessionId, filePath, workspaceDir) {
|
|
|
158
167
|
touchActivity(state);
|
|
159
168
|
return state;
|
|
160
169
|
}
|
|
161
|
-
export function trackLlmOutput(sessionId, usage, config, workspaceDir) {
|
|
162
|
-
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
170
|
+
export function trackLlmOutput(sessionId, usage, config, workspaceDir, sessionKey, trigger) {
|
|
171
|
+
const state = getOrCreateSession(sessionId, workspaceDir, sessionKey, trigger);
|
|
163
172
|
state.llmTurns += 1;
|
|
164
173
|
touchActivity(state);
|
|
165
174
|
if (usage) {
|
package/dist/hooks/llm.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface EmpathySignal {
|
|
|
7
7
|
mode?: 'structured' | 'legacy_tag';
|
|
8
8
|
}
|
|
9
9
|
export declare function extractEmpathySignal(text: string): EmpathySignal;
|
|
10
|
+
export declare function isEmpathyAuditPayload(text: string): boolean;
|
|
10
11
|
export declare function handleLlmOutput(event: PluginHookLlmOutputEvent, ctx: PluginHookAgentContext & {
|
|
11
12
|
workspaceDir?: string;
|
|
12
13
|
}): void;
|
package/dist/hooks/llm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { trackLlmOutput, recordThinkingCheckpoint, resetFriction } from '../core/session-tracker.js';
|
|
4
4
|
import { writePainFlag } from '../core/pain.js';
|
|
5
5
|
import { ControlUiDatabase } from '../core/control-ui-db.js';
|
|
6
6
|
import { DetectionService } from '../core/detection-service.js';
|
|
@@ -175,6 +175,18 @@ function applyRateLimit(sessionId, runId, score, config) {
|
|
|
175
175
|
empathyRateState.set(sessionId, prev);
|
|
176
176
|
return allowed;
|
|
177
177
|
}
|
|
178
|
+
export function isEmpathyAuditPayload(text) {
|
|
179
|
+
if (!text || typeof text !== 'string')
|
|
180
|
+
return false;
|
|
181
|
+
const trimmed = text.trim();
|
|
182
|
+
if (/^\{[\s\S]*"damageDetected"[\s\S]*\}$/.test(trimmed))
|
|
183
|
+
return true;
|
|
184
|
+
if (/^<empathy\s+([^>]*)\/?>/i.test(trimmed))
|
|
185
|
+
return true;
|
|
186
|
+
if (/^\s*\[EMOTIONAL_DAMAGE_DETECTED(?::(mild|moderate|severe))?\]\s*$/i.test(trimmed))
|
|
187
|
+
return true;
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
178
190
|
export function handleLlmOutput(event, ctx) {
|
|
179
191
|
if (!ctx.workspaceDir || !ctx.sessionId)
|
|
180
192
|
return;
|
|
@@ -182,7 +194,7 @@ export function handleLlmOutput(event, ctx) {
|
|
|
182
194
|
const config = wctx.config;
|
|
183
195
|
const eventLog = wctx.eventLog;
|
|
184
196
|
// Track this turn in the core session memory
|
|
185
|
-
const state = trackLlmOutput(ctx.sessionId, event.usage, config, ctx.workspaceDir);
|
|
197
|
+
const state = trackLlmOutput(ctx.sessionId, event.usage, config, ctx.workspaceDir, ctx.sessionKey, ctx.trigger);
|
|
186
198
|
// We need actual assistant text to analyze
|
|
187
199
|
if (!event.assistantTexts || event.assistantTexts.length === 0)
|
|
188
200
|
return;
|
|
@@ -207,14 +219,15 @@ export function handleLlmOutput(event, ctx) {
|
|
|
207
219
|
ctx.logger?.warn?.(`[PD:LLM] Failed to persist assistant turn to trajectory: ${String(error)}`);
|
|
208
220
|
}
|
|
209
221
|
// ── Track B: Semantic Pain Detection (V1.3.0 Funnel) ──
|
|
222
|
+
const detectionText = isEmpathyAuditPayload(text) ? '' : text;
|
|
210
223
|
const detectionService = DetectionService.get(wctx.stateDir);
|
|
211
|
-
const detection = detectionService.detect(
|
|
224
|
+
const detection = detectionService.detect(detectionText);
|
|
212
225
|
if (detection.detected) {
|
|
213
226
|
eventLog.recordRuleMatch(ctx.sessionId, {
|
|
214
227
|
ruleId: detection.ruleId || detection.source,
|
|
215
228
|
layer: detection.source === 'l1_exact' ? 'L1' : (detection.source === 'l2_cache' ? 'L2' : 'L3'),
|
|
216
229
|
severity: detection.severity || 0,
|
|
217
|
-
textPreview:
|
|
230
|
+
textPreview: detectionText.substring(0, 100)
|
|
218
231
|
});
|
|
219
232
|
}
|
|
220
233
|
let painScore = detection.detected ? (detection.severity || 0) : 0;
|
|
@@ -224,73 +237,7 @@ export function handleLlmOutput(event, ctx) {
|
|
|
224
237
|
let matchedReason = detection.detected
|
|
225
238
|
? `Agent triggered pain detection (Source: ${detection.source}${detection.ruleId ? `, Rule: ${detection.ruleId}` : ''})`
|
|
226
239
|
: '';
|
|
227
|
-
// empathy sub-pipeline (enabled by default)
|
|
228
|
-
const empathyEnabled = config.get('empathy_engine.enabled');
|
|
229
|
-
if (empathyEnabled !== false) {
|
|
230
|
-
if (signal.detected) {
|
|
231
|
-
const dedupeWindow = Number(config.get('empathy_engine.dedupe_window_ms') ?? 60000);
|
|
232
|
-
const deduped = shouldDedupe(ctx.sessionId, event.runId, signal, dedupeWindow);
|
|
233
|
-
if (!deduped) {
|
|
234
|
-
const baseScore = mapSeverityToPenalty(signal.severity, config);
|
|
235
|
-
const weightedScore = Math.round(baseScore * signal.confidence);
|
|
236
|
-
const calibrationFactor = resolveCalibrationFactor(event, config);
|
|
237
|
-
const calibratedScore = Math.round(weightedScore * calibrationFactor);
|
|
238
|
-
const boundedScore = applyRateLimit(ctx.sessionId, event.runId, calibratedScore, config);
|
|
239
|
-
if (boundedScore > 0) {
|
|
240
|
-
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir, { source: 'user_empathy' });
|
|
241
|
-
try {
|
|
242
|
-
wctx.trajectory?.recordPainEvent?.({
|
|
243
|
-
sessionId: ctx.sessionId,
|
|
244
|
-
source: 'user_empathy',
|
|
245
|
-
score: boundedScore,
|
|
246
|
-
reason: signal.reason || 'Assistant self-reported user emotional distress.',
|
|
247
|
-
severity: signal.severity,
|
|
248
|
-
origin: 'assistant_self_report',
|
|
249
|
-
confidence: signal.confidence,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
ctx.logger?.warn?.(`[PD:LLM] Failed to persist empathy pain event to trajectory: ${String(error)}`);
|
|
254
|
-
}
|
|
255
|
-
// Generate unique event ID for rollback support
|
|
256
|
-
const eventId = `emp_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
257
|
-
eventLog.recordPainSignal(ctx.sessionId, {
|
|
258
|
-
score: boundedScore,
|
|
259
|
-
source: 'user_empathy',
|
|
260
|
-
reason: signal.reason || 'Assistant self-reported user emotional distress.',
|
|
261
|
-
isRisky: false,
|
|
262
|
-
origin: 'assistant_self_report',
|
|
263
|
-
severity: signal.severity,
|
|
264
|
-
confidence: signal.confidence,
|
|
265
|
-
detection_mode: signal.mode,
|
|
266
|
-
deduped: false,
|
|
267
|
-
trigger_text_excerpt: text.substring(0, 120),
|
|
268
|
-
raw_score: weightedScore,
|
|
269
|
-
calibrated_score: calibratedScore,
|
|
270
|
-
eventId,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
eventLog.recordPainSignal(ctx.sessionId, {
|
|
276
|
-
score: 0,
|
|
277
|
-
source: 'user_empathy',
|
|
278
|
-
reason: signal.reason || 'Deduped empathy signal.',
|
|
279
|
-
isRisky: false,
|
|
280
|
-
origin: 'assistant_self_report',
|
|
281
|
-
severity: signal.severity,
|
|
282
|
-
confidence: signal.confidence,
|
|
283
|
-
detection_mode: signal.mode,
|
|
284
|
-
deduped: true,
|
|
285
|
-
trigger_text_excerpt: text.substring(0, 120),
|
|
286
|
-
raw_score: Math.round(mapSeverityToPenalty(signal.severity, config) * signal.confidence),
|
|
287
|
-
calibrated_score: Math.round(mapSeverityToPenalty(signal.severity, config) * signal.confidence * resolveCalibrationFactor(event, config))
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
240
|
// ═══ Natural Language Rollback Detection ═══
|
|
293
|
-
// Detect [EMPATHY_ROLLBACK_REQUEST] tag and trigger rollback
|
|
294
241
|
const rollbackMatch = text.match(/^\s*\[EMPATHY_ROLLBACK_REQUEST\]\s*$/m);
|
|
295
242
|
if (rollbackMatch) {
|
|
296
243
|
const eventId = eventLog.getLastEmpathyEventId(ctx.sessionId);
|
|
@@ -22,6 +22,46 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
24
24
|
import { recordGateBlockAndReturn } from './gate-block-helper.js';
|
|
25
|
+
// ═══ P-16: Core Governance Files — Exempt from all Blocking ═══
|
|
26
|
+
// 这些文件是团队协作的基础,必须始终放行,不受 GFI 和 Risk Path 限制
|
|
27
|
+
// 可通过 PROFILE.core_governance_files 扩展(merge 而非覆盖)
|
|
28
|
+
const DEFAULT_CORE_GOVERNANCE_PATTERNS = [
|
|
29
|
+
'PLAN.md',
|
|
30
|
+
'AGENTS.md',
|
|
31
|
+
'VERSION.md',
|
|
32
|
+
'.team/',
|
|
33
|
+
'MEMORY.md',
|
|
34
|
+
'SOUL.md',
|
|
35
|
+
'IDENTITY.md',
|
|
36
|
+
'USER.md',
|
|
37
|
+
'HEARTBEAT.md',
|
|
38
|
+
'BOOTSTRAP.md',
|
|
39
|
+
'PRINCIPLES.md',
|
|
40
|
+
'TEAM_ROLE.md',
|
|
41
|
+
'REPAIR_OPERATING_PROMPT.md',
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Get effective core governance patterns from PROFILE config, merged with defaults.
|
|
45
|
+
* PROFILE.core_governance_files extends (not replaces) the default list.
|
|
46
|
+
*/
|
|
47
|
+
function getCoreGovernancePatterns(profile) {
|
|
48
|
+
const base = DEFAULT_CORE_GOVERNANCE_PATTERNS;
|
|
49
|
+
const extra = profile?.core_governance_files ?? [];
|
|
50
|
+
return Array.from(new Set([...base, ...extra]));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a file path matches a core governance pattern.
|
|
54
|
+
* Core governance files are exempt from all gate blocking (P-16).
|
|
55
|
+
*/
|
|
56
|
+
function isCoreGovernanceFile(filePath, corePatterns) {
|
|
57
|
+
if (!filePath)
|
|
58
|
+
return false;
|
|
59
|
+
const patterns = corePatterns ?? DEFAULT_CORE_GOVERNANCE_PATTERNS;
|
|
60
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
61
|
+
return patterns.some(pattern => pattern.endsWith('/')
|
|
62
|
+
? normalized.includes(pattern)
|
|
63
|
+
: normalized.endsWith(pattern) || normalized.includes(`/${pattern}`));
|
|
64
|
+
}
|
|
25
65
|
/**
|
|
26
66
|
* Build EP gate rejection reason
|
|
27
67
|
*/
|
|
@@ -54,6 +94,11 @@ function block(filePath, reason, wctx, toolName, logger, sessionId) {
|
|
|
54
94
|
* @returns PluginHookBeforeToolCallResult to block, or undefined to allow
|
|
55
95
|
*/
|
|
56
96
|
export function checkProgressiveTrustGate(event, wctx, relPath, risky, lineChanges, logger, ctx, profile) {
|
|
97
|
+
// P-16: Core governance files are exempt from all gate blocking
|
|
98
|
+
if (isCoreGovernanceFile(relPath, getCoreGovernancePatterns(profile))) {
|
|
99
|
+
logger.info?.(`[PD_GATE:P-16] Core governance file exempt — bypass all gates: ${relPath}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
57
102
|
// EP is the only gate now - use actual gate decision
|
|
58
103
|
if (!ctx.workspaceDir) {
|
|
59
104
|
logger.warn?.('[PD_GATE] No workspaceDir, skipping EP gate check');
|
package/dist/hooks/prompt.d.ts
CHANGED
package/dist/hooks/prompt.js
CHANGED
|
@@ -6,6 +6,7 @@ import { defaultContextConfig } from '../types.js';
|
|
|
6
6
|
import { classifyTask } from '../core/local-worker-routing.js';
|
|
7
7
|
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
|
|
8
8
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
9
|
+
import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec } from '../service/subagent-workflow/index.js';
|
|
9
10
|
import { PathResolver } from '../core/path-resolver.js';
|
|
10
11
|
/**
|
|
11
12
|
* OpenClaw API Prompt Hook
|
|
@@ -414,11 +415,13 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
414
415
|
- If you need self-inspection, prioritize the worker entry pointed by PathResolver key: EVOLUTION_WORKER
|
|
415
416
|
`;
|
|
416
417
|
// ──── 2. Evolution Directive (always on, highest priority) - stays in prependContext ────
|
|
417
|
-
// NOTE: active evolution task prompt is injected from EVOLUTION_QUEUE for active tasks
|
|
418
|
-
// NOT used for Phase 3 eligibility decisions
|
|
419
|
-
// EVOLUTION_DIRECTIVE.json is a compatibility-only display artifact
|
|
420
|
-
// Phase 3 eligibility uses only queue and evolution (see phase3-input-filter.ts)
|
|
421
418
|
let activeEvolutionTaskPrompt = '';
|
|
419
|
+
const empathySilenceConstraint = `
|
|
420
|
+
### 【EMPATHY OUTPUT RESTRICTION】
|
|
421
|
+
Do NOT output empathy diagnostic text in JSON, XML, or tag format.
|
|
422
|
+
Do NOT include "damageDetected", "severity", "confidence", or "empathy" fields in your output.
|
|
423
|
+
The empathy observer subagent handles pain detection independently.
|
|
424
|
+
`.trim();
|
|
422
425
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
423
426
|
if (fs.existsSync(queuePath)) {
|
|
424
427
|
try {
|
|
@@ -498,7 +501,9 @@ REQUIRED ACTION:
|
|
|
498
501
|
}
|
|
499
502
|
}
|
|
500
503
|
// Inject queue-derived evolution task at the front of prependContext
|
|
501
|
-
|
|
504
|
+
// Skip for minimal mode (heartbeat / subagent / observer sessions) to avoid
|
|
505
|
+
// polluting empathy observer prompts and other internal subagent sessions.
|
|
506
|
+
if (activeEvolutionTaskPrompt && !isMinimalMode) {
|
|
502
507
|
prependContext = activeEvolutionTaskPrompt + prependContext;
|
|
503
508
|
}
|
|
504
509
|
// ─────────────────────────────────────────────────4. Empathy Observer Spawn (async sidecar)
|
|
@@ -506,7 +511,23 @@ REQUIRED ACTION:
|
|
|
506
511
|
const latestUserMessage = extractLatestUserMessage(event.messages);
|
|
507
512
|
const isAgentToAgent = latestUserMessage.includes('sourceSession=agent:') || sessionId?.includes(':subagent:') === true;
|
|
508
513
|
if (trigger === 'user' && sessionId && api && !isAgentToAgent) {
|
|
509
|
-
|
|
514
|
+
prependContext = '### BEHAVIORAL_CONSTRAINTS\n' + empathySilenceConstraint + '\n\n' + prependContext;
|
|
515
|
+
empathyObserverManager.spawn(api, sessionId, latestUserMessage, workspaceDir).catch((err) => api.logger.warn(String(err)));
|
|
516
|
+
if (api.config?.empathy_engine?.helper_empathy_enabled === true && workspaceDir) {
|
|
517
|
+
// Cast required because SDK SubagentRunParams lacks expectsCompletionMessage
|
|
518
|
+
// which is supported by the actual OpenClaw runtime
|
|
519
|
+
const shadowManager = new EmpathyObserverWorkflowManager({
|
|
520
|
+
workspaceDir,
|
|
521
|
+
logger: api.logger,
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
523
|
+
subagent: api.runtime.subagent,
|
|
524
|
+
});
|
|
525
|
+
shadowManager.startWorkflow(empathyObserverWorkflowSpec, {
|
|
526
|
+
parentSessionId: sessionId,
|
|
527
|
+
workspaceDir,
|
|
528
|
+
taskInput: latestUserMessage,
|
|
529
|
+
}).catch((err) => api.logger.warn(`[PD:ShadowEmpathy] workflow failed: ${String(err)}`));
|
|
530
|
+
}
|
|
510
531
|
}
|
|
511
532
|
// ──── 5. Heartbeat-specific checklist ────
|
|
512
533
|
if (trigger === 'heartbeat') {
|
package/dist/hooks/subagent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { writePainFlag } from '../core/pain.js';
|
|
3
3
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
4
|
-
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
4
|
+
import { empathyObserverManager, isEmpathyObserverSession } from '../service/empathy-observer-manager.js';
|
|
5
5
|
import { acquireQueueLock } from '../service/evolution-worker.js';
|
|
6
6
|
import { recordEvolutionSuccess } from '../core/evolution-engine.js';
|
|
7
7
|
const COMPLETION_RETRY_DELAY_MS = 250;
|
|
@@ -125,7 +125,7 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
125
125
|
return;
|
|
126
126
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
127
127
|
const logger = ctx.api?.logger ?? console;
|
|
128
|
-
if (targetSessionKey
|
|
128
|
+
if (isEmpathyObserverSession(targetSessionKey || '')) {
|
|
129
129
|
await empathyObserverManager.reap(ctx.api, targetSessionKey, workspaceDir);
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ControlUiQueryService } from '../service/control-ui-query-service.js';
|
|
4
4
|
import { getEvolutionQueryService } from '../service/evolution-query-service.js';
|
|
5
|
+
import { HealthQueryService } from '../service/health-query-service.js';
|
|
5
6
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
6
7
|
import { getCentralDatabase } from '../service/central-database.js';
|
|
7
8
|
const ROUTE_PREFIX = '/plugins/principles';
|
|
@@ -406,6 +407,119 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
406
407
|
evoService.dispose();
|
|
407
408
|
}
|
|
408
409
|
}
|
|
410
|
+
// === Health Query API (v1.1 new endpoints) ===
|
|
411
|
+
const healthService = () => {
|
|
412
|
+
const workspaceDir = api.resolvePath('.');
|
|
413
|
+
return new HealthQueryService(workspaceDir);
|
|
414
|
+
};
|
|
415
|
+
if (pathname === `${API_PREFIX}/overview/health` && method === 'GET') {
|
|
416
|
+
const hs = healthService();
|
|
417
|
+
try {
|
|
418
|
+
json(res, 200, hs.getOverviewHealth());
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
api.logger.warn(`[PD:ControlUI] Health overview failed: ${String(error)}`);
|
|
423
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
hs.dispose();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (pathname === `${API_PREFIX}/evolution/principles` && method === 'GET') {
|
|
431
|
+
const hs = healthService();
|
|
432
|
+
try {
|
|
433
|
+
json(res, 200, hs.getEvolutionPrinciples());
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
api.logger.warn(`[PD:ControlUI] Evolution principles failed: ${String(error)}`);
|
|
438
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
hs.dispose();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (pathname === `${API_PREFIX}/feedback/gfi` && method === 'GET') {
|
|
446
|
+
const hs = healthService();
|
|
447
|
+
try {
|
|
448
|
+
json(res, 200, hs.getFeedbackGfi());
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
api.logger.warn(`[PD:ControlUI] Feedback GFI failed: ${String(error)}`);
|
|
453
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
hs.dispose();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (pathname === `${API_PREFIX}/feedback/empathy-events` && method === 'GET') {
|
|
461
|
+
const hs = healthService();
|
|
462
|
+
try {
|
|
463
|
+
const limit = url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined;
|
|
464
|
+
json(res, 200, hs.getFeedbackEmpathyEvents(limit));
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
api.logger.warn(`[PD:ControlUI] Feedback empathy events failed: ${String(error)}`);
|
|
469
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
hs.dispose();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (pathname === `${API_PREFIX}/feedback/gate-blocks` && method === 'GET') {
|
|
477
|
+
const hs = healthService();
|
|
478
|
+
try {
|
|
479
|
+
const limit = url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined;
|
|
480
|
+
json(res, 200, hs.getFeedbackGateBlocks(limit));
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
api.logger.warn(`[PD:ControlUI] Feedback gate blocks failed: ${String(error)}`);
|
|
485
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
hs.dispose();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (pathname === `${API_PREFIX}/gate/stats` && method === 'GET') {
|
|
493
|
+
const hs = healthService();
|
|
494
|
+
try {
|
|
495
|
+
json(res, 200, hs.getGateStats());
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
api.logger.warn(`[PD:ControlUI] Gate stats failed: ${String(error)}`);
|
|
500
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
hs.dispose();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (pathname === `${API_PREFIX}/gate/blocks` && method === 'GET') {
|
|
508
|
+
const hs = healthService();
|
|
509
|
+
try {
|
|
510
|
+
const limit = url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined;
|
|
511
|
+
json(res, 200, hs.getGateBlocks(limit));
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
api.logger.warn(`[PD:ControlUI] Gate blocks failed: ${String(error)}`);
|
|
516
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
hs.dispose();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
409
523
|
if (pathname === `${API_PREFIX}/export/corrections` && method === 'GET') {
|
|
410
524
|
try {
|
|
411
525
|
const mode = url.searchParams.get('mode') === 'redacted' ? 'redacted' : 'raw';
|
|
@@ -13,7 +13,15 @@ export interface EmpathyObserverApi {
|
|
|
13
13
|
lane?: string;
|
|
14
14
|
deliver?: boolean;
|
|
15
15
|
idempotencyKey?: string;
|
|
16
|
+
expectsCompletionMessage?: boolean;
|
|
16
17
|
}) => Promise<unknown>;
|
|
18
|
+
waitForRun: (params: {
|
|
19
|
+
runId: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
status: 'ok' | 'error' | 'timeout';
|
|
23
|
+
error?: string;
|
|
24
|
+
}>;
|
|
17
25
|
getSessionMessages: (params: {
|
|
18
26
|
sessionKey: string;
|
|
19
27
|
limit?: number;
|
|
@@ -21,6 +29,10 @@ export interface EmpathyObserverApi {
|
|
|
21
29
|
messages: unknown[];
|
|
22
30
|
assistantTexts?: string[];
|
|
23
31
|
}>;
|
|
32
|
+
deleteSession: (params: {
|
|
33
|
+
sessionKey: string;
|
|
34
|
+
deleteTranscript?: boolean;
|
|
35
|
+
}) => Promise<void>;
|
|
24
36
|
};
|
|
25
37
|
};
|
|
26
38
|
logger: PluginLogger;
|
|
@@ -28,21 +40,44 @@ export interface EmpathyObserverApi {
|
|
|
28
40
|
export declare class EmpathyObserverManager {
|
|
29
41
|
private static instance;
|
|
30
42
|
private sessionLocks;
|
|
43
|
+
private activeRuns;
|
|
44
|
+
private completedSessions;
|
|
31
45
|
private constructor();
|
|
32
46
|
static getInstance(): EmpathyObserverManager;
|
|
33
47
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* every method throws "only available during a gateway request".
|
|
37
|
-
* We cache the result to avoid repeated probing.
|
|
48
|
+
* Build a safe session key for empathy observer
|
|
49
|
+
* Format: agent:main:subagent:empathy-obs-{safeParentSessionId}-{timestamp}
|
|
38
50
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
buildEmpathyObserverSessionKey(parentSessionId: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Check if a session key is an empathy observer session
|
|
54
|
+
*/
|
|
55
|
+
isObserverSession(sessionKey: string): boolean;
|
|
56
|
+
private markCompleted;
|
|
57
|
+
private isCompleted;
|
|
58
|
+
private isActive;
|
|
59
|
+
private getActiveMetadata;
|
|
41
60
|
shouldTrigger(api: EmpathyObserverApi | null | undefined, sessionId: string): boolean;
|
|
42
|
-
spawn(api: EmpathyObserverApi | null | undefined, sessionId: string, userMessage: string): Promise<string | null>;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
spawn(api: EmpathyObserverApi | null | undefined, sessionId: string, userMessage: string, workspaceDir?: string): Promise<string | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Main回收链路: 使用 waitForRun 驱动回收
|
|
64
|
+
* 仅在 ok 时回收; timeout/exception 保留 session 由 fallback 处理
|
|
65
|
+
*/
|
|
66
|
+
private finalizeRun;
|
|
67
|
+
/**
|
|
68
|
+
* 统一回收入口: reap + deleteSession + 清理状态
|
|
69
|
+
*/
|
|
70
|
+
private reapBySession;
|
|
71
|
+
/**
|
|
72
|
+
* Fallback回收: 由 subagent_ended 触发
|
|
73
|
+
* 仅在主链路未处理时执行补救回收
|
|
74
|
+
*/
|
|
75
|
+
reap(api: EmpathyObserverApi | null | undefined, targetSessionKey: string, workspaceDir?: string): Promise<void>;
|
|
76
|
+
private cleanupState;
|
|
77
|
+
/**
|
|
78
|
+
* Extract parent session ID from observer session key
|
|
79
|
+
*/
|
|
80
|
+
extractParentSessionId(sessionKey: string): string | null;
|
|
46
81
|
private parseJsonPayload;
|
|
47
82
|
private extractAssistantText;
|
|
48
83
|
private scoreFromSeverity;
|
|
@@ -50,3 +85,4 @@ export declare class EmpathyObserverManager {
|
|
|
50
85
|
private normalizeConfidence;
|
|
51
86
|
}
|
|
52
87
|
export declare const empathyObserverManager: EmpathyObserverManager;
|
|
88
|
+
export declare function isEmpathyObserverSession(sessionKey: string): boolean;
|