principles-disciple 1.16.0 → 1.18.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.
Files changed (132) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +4 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +480 -158
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +221 -109
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +11 -4
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
  113. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
  114. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
  115. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  116. package/templates/pain_settings.json +1 -1
  117. package/tests/build-artifacts.test.ts +4 -58
  118. package/tests/commands/pd-reflect.test.ts +49 -0
  119. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  120. package/tests/core/pain-auto-repair.test.ts +96 -0
  121. package/tests/core/pain-integration.test.ts +483 -0
  122. package/tests/core/pain.test.ts +5 -4
  123. package/tests/core/workspace-dir-service.test.ts +68 -0
  124. package/tests/core/workspace-dir-validation.test.ts +56 -192
  125. package/tests/hooks/pain.test.ts +20 -0
  126. package/tests/http/principles-console-route.test.ts +42 -20
  127. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  128. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  129. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  130. package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
  131. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  132. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -22,8 +22,14 @@ import type { WorkflowRow } from './subagent-workflow/types.js';
22
22
  import { EmpathyObserverWorkflowManager } from './subagent-workflow/empathy-observer-workflow-manager.js';
23
23
  import { DeepReflectWorkflowManager } from './subagent-workflow/deep-reflect-workflow-manager.js';
24
24
  import { NocturnalWorkflowManager, nocturnalWorkflowSpec } from './subagent-workflow/nocturnal-workflow-manager.js';
25
- import { createNocturnalTrajectoryExtractor } from '../core/nocturnal-trajectory-extractor.js';
25
+ import {
26
+ createNocturnalTrajectoryExtractor,
27
+ type NocturnalPainEvent,
28
+ type NocturnalSessionSnapshot,
29
+ } from '../core/nocturnal-trajectory-extractor.js';
30
+ import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
26
31
  import { isExpectedSubagentError } from './subagent-workflow/subagent-error-utils.js';
32
+ import { readPainFlagContract } from '../core/pain.js';
27
33
 
28
34
  const WORKFLOW_TTL_MS = 5 * 60 * 1000; // 5 minutes default TTL for helper workflows
29
35
  import { OpenClawTrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
@@ -126,9 +132,9 @@ async function runWorkflowWatchdog(
126
132
  if (dataSource === 'pain_context_fallback') {
127
133
  details.push(`fallback_snapshot: nocturnal workflow ${wf.workflow_id} uses pain-context fallback (stats may be incomplete)`);
128
134
  }
129
- const stats = snapshot.stats as Record<string, number> | undefined;
130
- if (stats && stats.totalAssistantTurns === 0 && stats.totalToolCalls === 0 && stats.totalPainEvents === 0 && stats.totalGateBlocks === 0) {
131
- details.push(`empty_snapshot: nocturnal workflow ${wf.workflow_id} has all-zero stats`);
135
+ const stats = snapshot.stats as Record<string, number | null> | undefined;
136
+ if (stats && stats.totalAssistantTurns === null && stats.totalToolCalls === null && stats.totalPainEvents === 0 && stats.totalGateBlocks === null) {
137
+ details.push(`fallback_snapshot_stats: nocturnal workflow ${wf.workflow_id} has null stats (data unavailable)`);
132
138
  }
133
139
  }
134
140
  } catch { /* ignore malformed metadata */ }
@@ -167,7 +173,7 @@ let timeoutId: NodeJS.Timeout | null = null;
167
173
  * Old queue items (without taskKind) are migrated to pain_diagnosis for compatibility.
168
174
  */
169
175
  export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
170
- export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback';
176
+ export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation';
171
177
 
172
178
  /**
173
179
  * Recent pain context attached to sleep_reflection tasks.
@@ -181,6 +187,8 @@ export interface RecentPainContext {
181
187
  source: string;
182
188
  reason: string;
183
189
  timestamp: string;
190
+ /** Session ID where the pain occurred */
191
+ sessionId: string;
184
192
  } | null;
185
193
  /** Count of pain events in the recent window (for signal strength) */
186
194
  recentPainCount: number;
@@ -188,30 +196,6 @@ export interface RecentPainContext {
188
196
  recentMaxPainScore: number;
189
197
  }
190
198
 
191
- function hasUsableNocturnalSnapshot(snapshotData: Record<string, unknown> | undefined): boolean {
192
- if (!snapshotData || typeof snapshotData.sessionId !== 'string' || snapshotData.sessionId.length === 0) {
193
- return false;
194
- }
195
-
196
- if (snapshotData._dataSource !== 'pain_context_fallback') {
197
- return true;
198
- }
199
-
200
- const stats = (snapshotData.stats && typeof snapshotData.stats === 'object')
201
- ? snapshotData.stats as Record<string, number | null | undefined>
202
- : undefined;
203
- const recentPain = Array.isArray(snapshotData.recentPain) ? snapshotData.recentPain.length : 0;
204
- const hasNonZeroStats = !!stats && [
205
- 'totalAssistantTurns',
206
- 'totalToolCalls',
207
- 'failureCount',
208
- 'totalPainEvents',
209
- 'totalGateBlocks',
210
- ].some((key) => Number(stats[key] ?? 0) > 0);
211
-
212
- return hasNonZeroStats || recentPain > 0;
213
- }
214
-
215
199
  export interface EvolutionQueueItem {
216
200
  // Core identity
217
201
  id: string;
@@ -333,6 +317,58 @@ function migrateQueueToV2(queue: RawQueueItem[]): EvolutionQueueItem[] {
333
317
  return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item as unknown as LegacyEvolutionQueueItem) : item as unknown as EvolutionQueueItem);
334
318
  }
335
319
 
320
+ function isSessionAtOrBeforeTriggerTime(
321
+ session: { startedAt: string; updatedAt: string },
322
+ triggerTimeMs: number,
323
+ ): boolean {
324
+ const startedAtMs = new Date(session.startedAt).getTime();
325
+ const updatedAtMs = new Date(session.updatedAt).getTime();
326
+ if (!Number.isFinite(triggerTimeMs)) {
327
+ return true;
328
+ }
329
+ if (Number.isFinite(startedAtMs) && startedAtMs > triggerTimeMs) {
330
+ return false;
331
+ }
332
+ if (Number.isFinite(updatedAtMs) && updatedAtMs > triggerTimeMs) {
333
+ return false;
334
+ }
335
+ return true;
336
+ }
337
+
338
+ function buildFallbackNocturnalSnapshot(sleepTask: EvolutionQueueItem): NocturnalSessionSnapshot | null {
339
+ const painContext = sleepTask.recentPainContext;
340
+ if (!painContext) {
341
+ return null;
342
+ }
343
+
344
+ const fallbackPainEvents: NocturnalPainEvent[] = painContext.mostRecent ? [{
345
+ source: painContext.mostRecent.source,
346
+ score: painContext.mostRecent.score,
347
+ severity: null,
348
+ reason: painContext.mostRecent.reason,
349
+ createdAt: painContext.mostRecent.timestamp,
350
+ }] : [];
351
+
352
+ return {
353
+ sessionId: painContext.mostRecent?.sessionId || sleepTask.id,
354
+ startedAt: sleepTask.timestamp,
355
+ updatedAt: sleepTask.timestamp,
356
+ assistantTurns: [],
357
+ userTurns: [],
358
+ toolCalls: [],
359
+ painEvents: fallbackPainEvents,
360
+ gateBlocks: [],
361
+ stats: {
362
+ totalAssistantTurns: null,
363
+ totalToolCalls: null,
364
+ failureCount: null,
365
+ totalPainEvents: painContext.recentPainCount,
366
+ totalGateBlocks: null,
367
+ },
368
+ _dataSource: 'pain_context_fallback',
369
+ };
370
+ }
371
+
336
372
  const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
337
373
 
338
374
  // P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
@@ -341,7 +377,7 @@ export const LOCK_MAX_RETRIES = 50;
341
377
  export const LOCK_RETRY_DELAY_MS = 50;
342
378
  export const LOCK_STALE_MS = 30_000;
343
379
 
344
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for unique task ID generation */
380
+
345
381
  export function createEvolutionTaskId(
346
382
  source: string,
347
383
  score: number,
@@ -357,7 +393,7 @@ export function createEvolutionTaskId(
357
393
  .substring(0, 8);
358
394
  }
359
395
 
360
- /* eslint-disable no-unused-vars -- Reason: type-level function parameter names in logger union type are documentation */
396
+
361
397
  export async function acquireQueueLock(resourcePath: string, logger: PluginLogger | { warn?: (message: string) => void; info?: (message: string) => void } | undefined, lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX): Promise<() => void> {
362
398
  try {
363
399
  const ctx: LockContext = await acquireLockAsync(resourcePath, {
@@ -374,7 +410,7 @@ export async function acquireQueueLock(resourcePath: string, logger: PluginLogge
374
410
  }
375
411
  }
376
412
 
377
- /* eslint-disable no-unused-vars, @typescript-eslint/max-params -- Reason: type-level function parameter names in logger union type are documentation */
413
+
378
414
  async function requireQueueLock(resourcePath: string, logger: PluginLogger | { warn?: (message: string) => void; info?: (message: string) => void } | undefined, scope: string, lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX): Promise<() => void> {
379
415
  try {
380
416
  return await acquireQueueLock(resourcePath, logger, lockSuffix);
@@ -389,7 +425,7 @@ export function extractEvolutionTaskId(task: string): string | null {
389
425
  return match?.[1] || null;
390
426
  }
391
427
 
392
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for duplicate detection */
428
+
393
429
  function findRecentDuplicateTask(
394
430
  queue: EvolutionQueueItem[],
395
431
  source: string,
@@ -397,13 +433,13 @@ function findRecentDuplicateTask(
397
433
  now: number,
398
434
  reason?: string
399
435
  ): EvolutionQueueItem | undefined {
400
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but used in helper for consistency
436
+
401
437
  const key = normalizePainDedupKey(source, preview, reason);
402
438
  return queue.find((task) => {
403
439
  if (task.status === 'completed') return false;
404
440
  const taskTime = new Date(task.enqueued_at || task.timestamp).getTime();
405
441
  if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS) return false;
406
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but used in helper for consistency
442
+
407
443
  return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
408
444
  });
409
445
  }
@@ -456,7 +492,7 @@ function normalizePainDedupKey(source: string, preview: string, reason?: string)
456
492
  return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
457
493
  }
458
494
 
459
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for duplicate detection */
495
+
460
496
  export function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number, reason?: string): boolean {
461
497
  return !!findRecentDuplicateTask(queue, source, preview, now, reason);
462
498
  }
@@ -477,34 +513,26 @@ export function hasEquivalentPromotedRule(dictionary: { getAllRules(): Record<st
477
513
 
478
514
  /**
479
515
  * Read recent pain context from PAIN_FLAG file.
516
+ * Extracts session_id to link to trajectory DB.
480
517
  * Returns structured pain metadata for attaching to sleep_reflection tasks.
481
518
  * Returns null if no pain flag exists.
482
519
  */
483
- function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext {
484
- const painFlagPath = wctx.resolve('PAIN_FLAG');
485
- if (!fs.existsSync(painFlagPath)) {
520
+ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext {
521
+ const contract = readPainFlagContract(wctx.workspaceDir);
522
+ if (contract.status !== 'valid') {
486
523
  return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
487
524
  }
488
525
 
489
526
  try {
490
- const rawPain = fs.readFileSync(painFlagPath, 'utf8');
491
- const lines = rawPain.split('\n');
492
-
493
- let score = 0;
494
- let source = '';
495
- let reason = '';
496
- let timestamp = '';
497
-
498
- for (const line of lines) {
499
- if (line.startsWith('score:')) score = parseInt(line.split(':', 2)[1].trim(), 10) || 0;
500
- if (line.startsWith('source:')) source = line.split(':', 2)[1].trim();
501
- if (line.startsWith('reason:')) reason = line.slice('reason:'.length).trim();
502
- if (line.startsWith('timestamp:')) timestamp = line.slice('timestamp:'.length).trim();
503
- }
527
+ const score = parseInt(contract.data.score ?? '0', 10) || 0;
528
+ const source = contract.data.source ?? '';
529
+ const reason = contract.data.reason ?? '';
530
+ const timestamp = contract.data.time ?? '';
531
+ const sessionId = contract.data.session_id ?? '';
504
532
 
505
533
  if (score > 0) {
506
534
  return {
507
- mostRecent: { score, source, reason, timestamp },
535
+ mostRecent: { score, source, reason, timestamp, sessionId },
508
536
  recentPainCount: 1,
509
537
  recentMaxPainScore: score,
510
538
  };
@@ -583,7 +611,7 @@ interface ParsedPainValues {
583
611
  traceId: string; sessionId: string; agentId: string;
584
612
  }
585
613
 
586
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for task enqueue */
614
+
587
615
  async function doEnqueuePainTask(
588
616
  wctx: WorkspaceContext, logger: PluginLogger, painFlagPath: string,
589
617
  result: WorkerStatusReport['pain_flag'], v: ParsedPainValues,
@@ -664,6 +692,41 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
664
692
  if (!fs.existsSync(painFlagPath)) return result;
665
693
 
666
694
  const rawPain = fs.readFileSync(painFlagPath, 'utf8');
695
+ const contract = readPainFlagContract(wctx.workspaceDir);
696
+
697
+ if (contract.status === 'valid') {
698
+ const score = parseInt(contract.data.score ?? '0', 10) || 0;
699
+ const source = contract.data.source ?? 'unknown';
700
+ const reason = contract.data.reason ?? 'Systemic pain detected';
701
+ const preview = contract.data.trigger_text_preview ?? '';
702
+ const isQueued = contract.data.status === 'queued';
703
+ const traceId = contract.data.trace_id ?? '';
704
+ const sessionId = contract.data.session_id ?? '';
705
+ const agentId = contract.data.agent_id ?? '';
706
+
707
+ result.exists = true;
708
+ result.score = score;
709
+ result.source = source;
710
+ result.enqueued = isQueued;
711
+
712
+ if (isQueued) {
713
+ result.skipped_reason = 'already_queued';
714
+ if (logger) logger.info(`[PD:EvolutionWorker] Pain flag already queued (score=${score}, source=${source})`);
715
+ return result;
716
+ }
717
+
718
+ if (logger) logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
719
+ return doEnqueuePainTask(wctx, logger, painFlagPath, result, {
720
+ score, source, reason, preview, traceId, sessionId, agentId,
721
+ });
722
+ }
723
+
724
+ if (contract.status === 'invalid' && (contract.format === 'kv' || contract.format === 'json' || contract.format === 'invalid_json')) {
725
+ result.exists = true;
726
+ result.skipped_reason = `invalid_pain_flag (${contract.missingFields.join(', ') || contract.format})`;
727
+ if (logger) logger.warn(`[PD:EvolutionWorker] Invalid pain flag skipped: ${result.skipped_reason}`);
728
+ return result;
729
+ }
667
730
 
668
731
  // Try JSON format first (pain skill structured output)
669
732
  // The file may have 'status: queued' and 'task_id: xxx' appended after the JSON object.
@@ -794,7 +857,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
794
857
  return result;
795
858
  }
796
859
 
797
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for queue processing */
860
+
798
861
  async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogger, eventLog: EventLog, api?: OpenClawPluginApi) {
799
862
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
800
863
  if (!fs.existsSync(queuePath)) {
@@ -1392,57 +1455,83 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1392
1455
  }
1393
1456
 
1394
1457
  let workflowId: string | undefined;
1395
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned when runtime API is available
1458
+
1396
1459
  let nocturnalManager: NocturnalWorkflowManager;
1397
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned only for newly started workflows
1398
- let snapshotData: Record<string, unknown> | undefined;
1460
+
1461
+ let snapshotData: NocturnalSessionSnapshot | undefined;
1399
1462
 
1400
1463
  if (isPollingTask) {
1401
1464
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: polling path requires existing resultRef
1402
1465
  workflowId = sleepTask.resultRef!;
1403
1466
  } else {
1404
- if (sleepTask.recentPainContext) {
1405
- try {
1406
- const extractor = createNocturnalTrajectoryExtractor(wctx.workspaceDir);
1407
- const fullSnapshot = extractor.getNocturnalSessionSnapshot(sleepTask.id);
1408
- if (fullSnapshot) {
1409
- snapshotData = {
1410
- sessionId: fullSnapshot.sessionId,
1411
- sessionStart: fullSnapshot.startedAt,
1412
- stats: fullSnapshot.stats,
1413
- recentPain: fullSnapshot.painEvents.slice(-5),
1414
- };
1467
+ // Phase 1: Build trajectory snapshot for Nocturnal pipeline
1468
+ // Priority: Pain signal sessionId → Task ID → Recent session with violations
1469
+ try {
1470
+ const extractor = createNocturnalTrajectoryExtractor(wctx.workspaceDir);
1471
+
1472
+ // 1. Try exact session ID from pain signal (most accurate)
1473
+ const painSessionId = sleepTask.recentPainContext?.mostRecent?.sessionId;
1474
+ let fullSnapshot = painSessionId ? extractor.getNocturnalSessionSnapshot(painSessionId) : undefined;
1475
+ if (fullSnapshot) {
1476
+ logger?.info?.(`[PD:EvolutionWorker] Task ${sleepTask.id} using exact session from pain signal: ${painSessionId}`);
1477
+ }
1478
+
1479
+ // 2. Try task ID (legacy compatibility, rarely matches)
1480
+ if (!fullSnapshot) {
1481
+ fullSnapshot = extractor.getNocturnalSessionSnapshot(sleepTask.id);
1482
+ }
1483
+
1484
+ // 3. If no match, find most recent session WITH violation signals
1485
+ if (!fullSnapshot) {
1486
+ const taskTimeMs = new Date(sleepTask.enqueued_at || sleepTask.timestamp).getTime();
1487
+ const recentSessions = extractor.listRecentNocturnalCandidateSessions({
1488
+ limit: 20,
1489
+ minToolCalls: 1,
1490
+ dateTo: sleepTask.enqueued_at || sleepTask.timestamp,
1491
+ }).filter((session) => isSessionAtOrBeforeTriggerTime(session, taskTimeMs));
1492
+ // Filter to sessions with actual violations (pain, failures, or gate blocks)
1493
+ const sessionsWithViolations = recentSessions.filter(
1494
+ s => s.failureCount > 0 || s.painEventCount > 0 || s.gateBlockCount > 0
1495
+ );
1496
+ if (sessionsWithViolations.length > 0) {
1497
+ const targetSession = sessionsWithViolations[0];
1498
+ logger?.info?.(`[PD:EvolutionWorker] Task ${sleepTask.id} using session with violations: ${targetSession.sessionId} (failed=${targetSession.failureCount}, pain=${targetSession.painEventCount}, gates=${targetSession.gateBlockCount})`);
1499
+ fullSnapshot = extractor.getNocturnalSessionSnapshot(targetSession.sessionId);
1500
+ } else if (recentSessions.length > 0) {
1501
+ // No sessions with violations, use most recent as last resort
1502
+ const latestSession = recentSessions[0];
1503
+ logger?.warn?.(`[PD:EvolutionWorker] Task ${sleepTask.id} no sessions with violations found, using most recent: ${latestSession.sessionId} (failed=${latestSession.failureCount}, pain=${latestSession.painEventCount}, gates=${latestSession.gateBlockCount})`);
1504
+ fullSnapshot = extractor.getNocturnalSessionSnapshot(latestSession.sessionId);
1505
+ } else {
1506
+ logger?.warn?.(`[PD:EvolutionWorker] Task ${sleepTask.id} no sessions with tool calls in trajectory DB`);
1415
1507
  }
1416
- } catch (snapErr) {
1417
- logger?.warn?.(`[PD:EvolutionWorker] Failed to build trajectory snapshot for ${sleepTask.id}: ${String(snapErr)}`);
1418
1508
  }
1509
+
1510
+ if (fullSnapshot) {
1511
+ snapshotData = fullSnapshot;
1512
+ }
1513
+ } catch (snapErr) {
1514
+ logger?.warn?.(`[PD:EvolutionWorker] Failed to build trajectory snapshot for ${sleepTask.id}: ${String(snapErr)}`);
1419
1515
  }
1516
+
1517
+ // Phase 2: If no trajectory data, try pain-context fallback
1420
1518
  if (!snapshotData && sleepTask.recentPainContext) {
1421
- logger?.warn?.(`[PD:EvolutionWorker] Using pain-context fallback for ${sleepTask.id}: trajectory stats unavailable (stats will be partial)`);
1422
- snapshotData = {
1423
- sessionId: sleepTask.id,
1424
- sessionStart: sleepTask.timestamp,
1425
- stats: {
1426
- totalAssistantTurns: 0,
1427
- totalToolCalls: 0,
1428
- failureCount: 0,
1429
- totalPainEvents: sleepTask.recentPainContext.recentPainCount,
1430
- totalGateBlocks: 0,
1431
- },
1432
- recentPain: sleepTask.recentPainContext.mostRecent ? [sleepTask.recentPainContext.mostRecent] : [],
1433
- _dataSource: 'pain_context_fallback',
1434
- };
1519
+ logger?.warn?.(`[PD:EvolutionWorker] Using pain-context fallback for ${sleepTask.id}: trajectory stats unavailable (stats will be null)`);
1520
+ snapshotData = buildFallbackNocturnalSnapshot(sleepTask) ?? undefined;
1435
1521
  }
1436
1522
 
1437
- if (!hasUsableNocturnalSnapshot(snapshotData)) {
1523
+ const snapshotValidation = validateNocturnalSnapshotIngress(snapshotData);
1524
+ if (snapshotValidation.status !== 'valid') {
1438
1525
  sleepTask.status = 'failed';
1439
1526
  sleepTask.completed_at = new Date().toISOString();
1440
1527
  sleepTask.resolution = 'failed_max_retries';
1441
- sleepTask.lastError = 'sleep_reflection failed: missing_usable_snapshot (skipReason: empty_fallback_snapshot)';
1528
+ sleepTask.lastError = `sleep_reflection failed: invalid_snapshot_ingress (${snapshotValidation.reasons.join('; ') || 'missing snapshot'})`;
1442
1529
  sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
1443
- logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} rejected: missing usable snapshot`);
1530
+ logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} rejected: ${sleepTask.lastError}`);
1444
1531
  continue;
1445
1532
  }
1533
+
1534
+ snapshotData = snapshotValidation.snapshot;
1446
1535
  }
1447
1536
 
1448
1537
  if (!api) {
@@ -1506,24 +1595,35 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1506
1595
  const errorReason = lastEvent?.reason ?? 'unknown';
1507
1596
  // #219: Include payload details for better diagnostics
1508
1597
  let detailedError = `Workflow terminal_error: ${errorReason}`;
1598
+ let payload: unknown = {};
1509
1599
  try {
1510
- const payload = lastEvent?.payload ?? {};
1511
- if (payload.skipReason) {
1512
- detailedError += ` (skipReason: ${payload.skipReason})`;
1600
+ payload = lastEvent?.payload ?? {};
1601
+ if ((payload as any).skipReason) {
1602
+ detailedError += ` (skipReason: ${(payload as any).skipReason})`;
1513
1603
  }
1514
- if (payload.failures && Array.isArray(payload.failures) && payload.failures.length > 0) {
1515
- detailedError += ` | failures: ${(payload.failures as string[]).slice(0, 3).join(', ')}`;
1604
+ if ((payload as any).failures && Array.isArray((payload as any).failures) && (payload as any).failures.length > 0) {
1605
+ detailedError += ` | failures: ${((payload as any).failures as string[]).slice(0, 3).join(', ')}`;
1516
1606
  }
1517
1607
  } catch { /* ignore parse errors */ }
1518
1608
  sleepTask.lastError = detailedError;
1519
1609
  sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
1520
1610
 
1521
- sleepTask.status = 'failed';
1522
- sleepTask.completed_at = new Date().toISOString();
1523
- sleepTask.resolution = 'failed_max_retries';
1524
1611
  if (isExpectedSubagentError(errorReason)) {
1525
- logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable: ${errorReason}`);
1612
+ // #237: Expected unavailability stub fallback, not hard failure
1613
+ sleepTask.status = 'completed';
1614
+ sleepTask.completed_at = new Date().toISOString();
1615
+ sleepTask.resolution = 'stub_fallback';
1616
+ logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable, using stub fallback: ${errorReason}`);
1617
+ } else if ((payload as any).skipReason === 'no_violating_sessions') {
1618
+ // #244: No meaningful violations found (thin filter) → skip without failure
1619
+ sleepTask.status = 'completed';
1620
+ sleepTask.completed_at = new Date().toISOString();
1621
+ sleepTask.resolution = 'skipped_thin_violation';
1622
+ logger?.info?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} completed: no sessions with meaningful violations found`);
1526
1623
  } else {
1624
+ sleepTask.status = 'failed';
1625
+ sleepTask.completed_at = new Date().toISOString();
1626
+ sleepTask.resolution = 'failed_max_retries';
1527
1627
  logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} workflow failed: ${sleepTask.lastError}`);
1528
1628
  }
1529
1629
  } else {
@@ -1535,14 +1635,20 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1535
1635
  // #202: Handle expected subagent unavailability (e.g., process isolation in daemon mode)
1536
1636
  // When subagent is unavailable due to gateway running in separate process,
1537
1637
  // use stub fallback instead of failing the task.
1538
- sleepTask.status = 'failed';
1539
1638
  sleepTask.completed_at = new Date().toISOString();
1540
- sleepTask.resolution = 'failed_max_retries';
1541
1639
  sleepTask.lastError = String(taskErr);
1542
1640
  sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
1641
+
1543
1642
  if (isExpectedSubagentError(taskErr)) {
1544
- logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable: ${String(taskErr)}`);
1643
+ // #237: Expected unavailability stub fallback, not hard failure
1644
+ sleepTask.status = 'completed';
1645
+ sleepTask.completed_at = new Date().toISOString();
1646
+ sleepTask.resolution = 'stub_fallback';
1647
+ logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable, using stub fallback: ${String(taskErr)}`);
1545
1648
  } else {
1649
+ sleepTask.status = 'failed';
1650
+ sleepTask.completed_at = new Date().toISOString();
1651
+ sleepTask.resolution = 'failed_max_retries';
1546
1652
  logger?.error?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} threw: ${taskErr}`);
1547
1653
  }
1548
1654
  }
@@ -1677,7 +1783,7 @@ async function processDetectionQueue(wctx: WorkspaceContext, api: OpenClawPlugin
1677
1783
  // PAIN_CANDIDATES system removed (D-05, D-06): trackPainCandidate and processPromotion deleted
1678
1784
  // Evolution queue is now the single active pain→principle path
1679
1785
 
1680
- /* eslint-disable no-unused-vars, @typescript-eslint/max-params -- Reason: type-level function parameter names in logger union type and unused workspaceResolve key are documentation/signature */
1786
+
1681
1787
  export async function registerEvolutionTaskSession(
1682
1788
  workspaceResolve: (key: string) => string,
1683
1789
  taskId: string,
@@ -1690,7 +1796,7 @@ export async function registerEvolutionTaskSession(
1690
1796
  const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
1691
1797
 
1692
1798
  try {
1693
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has early return
1799
+
1694
1800
  let rawQueue: RawQueueItem[];
1695
1801
  try {
1696
1802
  rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
@@ -1730,14 +1836,14 @@ export async function registerEvolutionTaskSession(
1730
1836
  * Production evidence shows directive stopped updating on 2026-03-22 and is stale.
1731
1837
  */
1732
1838
 
1733
- /* eslint-disable no-unused-vars -- Reason: interface method parameters are type signatures */
1839
+
1734
1840
  export interface ExtendedEvolutionWorkerService {
1735
1841
  id: string;
1736
1842
  api: OpenClawPluginApi | null;
1737
1843
  start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
1738
1844
  stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
1739
1845
  }
1740
- /* eslint-enable no-unused-vars */
1846
+
1741
1847
 
1742
1848
  interface WorkerStatusReport {
1743
1849
  timestamp: string;
@@ -1752,13 +1858,12 @@ function writeWorkerStatus(stateDir: string, report: WorkerStatusReport): void {
1752
1858
  try {
1753
1859
  const statusPath = path.join(stateDir, 'worker-status.json');
1754
1860
  fs.writeFileSync(statusPath, JSON.stringify(report, null, 2), 'utf8');
1755
- } catch (err) {
1756
- // Non-critical: worker-status.json is for monitoring, not core logic
1757
- console.warn(`[PD:EvolutionWorker] Failed to write worker-status.json: ${String(err)}`);
1861
+ } catch {
1862
+ // Non-critical: worker-status.json is for monitoring, failure is acceptable
1758
1863
  }
1759
1864
  }
1760
1865
 
1761
- /* eslint-disable @typescript-eslint/max-params -- Reason: Function requires all parameters for queue processing */
1866
+
1762
1867
  async function processEvolutionQueueWithResult(
1763
1868
  wctx: WorkspaceContext,
1764
1869
  logger: PluginLogger,
@@ -1831,6 +1936,13 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
1831
1936
 
1832
1937
  async function runCycle(): Promise<void> {
1833
1938
  const cycleStart = Date.now();
1939
+
1940
+ // ──── DEBUG: Verify subagent availability in heartbeat context ────
1941
+ const hbSubagent = api?.runtime?.subagent;
1942
+ logger?.info?.(`[PD:DEBUG:SubagentCheck:Heartbeat] api_exists=${!!api}, subagent_exists=${!!hbSubagent}, subagent.run_exists=${!!hbSubagent?.run}`);
1943
+ if (hbSubagent?.run) {
1944
+ logger?.info?.('[PD:DEBUG:SubagentCheck:Heartbeat] run entrypoint is callable');
1945
+ }
1834
1946
  const cycleResult: {
1835
1947
  timestamp: string;
1836
1948
  cycle_start_ms: number;