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.
- package/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +4 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +480 -158
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +221 -109
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +11 -4
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- 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 {
|
|
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 ===
|
|
131
|
-
details.push(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
485
|
-
if (
|
|
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
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1458
|
+
|
|
1396
1459
|
let nocturnalManager: NocturnalWorkflowManager;
|
|
1397
|
-
|
|
1398
|
-
let snapshotData:
|
|
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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1756
|
-
// Non-critical: worker-status.json is for monitoring,
|
|
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
|
-
|
|
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;
|