principles-disciple 1.8.0 → 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.
Files changed (32) hide show
  1. package/dist/core/config.d.ts +2 -0
  2. package/dist/core/session-tracker.d.ts +3 -1
  3. package/dist/core/session-tracker.js +12 -3
  4. package/dist/hooks/llm.d.ts +1 -0
  5. package/dist/hooks/llm.js +17 -70
  6. package/dist/hooks/progressive-trust-gate.d.ts +1 -0
  7. package/dist/hooks/progressive-trust-gate.js +45 -0
  8. package/dist/hooks/prompt.d.ts +2 -0
  9. package/dist/hooks/prompt.js +27 -6
  10. package/dist/hooks/subagent.js +2 -2
  11. package/dist/http/principles-console-route.js +114 -0
  12. package/dist/service/empathy-observer-manager.d.ts +46 -10
  13. package/dist/service/empathy-observer-manager.js +249 -64
  14. package/dist/service/evolution-worker.js +1 -0
  15. package/dist/service/health-query-service.d.ts +170 -0
  16. package/dist/service/health-query-service.js +662 -0
  17. package/dist/service/nocturnal-runtime.d.ts +2 -2
  18. package/dist/service/nocturnal-runtime.js +75 -4
  19. package/dist/service/nocturnal-service.js +2 -2
  20. package/dist/service/subagent-workflow/empathy-observer-workflow-manager.d.ts +48 -0
  21. package/dist/service/subagent-workflow/empathy-observer-workflow-manager.js +480 -0
  22. package/dist/service/subagent-workflow/index.d.ts +4 -0
  23. package/dist/service/subagent-workflow/index.js +3 -0
  24. package/dist/service/subagent-workflow/runtime-direct-driver.d.ts +77 -0
  25. package/dist/service/subagent-workflow/runtime-direct-driver.js +75 -0
  26. package/dist/service/subagent-workflow/types.d.ts +259 -0
  27. package/dist/service/subagent-workflow/types.js +11 -0
  28. package/dist/service/subagent-workflow/workflow-store.d.ts +26 -0
  29. package/dist/service/subagent-workflow/workflow-store.js +165 -0
  30. package/dist/tools/deep-reflect.js +2 -2
  31. package/openclaw.plugin.json +6 -1
  32. package/package.json +3 -3
@@ -22,6 +22,74 @@ import * as path from 'path';
22
22
  import { listSessions } from '../core/session-tracker.js';
23
23
  import { withLockAsync } from '../utils/file-lock.js';
24
24
  // ---------------------------------------------------------------------------
25
+ // Session Key Parsing (replicated from openclaw/src/sessions/session-key-utils.ts)
26
+ // ---------------------------------------------------------------------------
27
+ /** Parse an agent-scoped session key. Returns null for non-agent keys. */
28
+ function parseAgentSessionKey(sessionKey) {
29
+ const raw = (sessionKey ?? '').trim().toLowerCase();
30
+ if (!raw)
31
+ return null;
32
+ const parts = raw.split(':').filter(Boolean);
33
+ if (parts.length < 3 || parts[0] !== 'agent')
34
+ return null;
35
+ const agentId = parts[1]?.trim();
36
+ const rest = parts.slice(2).join(':');
37
+ if (!agentId || !rest)
38
+ return null;
39
+ return { agentId, rest };
40
+ }
41
+ /**
42
+ * Returns true if the session was created by a system process (cron, boot, probe, subagent, acp).
43
+ * Uses OpenClaw's native session key patterns to avoid false positives.
44
+ *
45
+ * System patterns:
46
+ * - boot- prefix: boot sessions (e.g. boot-2026-04-02_10-43-45)
47
+ * - probe- prefix: probe sessions (e.g. probe-glm-4.9-xxx)
48
+ * - cron:<...> rest: cron run sessions (agent:<id>:cron:...)
49
+ * - subagent: prefix: subagent sessions
50
+ * - acp: prefix: acp sessions
51
+ *
52
+ * Excluded (NOT system sessions):
53
+ * - User sessions like agent:main:feishu:user:xxx — third component is channel type, NOT cron/subagent/acp
54
+ */
55
+ /**
56
+ * Returns true if the session was created by a system process (cron, boot, probe, subagent, acp).
57
+ * Uses OpenClaw's native session key patterns to avoid false positives.
58
+ *
59
+ * Detection priority (most reliable first):
60
+ * 1. trigger field: Most reliable - explicitly set by OpenClaw ("cron", "heartbeat", "subagent")
61
+ * 2. sessionKey patterns: Secondary confirmation via structured key (agent:main:cron:...)
62
+ * 3. sessionId prefix: Fallback for boot-, probe- prefixed IDs
63
+ *
64
+ * System patterns:
65
+ * - trigger === 'cron' | 'heartbeat' | 'subagent'
66
+ * - sessionKey contains: cron:, subagent:, acp:
67
+ * - sessionId starts with: boot-, probe-
68
+ */
69
+ function isSystemSession(state) {
70
+ const { sessionId, sessionKey, trigger } = state;
71
+ // Primary: trigger field is explicitly set by OpenClaw - most reliable
72
+ if (trigger === 'cron' || trigger === 'heartbeat' || trigger === 'subagent') {
73
+ return true;
74
+ }
75
+ // Secondary: sessionKey pattern matching
76
+ if (sessionKey) {
77
+ const raw = sessionKey.toLowerCase();
78
+ if (raw.includes('cron:'))
79
+ return true;
80
+ if (raw.includes('subagent:'))
81
+ return true;
82
+ if (raw.includes('acp:'))
83
+ return true;
84
+ }
85
+ // Fallback: sessionId prefix patterns (boot-, probe-)
86
+ if (sessionId?.startsWith('boot-'))
87
+ return true;
88
+ if (sessionId?.startsWith('probe-'))
89
+ return true;
90
+ return false;
91
+ }
92
+ // ---------------------------------------------------------------------------
25
93
  // Constants
26
94
  // ---------------------------------------------------------------------------
27
95
  /** File name for nocturnal runtime bookkeeping */
@@ -130,14 +198,17 @@ export function checkWorkspaceIdle(workspaceDir, options = {}, trajectoryLastAct
130
198
  // Separate active vs abandoned sessions
131
199
  const abandonedSessions = [];
132
200
  let mostRecentActivityAt = 0;
133
- let activeSessionCount = 0;
201
+ let userActiveSessions = 0;
134
202
  for (const session of sessions) {
203
+ // Skip system sessions (cron, boot, probe, subagent, acp) from idle determination
204
+ if (isSystemSession(session))
205
+ continue;
135
206
  const inactiveFor = now - session.lastActivityAt;
136
207
  if (inactiveFor > abandonedThresholdMs) {
137
208
  abandonedSessions.push(session.sessionId);
138
209
  }
139
210
  else {
140
- activeSessionCount++;
211
+ userActiveSessions++;
141
212
  if (session.lastActivityAt > mostRecentActivityAt) {
142
213
  mostRecentActivityAt = session.lastActivityAt;
143
214
  }
@@ -171,7 +242,7 @@ export function checkWorkspaceIdle(workspaceDir, options = {}, trajectoryLastAct
171
242
  isIdle,
172
243
  mostRecentActivityAt,
173
244
  idleForMs,
174
- activeSessionCount,
245
+ userActiveSessions,
175
246
  abandonedSessionIds: abandonedSessions,
176
247
  trajectoryGuardrailConfirmsIdle,
177
248
  reason,
@@ -339,7 +410,7 @@ export function checkPreflight(workspaceDir, stateDir, principleId, trajectoryLa
339
410
  if (cooldown.quotaExhausted) {
340
411
  blockers.push(`Quota exhausted (${DEFAULT_MAX_RUNS_PER_WINDOW} runs per ${DEFAULT_QUOTA_WINDOW_MS / 3600000}h window)`);
341
412
  }
342
- if (idle.abandonedSessionIds.length > 0 && idle.activeSessionCount === 0) {
413
+ if (idle.abandonedSessionIds.length > 0 && idle.userActiveSessions === 0) {
343
414
  // Only block if ALL sessions are abandoned (meaning workspace truly has no activity)
344
415
  // If some sessions are active, we trust the session-based idle check
345
416
  }
@@ -217,7 +217,7 @@ export function executeNocturnalReflection(workspaceDir, stateDir, options = {})
217
217
  diagnostics,
218
218
  };
219
219
  }
220
- diagnostics.idle = { isIdle: true, mostRecentActivityAt: 0, idleForMs: 0, activeSessionCount: 0, abandonedSessionIds: [], trajectoryGuardrailConfirmsIdle: true, reason: 'preflight passed' };
220
+ diagnostics.idle = { isIdle: true, mostRecentActivityAt: 0, idleForMs: 0, userActiveSessions: 0, abandonedSessionIds: [], trajectoryGuardrailConfirmsIdle: true, reason: 'preflight passed' };
221
221
  // -------------------------------------------------------------------------
222
222
  // Step 4: Record run start (begin cooldown window)
223
223
  // -------------------------------------------------------------------------
@@ -597,7 +597,7 @@ async function executeNocturnalReflectionWithAdapter(workspaceDir, stateDir, opt
597
597
  diagnostics,
598
598
  };
599
599
  }
600
- diagnostics.idle = { isIdle: true, mostRecentActivityAt: 0, idleForMs: 0, activeSessionCount: 0, abandonedSessionIds: [], trajectoryGuardrailConfirmsIdle: true, reason: 'preflight passed' };
600
+ diagnostics.idle = { isIdle: true, mostRecentActivityAt: 0, idleForMs: 0, userActiveSessions: 0, abandonedSessionIds: [], trajectoryGuardrailConfirmsIdle: true, reason: 'preflight passed' };
601
601
  // Step 3: Record run start
602
602
  void recordRunStart(stateDir, selectedPrincipleId).catch((err) => {
603
603
  console.warn(`[nocturnal-service] Failed to record run start: ${String(err)}`);
@@ -0,0 +1,48 @@
1
+ import type { PluginLogger } from '../../openclaw-sdk.js';
2
+ import type { WorkflowManager, WorkflowHandle, SubagentWorkflowSpec, WorkflowDebugSummary, EmpathyObserverPayload, EmpathyResult } from './types.js';
3
+ import { RuntimeDirectDriver } from './runtime-direct-driver.js';
4
+ export interface EmpathyObserverWorkflowOptions {
5
+ workspaceDir: string;
6
+ logger: PluginLogger;
7
+ subagent: RuntimeDirectDriver['subagent'];
8
+ }
9
+ export declare class EmpathyObserverWorkflowManager implements WorkflowManager {
10
+ private readonly store;
11
+ private readonly driver;
12
+ private readonly logger;
13
+ private readonly workspaceDir;
14
+ private activeWorkflows;
15
+ private completedWorkflows;
16
+ private workflowSpecs;
17
+ constructor(opts: EmpathyObserverWorkflowOptions);
18
+ startWorkflow<TResult>(spec: SubagentWorkflowSpec<TResult>, options: {
19
+ parentSessionId: string;
20
+ workspaceDir?: string;
21
+ taskInput: unknown;
22
+ metadata?: Record<string, unknown>;
23
+ }): Promise<WorkflowHandle>;
24
+ private buildRunParams;
25
+ static buildEmpathyPrompt(userMessage: string): string;
26
+ private scheduleWaitPoll;
27
+ notifyWaitResult(workflowId: string, status: 'ok' | 'error' | 'timeout', error?: string): Promise<void>;
28
+ notifyLifecycleEvent(workflowId: string, event: 'subagent_spawned' | 'subagent_ended', data?: {
29
+ outcome?: 'ok' | 'error' | 'timeout' | 'killed' | 'reset' | 'deleted';
30
+ error?: string;
31
+ }): Promise<void>;
32
+ finalizeOnce(workflowId: string): Promise<void>;
33
+ sweepExpiredWorkflows(maxAgeMs?: number): Promise<number>;
34
+ getWorkflowDebugSummary(workflowId: string, eventLimit?: number): Promise<WorkflowDebugSummary | null>;
35
+ private generateWorkflowId;
36
+ private buildChildSessionKey;
37
+ private extractAssistantText;
38
+ parseEmpathyPayload(rawText: string): EmpathyObserverPayload | null;
39
+ private isCompleted;
40
+ private markCompleted;
41
+ dispose(): void;
42
+ }
43
+ export declare function createEmpathyObserverWorkflowManager(opts: EmpathyObserverWorkflowOptions): EmpathyObserverWorkflowManager;
44
+ /**
45
+ * EmpathyObserver workflow specification.
46
+ * This spec drives EmpathyObserverWorkflowManager for the empathy observer workflow.
47
+ */
48
+ export declare const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult>;
@@ -0,0 +1,480 @@
1
+ import { RuntimeDirectDriver } from './runtime-direct-driver.js';
2
+ import { WorkflowStore } from './workflow-store.js';
3
+ import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
4
+ import { WorkspaceContext } from '../../core/workspace-context.js';
5
+ import { trackFriction } from '../../core/session-tracker.js';
6
+ const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
7
+ const DEFAULT_TIMEOUT_MS = 30_000;
8
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
9
+ export class EmpathyObserverWorkflowManager {
10
+ store;
11
+ driver;
12
+ logger;
13
+ workspaceDir;
14
+ activeWorkflows = new Map();
15
+ completedWorkflows = new Map();
16
+ workflowSpecs = new Map();
17
+ constructor(opts) {
18
+ this.workspaceDir = opts.workspaceDir;
19
+ this.logger = opts.logger;
20
+ this.store = new WorkflowStore({ workspaceDir: opts.workspaceDir });
21
+ this.driver = new RuntimeDirectDriver({ subagent: opts.subagent, logger: opts.logger });
22
+ }
23
+ async startWorkflow(spec, options) {
24
+ const workflowId = this.generateWorkflowId();
25
+ const childSessionKey = this.buildChildSessionKey(options.parentSessionId);
26
+ const now = Date.now();
27
+ const metadata = {
28
+ parentSessionId: options.parentSessionId,
29
+ workspaceDir: options.workspaceDir,
30
+ taskInput: options.taskInput,
31
+ startedAt: now,
32
+ workflowType: spec.workflowType,
33
+ ...options.metadata,
34
+ };
35
+ this.logger.info(`[PD:EmpathyObserverWorkflow] Starting workflow: workflowId=${workflowId}, type=${spec.workflowType}`);
36
+ // Surface degrade: skip boot sessions (they run outside gateway request context)
37
+ if (options.parentSessionId.startsWith('boot-')) {
38
+ this.logger.info(`[PD:EmpathyObserverWorkflow] Skipping workflow: boot session (gateway request context unavailable)`);
39
+ throw new Error(`EmpathyObserverWorkflowManager: cannot start workflow for boot session`);
40
+ }
41
+ // Surface degrade: check subagent runtime availability before calling run()
42
+ if (!isSubagentRuntimeAvailable(this.driver.getSubagent())) {
43
+ this.logger.info(`[PD:EmpathyObserverWorkflow] Skipping workflow: subagent runtime unavailable`);
44
+ throw new Error(`EmpathyObserverWorkflowManager: subagent runtime unavailable`);
45
+ }
46
+ if (spec.transport !== 'runtime_direct') {
47
+ throw new Error(`EmpathyObserverWorkflowManager only supports runtime_direct transport`);
48
+ }
49
+ const runParams = this.buildRunParams(spec, options, childSessionKey);
50
+ const runResult = await this.driver.run(runParams);
51
+ this.store.createWorkflow({
52
+ workflow_id: workflowId,
53
+ workflow_type: spec.workflowType,
54
+ transport: spec.transport,
55
+ parent_session_id: options.parentSessionId,
56
+ child_session_key: childSessionKey,
57
+ run_id: runResult.runId,
58
+ state: 'active',
59
+ created_at: now,
60
+ updated_at: now,
61
+ metadata_json: JSON.stringify(metadata),
62
+ });
63
+ this.store.recordEvent(workflowId, 'spawned', null, 'active', 'subagent spawned', { runId: runResult.runId });
64
+ this.workflowSpecs.set(workflowId, spec);
65
+ this.scheduleWaitPoll(workflowId, spec.timeoutMs ?? DEFAULT_TIMEOUT_MS, runResult.runId);
66
+ return {
67
+ workflowId,
68
+ childSessionKey,
69
+ runId: runResult.runId,
70
+ state: 'active',
71
+ };
72
+ }
73
+ buildRunParams(spec, options, childSessionKey) {
74
+ const message = spec.buildPrompt(options.taskInput, {
75
+ parentSessionId: options.parentSessionId,
76
+ workspaceDir: options.workspaceDir,
77
+ taskInput: options.taskInput,
78
+ startedAt: Date.now(),
79
+ workflowType: spec.workflowType,
80
+ ...(options.metadata ?? {}),
81
+ });
82
+ return {
83
+ sessionKey: childSessionKey,
84
+ message,
85
+ lane: 'subagent',
86
+ deliver: false,
87
+ idempotencyKey: `${options.parentSessionId}:${Date.now()}`,
88
+ expectsCompletionMessage: true,
89
+ };
90
+ }
91
+ static buildEmpathyPrompt(userMessage) {
92
+ return [
93
+ 'You are an empathy observer.',
94
+ 'Analyze ONLY the user message and return strict JSON (no markdown):',
95
+ '{"damageDetected": boolean, "severity": "mild|moderate|severe", "confidence": number, "reason": string}',
96
+ `User message: ${JSON.stringify(userMessage.trim())}`,
97
+ ].join('\n');
98
+ }
99
+ scheduleWaitPoll(workflowId, timeoutMs, runId) {
100
+ const effectiveTimeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
101
+ const timeout = setTimeout(async () => {
102
+ try {
103
+ const result = await this.driver.wait({ runId, timeoutMs: effectiveTimeoutMs });
104
+ await this.notifyWaitResult(workflowId, result.status, result.error);
105
+ }
106
+ catch (error) {
107
+ this.logger.error(`[PD:EmpathyObserverWorkflow] Wait poll failed: ${String(error)}`);
108
+ await this.notifyWaitResult(workflowId, 'error', String(error));
109
+ }
110
+ }, 100);
111
+ this.activeWorkflows.set(workflowId, timeout);
112
+ }
113
+ async notifyWaitResult(workflowId, status, error) {
114
+ const workflow = this.store.getWorkflow(workflowId);
115
+ if (!workflow) {
116
+ this.logger.warn(`[PD:EmpathyObserverWorkflow] notifyWaitResult: workflow not found: ${workflowId}`);
117
+ return;
118
+ }
119
+ if (workflow.state === 'completed' || workflow.state === 'terminal_error' || workflow.state === 'expired') {
120
+ this.logger.info(`[PD:EmpathyObserverWorkflow] notifyWaitResult: ignoring terminal workflow: ${workflowId}, state=${workflow.state}`);
121
+ return;
122
+ }
123
+ this.logger.info(`[PD:EmpathyObserverWorkflow] notifyWaitResult: workflowId=${workflowId}, status=${status}`);
124
+ const previousState = workflow.state;
125
+ this.store.updateWorkflowState(workflowId, 'wait_result');
126
+ this.store.recordEvent(workflowId, 'wait_result', previousState, 'wait_result', `wait completed: ${status}`, { error });
127
+ const spec = this.workflowSpecs.get(workflowId);
128
+ const shouldFinalize = spec ? spec.shouldFinalizeOnWaitStatus(status) : status === 'ok';
129
+ if (shouldFinalize) {
130
+ await this.finalizeOnce(workflowId);
131
+ }
132
+ else {
133
+ this.store.updateWorkflowState(workflowId, 'terminal_error');
134
+ this.store.recordEvent(workflowId, 'finalize_skipped', 'wait_result', 'terminal_error', `wait status: ${status}`, { error });
135
+ }
136
+ }
137
+ async notifyLifecycleEvent(workflowId, event, data) {
138
+ this.logger.info(`[PD:EmpathyObserverWorkflow] notifyLifecycleEvent: workflowId=${workflowId}, event=${event}`);
139
+ if (event === 'subagent_ended' && data?.outcome) {
140
+ await this.notifyWaitResult(workflowId, data.outcome === 'ok' ? 'ok' : data.outcome === 'error' ? 'error' : 'timeout', data.error);
141
+ }
142
+ }
143
+ async finalizeOnce(workflowId) {
144
+ const workflow = this.store.getWorkflow(workflowId);
145
+ if (!workflow) {
146
+ this.logger.warn(`[PD:EmpathyObserverWorkflow] finalizeOnce: workflow not found: ${workflowId}`);
147
+ return;
148
+ }
149
+ const spec = this.workflowSpecs.get(workflowId);
150
+ if (!spec) {
151
+ throw new Error(`Workflow spec not registered for ${workflowId}`);
152
+ }
153
+ if (this.isCompleted(workflowId)) {
154
+ this.logger.info(`[PD:EmpathyObserverWorkflow] finalizeOnce: already completed: ${workflowId}`);
155
+ return;
156
+ }
157
+ this.logger.info(`[PD:EmpathyObserverWorkflow] Finalizing workflow: ${workflowId}`);
158
+ this.store.updateWorkflowState(workflowId, 'finalizing');
159
+ try {
160
+ const result = await this.driver.getResult({ sessionKey: workflow.child_session_key, limit: 20 });
161
+ const metadata = JSON.parse(workflow.metadata_json);
162
+ const parsed = await spec.parseResult({
163
+ messages: result.messages,
164
+ assistantTexts: result.assistantTexts,
165
+ metadata,
166
+ waitStatus: 'ok',
167
+ });
168
+ if (!parsed) {
169
+ this.store.updateWorkflowState(workflowId, 'terminal_error');
170
+ this.store.recordEvent(workflowId, 'parse_failed', 'finalizing', 'terminal_error', 'spec.parseResult returned null', {});
171
+ return;
172
+ }
173
+ await spec.persistResult({
174
+ result: parsed,
175
+ metadata,
176
+ workspaceDir: this.workspaceDir,
177
+ });
178
+ this.store.recordEvent(workflowId, 'persisted', 'finalizing', 'finalizing', 'result persisted', {});
179
+ if (spec.shouldDeleteSessionAfterFinalize && workflow.run_id) {
180
+ try {
181
+ await this.driver.cleanup({ sessionKey: workflow.child_session_key });
182
+ this.store.updateCleanupState(workflowId, 'completed');
183
+ }
184
+ catch (cleanupError) {
185
+ this.logger.error(`[PD:EmpathyObserverWorkflow] cleanup failed after persistence: ${String(cleanupError)}`);
186
+ this.store.updateCleanupState(workflowId, 'failed');
187
+ this.store.updateWorkflowState(workflowId, 'cleanup_pending');
188
+ this.store.recordEvent(workflowId, 'cleanup_failed', 'finalizing', 'cleanup_pending', String(cleanupError), {});
189
+ this.markCompleted(workflowId);
190
+ return;
191
+ }
192
+ }
193
+ this.store.updateWorkflowState(workflowId, 'completed');
194
+ this.store.recordEvent(workflowId, 'finalized', 'finalizing', 'completed', 'success', {});
195
+ this.markCompleted(workflowId);
196
+ }
197
+ catch (error) {
198
+ this.logger.error(`[PD:EmpathyObserverWorkflow] finalizeOnce failed: ${String(error)}`);
199
+ this.store.updateWorkflowState(workflowId, 'terminal_error');
200
+ this.store.recordEvent(workflowId, 'finalize_error', 'finalizing', 'terminal_error', String(error), {});
201
+ throw error;
202
+ }
203
+ }
204
+ async sweepExpiredWorkflows(maxAgeMs = DEFAULT_TTL_MS) {
205
+ const expired = this.store.getExpiredWorkflows(maxAgeMs);
206
+ this.logger.info(`[PD:EmpathyObserverWorkflow] sweepExpiredWorkflows: found ${expired.length} expired`);
207
+ for (const workflow of expired) {
208
+ try {
209
+ this.logger.info(`[PD:EmpathyObserverWorkflow] Sweeping expired workflow: ${workflow.workflow_id}`);
210
+ await this.driver.cleanup({ sessionKey: workflow.child_session_key });
211
+ this.store.updateCleanupState(workflow.workflow_id, 'completed');
212
+ this.store.updateWorkflowState(workflow.workflow_id, 'expired');
213
+ this.store.recordEvent(workflow.workflow_id, 'swept', workflow.state, 'expired', 'TTL expired', {});
214
+ }
215
+ catch (error) {
216
+ this.logger.error(`[PD:EmpathyObserverWorkflow] Sweep cleanup failed for ${workflow.workflow_id}: ${String(error)}`);
217
+ this.store.updateCleanupState(workflow.workflow_id, 'failed');
218
+ }
219
+ }
220
+ return expired.length;
221
+ }
222
+ async getWorkflowDebugSummary(workflowId, eventLimit = 10) {
223
+ const workflow = this.store.getWorkflow(workflowId);
224
+ if (!workflow)
225
+ return null;
226
+ const metadata = JSON.parse(workflow.metadata_json);
227
+ const recentEvents = this.store
228
+ .getEvents(workflowId)
229
+ .slice(-eventLimit)
230
+ .map((event) => ({
231
+ eventType: event.event_type,
232
+ fromState: event.from_state,
233
+ toState: event.to_state,
234
+ reason: event.reason,
235
+ createdAt: event.created_at,
236
+ payload: JSON.parse(event.payload_json || '{}'),
237
+ }));
238
+ return {
239
+ workflowId: workflow.workflow_id,
240
+ workflowType: workflow.workflow_type,
241
+ transport: workflow.transport,
242
+ parentSessionId: workflow.parent_session_id,
243
+ childSessionKey: workflow.child_session_key,
244
+ runId: workflow.run_id,
245
+ state: workflow.state,
246
+ cleanupState: workflow.cleanup_state,
247
+ lastObservedAt: workflow.last_observed_at ?? null,
248
+ metadata,
249
+ recentEvents,
250
+ };
251
+ }
252
+ generateWorkflowId() {
253
+ return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
254
+ }
255
+ buildChildSessionKey(parentSessionId) {
256
+ const safeParentSessionId = parentSessionId
257
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
258
+ .substring(0, 64);
259
+ const timestamp = Date.now();
260
+ return `${WORKFLOW_SESSION_PREFIX}${safeParentSessionId}-${timestamp}`;
261
+ }
262
+ extractAssistantText(messages, assistantTexts) {
263
+ if (assistantTexts && assistantTexts.length > 0) {
264
+ return assistantTexts[assistantTexts.length - 1] || '';
265
+ }
266
+ for (let i = messages.length - 1; i >= 0; i--) {
267
+ const msg = messages[i];
268
+ if (msg?.role !== 'assistant')
269
+ continue;
270
+ if (typeof msg.content === 'string')
271
+ return msg.content;
272
+ if (Array.isArray(msg.content)) {
273
+ const txt = msg.content
274
+ .filter((part) => part?.type === 'text' && typeof part.text === 'string')
275
+ .map((part) => part.text)
276
+ .join('\n');
277
+ if (txt)
278
+ return txt;
279
+ }
280
+ }
281
+ return '';
282
+ }
283
+ parseEmpathyPayload(rawText) {
284
+ if (!rawText?.trim())
285
+ return null;
286
+ try {
287
+ return JSON.parse(rawText.trim());
288
+ }
289
+ catch {
290
+ const match = rawText.match(/\{[\s\S]*\}/);
291
+ if (!match) {
292
+ this.logger.warn('[PD:EmpathyObserverWorkflow] Observer payload is not valid JSON');
293
+ return null;
294
+ }
295
+ try {
296
+ return JSON.parse(match[0]);
297
+ }
298
+ catch {
299
+ this.logger.warn('[PD:EmpathyObserverWorkflow] Failed to parse observer JSON payload');
300
+ return null;
301
+ }
302
+ }
303
+ }
304
+ isCompleted(workflowId) {
305
+ const timestamp = this.completedWorkflows.get(workflowId);
306
+ if (!timestamp)
307
+ return false;
308
+ if (Date.now() - timestamp > 5 * 60 * 1000) {
309
+ this.completedWorkflows.delete(workflowId);
310
+ return false;
311
+ }
312
+ return true;
313
+ }
314
+ markCompleted(workflowId) {
315
+ this.completedWorkflows.set(workflowId, Date.now());
316
+ this.workflowSpecs.delete(workflowId);
317
+ const timeout = this.activeWorkflows.get(workflowId);
318
+ if (timeout) {
319
+ clearTimeout(timeout);
320
+ this.activeWorkflows.delete(workflowId);
321
+ }
322
+ }
323
+ dispose() {
324
+ for (const timeout of this.activeWorkflows.values()) {
325
+ clearTimeout(timeout);
326
+ }
327
+ this.activeWorkflows.clear();
328
+ this.store.dispose();
329
+ }
330
+ }
331
+ export function createEmpathyObserverWorkflowManager(opts) {
332
+ return new EmpathyObserverWorkflowManager(opts);
333
+ }
334
+ /**
335
+ * Extract raw assistant text from messages or assistantTexts array.
336
+ */
337
+ function extractAssistantTextForSpec(messages, assistantTexts) {
338
+ if (assistantTexts && assistantTexts.length > 0) {
339
+ return assistantTexts[assistantTexts.length - 1] || '';
340
+ }
341
+ for (let i = messages.length - 1; i >= 0; i--) {
342
+ const msg = messages[i];
343
+ if (msg?.role !== 'assistant')
344
+ continue;
345
+ if (typeof msg.content === 'string')
346
+ return msg.content;
347
+ if (Array.isArray(msg.content)) {
348
+ const txt = msg.content
349
+ .filter((part) => part?.type === 'text' && typeof part.text === 'string')
350
+ .map((part) => part.text)
351
+ .join('\n');
352
+ if (txt)
353
+ return txt;
354
+ }
355
+ }
356
+ return '';
357
+ }
358
+ /**
359
+ * Parse empathy observer JSON payload from raw text.
360
+ */
361
+ function parseEmpathyPayloadForSpec(rawText) {
362
+ if (!rawText?.trim())
363
+ return null;
364
+ try {
365
+ return JSON.parse(rawText.trim());
366
+ }
367
+ catch {
368
+ const match = rawText.match(/\{[\s\S]*\}/);
369
+ if (!match)
370
+ return null;
371
+ try {
372
+ return JSON.parse(match[0]);
373
+ }
374
+ catch {
375
+ return null;
376
+ }
377
+ }
378
+ }
379
+ /**
380
+ * Normalize severity to valid enum.
381
+ */
382
+ function normalizeSeverityForSpec(severity) {
383
+ if (severity === 'severe')
384
+ return 'severe';
385
+ if (severity === 'moderate')
386
+ return 'moderate';
387
+ return 'mild';
388
+ }
389
+ /**
390
+ * Normalize confidence to [0, 1] range.
391
+ */
392
+ function normalizeConfidenceForSpec(value) {
393
+ if (!Number.isFinite(value))
394
+ return 1;
395
+ return Math.max(0, Math.min(1, Number(value)));
396
+ }
397
+ /**
398
+ * Calculate pain score from severity using config.
399
+ */
400
+ function scoreFromSeverityForSpec(severity, wctx) {
401
+ if (severity === 'severe')
402
+ return Number(wctx.config.get('empathy_engine.penalties.severe') ?? 40);
403
+ if (severity === 'moderate')
404
+ return Number(wctx.config.get('empathy_engine.penalties.moderate') ?? 25);
405
+ return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
406
+ }
407
+ /**
408
+ * EmpathyObserver workflow specification.
409
+ * This spec drives EmpathyObserverWorkflowManager for the empathy observer workflow.
410
+ */
411
+ export const empathyObserverWorkflowSpec = {
412
+ workflowType: 'empathy-observer',
413
+ transport: 'runtime_direct',
414
+ timeoutMs: 30_000,
415
+ ttlMs: 300_000,
416
+ shouldDeleteSessionAfterFinalize: true,
417
+ buildPrompt(taskInput, _metadata) {
418
+ const userMessage = String(taskInput).trim();
419
+ return [
420
+ 'You are an empathy observer.',
421
+ 'Analyze ONLY the user message and return strict JSON (no markdown):',
422
+ '{"damageDetected": boolean, "severity": "mild|moderate|severe", "confidence": number, "reason": string}',
423
+ `User message: ${JSON.stringify(userMessage)}`,
424
+ ].join('\n');
425
+ },
426
+ async parseResult(ctx) {
427
+ const rawText = extractAssistantTextForSpec(ctx.messages, ctx.assistantTexts);
428
+ const payload = parseEmpathyPayloadForSpec(rawText);
429
+ if (!payload)
430
+ return null;
431
+ return {
432
+ damageDetected: payload.damageDetected ?? false,
433
+ severity: normalizeSeverityForSpec(payload.severity),
434
+ confidence: normalizeConfidenceForSpec(payload.confidence),
435
+ reason: payload.reason ?? '',
436
+ painScore: 0,
437
+ };
438
+ },
439
+ async persistResult(ctx) {
440
+ const { result, metadata, workspaceDir } = ctx;
441
+ if (!result.damageDetected)
442
+ return;
443
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
444
+ const painScore = scoreFromSeverityForSpec(result.severity, wctx);
445
+ trackFriction(metadata.parentSessionId, painScore, `observer_empathy_${result.severity}`, workspaceDir, { source: 'user_empathy' });
446
+ const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
447
+ wctx.eventLog.recordPainSignal(metadata.parentSessionId, {
448
+ score: painScore,
449
+ source: 'user_empathy',
450
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
451
+ isRisky: false,
452
+ origin: 'system_infer',
453
+ severity: result.severity,
454
+ confidence: result.confidence,
455
+ detection_mode: 'structured',
456
+ deduped: false,
457
+ trigger_text_excerpt: '',
458
+ raw_score: painScore,
459
+ calibrated_score: painScore,
460
+ eventId,
461
+ });
462
+ try {
463
+ wctx.trajectory?.recordPainEvent?.({
464
+ sessionId: metadata.parentSessionId,
465
+ source: 'user_empathy',
466
+ score: painScore,
467
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
468
+ severity: result.severity,
469
+ origin: 'system_infer',
470
+ confidence: result.confidence,
471
+ });
472
+ }
473
+ catch (error) {
474
+ console.warn(`[PD:EmpathyObserverWorkflow] Failed to persist trajectory: ${String(error)}`);
475
+ }
476
+ },
477
+ shouldFinalizeOnWaitStatus(status) {
478
+ return status === 'ok';
479
+ },
480
+ };
@@ -0,0 +1,4 @@
1
+ export { RuntimeDirectDriver, type TransportDriver, type RunParams, type RunResult, type WaitParams, type WaitResult, type GetResultParams, type GetResultResult, type CleanupParams, } from './runtime-direct-driver.js';
2
+ export { WorkflowStore, type WorkflowStoreOptions } from './workflow-store.js';
3
+ export { EmpathyObserverWorkflowManager, createEmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, type EmpathyObserverWorkflowOptions, } from './empathy-observer-workflow-manager.js';
4
+ export type { WorkflowState, WorkflowTransport, WorkflowMetadata, WorkflowResultContext, WorkflowPersistContext, WorkflowHandle, SubagentWorkflowSpec, EmpathyObserverWorkflowSpec, EmpathyObserverPayload, EmpathyResult, WorkflowRow, WorkflowEventRow, WorkflowDebugSummary, } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { RuntimeDirectDriver, } from './runtime-direct-driver.js';
2
+ export { WorkflowStore } from './workflow-store.js';
3
+ export { EmpathyObserverWorkflowManager, createEmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, } from './empathy-observer-workflow-manager.js';