principles-disciple 1.16.0 → 1.17.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 +3 -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 +27 -28
- 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 +209 -104
- 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 +2 -2
- 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/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 +562 -6
- 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';
|
|
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) {
|
|
@@ -1518,12 +1607,16 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1518
1607
|
sleepTask.lastError = detailedError;
|
|
1519
1608
|
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
1520
1609
|
|
|
1521
|
-
sleepTask.status = 'failed';
|
|
1522
|
-
sleepTask.completed_at = new Date().toISOString();
|
|
1523
|
-
sleepTask.resolution = 'failed_max_retries';
|
|
1524
1610
|
if (isExpectedSubagentError(errorReason)) {
|
|
1525
|
-
|
|
1611
|
+
// #237: Expected unavailability → stub fallback, not hard failure
|
|
1612
|
+
sleepTask.status = 'completed';
|
|
1613
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
1614
|
+
sleepTask.resolution = 'stub_fallback';
|
|
1615
|
+
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable, using stub fallback: ${errorReason}`);
|
|
1526
1616
|
} else {
|
|
1617
|
+
sleepTask.status = 'failed';
|
|
1618
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
1619
|
+
sleepTask.resolution = 'failed_max_retries';
|
|
1527
1620
|
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} workflow failed: ${sleepTask.lastError}`);
|
|
1528
1621
|
}
|
|
1529
1622
|
} else {
|
|
@@ -1535,14 +1628,20 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1535
1628
|
// #202: Handle expected subagent unavailability (e.g., process isolation in daemon mode)
|
|
1536
1629
|
// When subagent is unavailable due to gateway running in separate process,
|
|
1537
1630
|
// use stub fallback instead of failing the task.
|
|
1538
|
-
sleepTask.status = 'failed';
|
|
1539
1631
|
sleepTask.completed_at = new Date().toISOString();
|
|
1540
|
-
sleepTask.resolution = 'failed_max_retries';
|
|
1541
1632
|
sleepTask.lastError = String(taskErr);
|
|
1542
1633
|
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
1634
|
+
|
|
1543
1635
|
if (isExpectedSubagentError(taskErr)) {
|
|
1544
|
-
|
|
1636
|
+
// #237: Expected unavailability → stub fallback, not hard failure
|
|
1637
|
+
sleepTask.status = 'completed';
|
|
1638
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
1639
|
+
sleepTask.resolution = 'stub_fallback';
|
|
1640
|
+
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} background runtime unavailable, using stub fallback: ${String(taskErr)}`);
|
|
1545
1641
|
} else {
|
|
1642
|
+
sleepTask.status = 'failed';
|
|
1643
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
1644
|
+
sleepTask.resolution = 'failed_max_retries';
|
|
1546
1645
|
logger?.error?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} threw: ${taskErr}`);
|
|
1547
1646
|
}
|
|
1548
1647
|
}
|
|
@@ -1677,7 +1776,7 @@ async function processDetectionQueue(wctx: WorkspaceContext, api: OpenClawPlugin
|
|
|
1677
1776
|
// PAIN_CANDIDATES system removed (D-05, D-06): trackPainCandidate and processPromotion deleted
|
|
1678
1777
|
// Evolution queue is now the single active pain→principle path
|
|
1679
1778
|
|
|
1680
|
-
|
|
1779
|
+
|
|
1681
1780
|
export async function registerEvolutionTaskSession(
|
|
1682
1781
|
workspaceResolve: (key: string) => string,
|
|
1683
1782
|
taskId: string,
|
|
@@ -1690,7 +1789,7 @@ export async function registerEvolutionTaskSession(
|
|
|
1690
1789
|
const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
|
|
1691
1790
|
|
|
1692
1791
|
try {
|
|
1693
|
-
|
|
1792
|
+
|
|
1694
1793
|
let rawQueue: RawQueueItem[];
|
|
1695
1794
|
try {
|
|
1696
1795
|
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
@@ -1730,14 +1829,14 @@ export async function registerEvolutionTaskSession(
|
|
|
1730
1829
|
* Production evidence shows directive stopped updating on 2026-03-22 and is stale.
|
|
1731
1830
|
*/
|
|
1732
1831
|
|
|
1733
|
-
|
|
1832
|
+
|
|
1734
1833
|
export interface ExtendedEvolutionWorkerService {
|
|
1735
1834
|
id: string;
|
|
1736
1835
|
api: OpenClawPluginApi | null;
|
|
1737
1836
|
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
1738
1837
|
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
1739
1838
|
}
|
|
1740
|
-
|
|
1839
|
+
|
|
1741
1840
|
|
|
1742
1841
|
interface WorkerStatusReport {
|
|
1743
1842
|
timestamp: string;
|
|
@@ -1752,13 +1851,12 @@ function writeWorkerStatus(stateDir: string, report: WorkerStatusReport): void {
|
|
|
1752
1851
|
try {
|
|
1753
1852
|
const statusPath = path.join(stateDir, 'worker-status.json');
|
|
1754
1853
|
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)}`);
|
|
1854
|
+
} catch {
|
|
1855
|
+
// Non-critical: worker-status.json is for monitoring, failure is acceptable
|
|
1758
1856
|
}
|
|
1759
1857
|
}
|
|
1760
1858
|
|
|
1761
|
-
|
|
1859
|
+
|
|
1762
1860
|
async function processEvolutionQueueWithResult(
|
|
1763
1861
|
wctx: WorkspaceContext,
|
|
1764
1862
|
logger: PluginLogger,
|
|
@@ -1831,6 +1929,13 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
|
|
|
1831
1929
|
|
|
1832
1930
|
async function runCycle(): Promise<void> {
|
|
1833
1931
|
const cycleStart = Date.now();
|
|
1932
|
+
|
|
1933
|
+
// ──── DEBUG: Verify subagent availability in heartbeat context ────
|
|
1934
|
+
const hbSubagent = api?.runtime?.subagent;
|
|
1935
|
+
logger?.info?.(`[PD:DEBUG:SubagentCheck:Heartbeat] api_exists=${!!api}, subagent_exists=${!!hbSubagent}, subagent.run_exists=${!!hbSubagent?.run}`);
|
|
1936
|
+
if (hbSubagent?.run) {
|
|
1937
|
+
logger?.info?.('[PD:DEBUG:SubagentCheck:Heartbeat] run entrypoint is callable');
|
|
1938
|
+
}
|
|
1834
1939
|
const cycleResult: {
|
|
1835
1940
|
timestamp: string;
|
|
1836
1941
|
cycle_start_ms: number;
|