principles-disciple 1.27.0 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -4
- package/scripts/diagnose-nocturnal.mjs +139 -2
- package/scripts/seed-nocturnal-scenarios.mjs +377 -0
- package/scripts/validate-live-path.ts +18 -18
- package/src/commands/nocturnal-train.ts +4 -6
- package/src/commands/pain.ts +8 -11
- package/src/commands/pd-reflect.ts +1 -1
- package/src/core/bootstrap-rules.ts +3 -3
- package/src/core/merge-gate-audit.ts +1 -1
- package/src/core/nocturnal-candidate-scoring.ts +131 -0
- package/src/core/nocturnal-reasoning-deriver.ts +337 -0
- package/src/core/nocturnal-trinity.ts +462 -25
- package/src/core/pain-context-extractor.ts +1 -3
- package/src/core/principle-tree-migration.ts +2 -4
- package/src/core/thinking-os-parser.ts +3 -3
- package/src/hooks/bash-risk.ts +1 -1
- package/src/hooks/gfi-gate.ts +1 -1
- package/src/hooks/pain.ts +1 -1
- package/src/hooks/prompt.ts +36 -2
- package/src/hooks/subagent.ts +1 -1
- package/src/index.ts +3 -1
- package/src/service/evolution-worker.ts +138 -44
- package/src/service/health-query-service.ts +15 -6
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -1
- package/src/tools/write-pain-flag.ts +191 -0
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +34 -20
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +34 -20
- package/tests/core/nocturnal-candidate-scoring.test.ts +132 -0
- package/tests/core/nocturnal-e2e.test.ts +224 -0
- package/tests/core/nocturnal-reasoning-deriver.test.ts +372 -0
- package/tests/core/nocturnal-trinity.test.ts +791 -0
- package/tests/tools/write-pain-flag.test.ts +240 -0
|
@@ -235,11 +235,9 @@ export async function extractRecentConversation(
|
|
|
235
235
|
/**
|
|
236
236
|
* Extracts failed tool call context with argument correlation.
|
|
237
237
|
*/
|
|
238
|
-
// Reason: breaking API change - default param must precede required params for type inference compatibility
|
|
239
|
-
|
|
240
238
|
export async function extractFailedToolContext(
|
|
241
239
|
sessionId: string,
|
|
242
|
-
agentId
|
|
240
|
+
agentId: string,
|
|
243
241
|
toolName: string,
|
|
244
242
|
filePath?: string,
|
|
245
243
|
): Promise<string> {
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* - Or run manually: node scripts/migrate-principle-tree.mjs <workspace-dir>
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
12
|
import {
|
|
15
13
|
loadLedger,
|
|
16
14
|
saveLedger,
|
|
@@ -23,11 +21,11 @@ export interface PrincipleTreeMigrationResult {
|
|
|
23
21
|
migratedCount: number;
|
|
24
22
|
skippedCount: number;
|
|
25
23
|
errorCount: number;
|
|
26
|
-
details:
|
|
24
|
+
details: {
|
|
27
25
|
principleId: string;
|
|
28
26
|
status: 'migrated' | 'skipped' | 'error';
|
|
29
27
|
reason?: string;
|
|
30
|
-
}
|
|
28
|
+
}[];
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
/**
|
|
@@ -45,10 +45,10 @@ export function parseThinkingOsMd(content: string): ThinkingOsDirective[] {
|
|
|
45
45
|
// Match all <directive ...> ... </directive> blocks
|
|
46
46
|
const directiveRegex = /<directive\s+([^>]*)>([\s\S]*?)<\/directive>/gi;
|
|
47
47
|
|
|
48
|
-
let
|
|
48
|
+
let _match: RegExpExecArray | null = null;
|
|
49
49
|
|
|
50
|
-
while ((
|
|
51
|
-
const [, attrs, body] =
|
|
50
|
+
while ((_match = directiveRegex.exec(content)) !== null) {
|
|
51
|
+
const [, attrs, body] = _match;
|
|
52
52
|
|
|
53
53
|
const idMatch = /id="([^"]+)"/i.exec(attrs);
|
|
54
54
|
const nameMatch = /name="([^"]+)"/i.exec(attrs);
|
package/src/hooks/bash-risk.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function analyzeBashCommand(
|
|
|
66
66
|
// - Word joiner (U+2060)
|
|
67
67
|
// - Zero-width invisible separator (U+FEFF)
|
|
68
68
|
|
|
69
|
-
const ZERO_WIDTH_CHARS =
|
|
69
|
+
const ZERO_WIDTH_CHARS = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
70
70
|
if (ZERO_WIDTH_CHARS.test(command)) {
|
|
71
71
|
logger?.warn?.(`[PD_GATE] Bash command contains zero-width characters — blocking as dangerous`);
|
|
72
72
|
return 'dangerous'; // Fail-closed: zero-width chars are suspicious
|
package/src/hooks/gfi-gate.ts
CHANGED
package/src/hooks/pain.ts
CHANGED
|
@@ -191,7 +191,7 @@ export function handleAfterToolCall(
|
|
|
191
191
|
// Only reduce tool_failure source GFI by 50%, preserve user_empathy and other sources
|
|
192
192
|
// This prevents "read file success" from wiping user frustration signals
|
|
193
193
|
const session = getSession(sessionId);
|
|
194
|
-
const toolFailureGfi = session?.gfiBySource?.
|
|
194
|
+
const toolFailureGfi = session?.gfiBySource?.tool_failure || 0;
|
|
195
195
|
|
|
196
196
|
let resetState: SessionState;
|
|
197
197
|
if (toolFailureGfi > 0) {
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingM
|
|
|
10
10
|
import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
|
|
11
11
|
import { PathResolver } from '../core/path-resolver.js';
|
|
12
12
|
import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
|
|
13
|
+
import { getPendingDiagnosticianTasks } from '../core/diagnostician-task-store.js';
|
|
13
14
|
import {
|
|
14
15
|
matchEmpathyKeywords,
|
|
15
16
|
loadKeywordStore,
|
|
@@ -366,7 +367,7 @@ export async function handleBeforePromptBuild(
|
|
|
366
367
|
// prependContext: Only short dynamic directives: evolutionDirective + heartbeat
|
|
367
368
|
|
|
368
369
|
|
|
369
|
-
let prependSystemContext
|
|
370
|
+
let prependSystemContext: string;
|
|
370
371
|
let prependContext = '';
|
|
371
372
|
let appendSystemContext = '';
|
|
372
373
|
|
|
@@ -644,11 +645,44 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
644
645
|
logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
|
|
645
646
|
}
|
|
646
647
|
}
|
|
648
|
+
|
|
649
|
+
// ──── 4b. Inject pending diagnostician tasks ────
|
|
650
|
+
// FIX (#283): The evolution worker writes pain diagnosis tasks to
|
|
651
|
+
// diagnostician_tasks.json. The heartbeat prompt hook must read and inject
|
|
652
|
+
// them so the LLM (acting as diagnostician) can process them.
|
|
653
|
+
try {
|
|
654
|
+
const pendingTasks = getPendingDiagnosticianTasks(wctx.stateDir);
|
|
655
|
+
if (pendingTasks.length > 0) {
|
|
656
|
+
const taskBlocks = pendingTasks
|
|
657
|
+
.slice(0, 3)
|
|
658
|
+
.map(({ id, task }) => `<diagnostician_task id="${id}">\n${task.prompt}\n</diagnostician_task>`)
|
|
659
|
+
.join('\n\n');
|
|
660
|
+
|
|
661
|
+
const pendingCount = pendingTasks.length;
|
|
662
|
+
const processingNote = pendingCount > 3
|
|
663
|
+
? `\n\nNOTE: ${pendingCount - 3} more tasks are queued. Process these 3 first; remaining tasks will be handled on subsequent heartbeats.`
|
|
664
|
+
: '';
|
|
665
|
+
|
|
666
|
+
prependContext += `<diagnostician_tasks pending="${pendingCount}">
|
|
667
|
+
You are acting as a **Pain Diagnostician**. Process the following task(s) by:
|
|
668
|
+
1. Analyzing the pain signal and its context
|
|
669
|
+
2. Identifying the root cause and violated principles
|
|
670
|
+
3. Writing a completion marker file: .evolution_complete_<TASK_ID>
|
|
671
|
+
4. Writing a diagnostic report: .diagnostician_report_<TASK_ID>.json
|
|
672
|
+
|
|
673
|
+
${taskBlocks}${processingNote}
|
|
674
|
+
</diagnostician_tasks>\n`;
|
|
675
|
+
|
|
676
|
+
logger?.info?.(`[PD:Prompt] Injected ${Math.min(pendingCount, 3)}/${pendingCount} pending diagnostician task(s) into heartbeat prompt`);
|
|
677
|
+
}
|
|
678
|
+
} catch (e) {
|
|
679
|
+
logger?.warn?.(`[PD:Prompt] Failed to read diagnostician tasks: ${String(e)}`);
|
|
680
|
+
}
|
|
647
681
|
}
|
|
648
682
|
|
|
649
683
|
// ──── 6. Dynamic Attitude Matrix (based on GFI) ────
|
|
650
684
|
|
|
651
|
-
let attitudeDirective
|
|
685
|
+
let attitudeDirective: string;
|
|
652
686
|
const currentGfi = session?.currentGfi || 0;
|
|
653
687
|
|
|
654
688
|
if (currentGfi >= 70) {
|
package/src/hooks/subagent.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { ensureWorkspaceTemplates } from './core/init.js';
|
|
|
57
57
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
58
58
|
import { SystemLogger } from './core/system-logger.js';
|
|
59
59
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
60
|
+
import { createWritePainFlagTool } from './tools/write-pain-flag.js';
|
|
60
61
|
import { PathResolver, resolveWorkspaceDirFromApi } from './core/path-resolver.js';
|
|
61
62
|
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
62
63
|
import { resolveRequiredWorkspaceDir, resolveWorkspaceDir, type WorkspaceResolutionContext } from './core/workspace-dir-service.js';
|
|
@@ -117,7 +118,7 @@ function computeRuntimeShadowTaskFingerprint(event: PluginHookSubagentSpawningEv
|
|
|
117
118
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
function
|
|
121
|
+
function _resolveCommandWorkspaceDirStrict(
|
|
121
122
|
api: OpenClawPluginApi,
|
|
122
123
|
ctx: WorkspaceResolutionContext,
|
|
123
124
|
): string {
|
|
@@ -773,6 +774,7 @@ const plugin = {
|
|
|
773
774
|
});
|
|
774
775
|
|
|
775
776
|
api.registerTool(createDeepReflectTool(api));
|
|
777
|
+
api.registerTool(createWritePainFlagTool(api));
|
|
776
778
|
}
|
|
777
779
|
};
|
|
778
780
|
|
|
@@ -356,7 +356,8 @@ function isSessionAtOrBeforeTriggerTime(
|
|
|
356
356
|
|
|
357
357
|
function buildFallbackNocturnalSnapshot(
|
|
358
358
|
sleepTask: EvolutionQueueItem,
|
|
359
|
-
extractor?: ReturnType<typeof createNocturnalTrajectoryExtractor> | null
|
|
359
|
+
extractor?: ReturnType<typeof createNocturnalTrajectoryExtractor> | null,
|
|
360
|
+
logger?: { warn?: (message: string) => void }
|
|
360
361
|
): NocturnalSessionSnapshot | null {
|
|
361
362
|
const painContext = sleepTask.recentPainContext;
|
|
362
363
|
if (!painContext) {
|
|
@@ -381,7 +382,7 @@ function buildFallbackNocturnalSnapshot(
|
|
|
381
382
|
// #246-fix: Use minToolCalls=0 to avoid filtering out sessions with 0 tool calls.
|
|
382
383
|
// The pain-triggering session may have no tool calls but still be worth tracking.
|
|
383
384
|
const summaries = extractor.listRecentNocturnalCandidateSessions({ limit: 300, minToolCalls: 0 });
|
|
384
|
-
const match = summaries.find(s => s.sessionId === painContext.mostRecent
|
|
385
|
+
const match = summaries.find(s => s.sessionId === painContext.mostRecent?.sessionId);
|
|
385
386
|
if (match) {
|
|
386
387
|
realStats = {
|
|
387
388
|
totalAssistantTurns: match.assistantTurnCount,
|
|
@@ -390,8 +391,10 @@ function buildFallbackNocturnalSnapshot(
|
|
|
390
391
|
totalGateBlocks: match.gateBlockCount,
|
|
391
392
|
};
|
|
392
393
|
}
|
|
393
|
-
} catch {
|
|
394
|
-
//
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// #260: Log extraction failures — silent swallowing makes debugging impossible
|
|
396
|
+
// and can mask systemic trajectory DB issues.
|
|
397
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to extract real stats for session ${painContext.mostRecent?.sessionId} (falling back to zeros): ${String(err)}`);
|
|
395
398
|
}
|
|
396
399
|
}
|
|
397
400
|
|
|
@@ -590,63 +593,154 @@ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext
|
|
|
590
593
|
return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
|
|
591
594
|
}
|
|
592
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Build a dedup key from pain context.
|
|
598
|
+
* Returns null when no pain context is available (bypasses dedup).
|
|
599
|
+
*/
|
|
600
|
+
function buildPainSourceKey(
|
|
601
|
+
painCtx: ReturnType<typeof readRecentPainContext>,
|
|
602
|
+
): string | null {
|
|
603
|
+
if (!painCtx.mostRecent) return null;
|
|
604
|
+
return `${painCtx.mostRecent.source}::${painCtx.mostRecent.reason?.slice(0, 50) ?? ''}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Check whether a similar sleep_reflection task completed recently.
|
|
609
|
+
* Phase 3c: Prevents redundant reflections of the same underlying issue.
|
|
610
|
+
*/
|
|
611
|
+
function hasRecentSimilarReflection(
|
|
612
|
+
queue: EvolutionQueueItem[],
|
|
613
|
+
painSourceKey: string,
|
|
614
|
+
now: number,
|
|
615
|
+
): EvolutionQueueItem | null {
|
|
616
|
+
const DEDUP_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
617
|
+
return queue.find((t) => {
|
|
618
|
+
if (t.taskKind !== 'sleep_reflection') return false;
|
|
619
|
+
// Only match completed tasks (exclude failed to allow retries)
|
|
620
|
+
if (t.status !== 'completed') return false;
|
|
621
|
+
if (!t.completed_at) return false;
|
|
622
|
+
const age = now - new Date(t.completed_at).getTime();
|
|
623
|
+
if (age > DEDUP_WINDOW_MS) return false;
|
|
624
|
+
const taskPainKey = buildPainSourceKey(t.recentPainContext ?? { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 });
|
|
625
|
+
// If either side has no pain context, they don't match
|
|
626
|
+
if (!taskPainKey) return false;
|
|
627
|
+
return taskPainKey === painSourceKey;
|
|
628
|
+
}) ?? null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Check whether a specific task kind has a pending or in-progress entry.
|
|
633
|
+
*/
|
|
634
|
+
function hasPendingTask(queue: EvolutionQueueItem[], taskKind: string): boolean {
|
|
635
|
+
return queue.some(
|
|
636
|
+
(t) => t.taskKind === taskKind && (t.status === 'pending' || t.status === 'in_progress'),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Decide whether to skip enqueuing due to a recent similar reflection.
|
|
642
|
+
* Returns true if skipped (with log), false if should proceed.
|
|
643
|
+
*/
|
|
644
|
+
function shouldSkipForDedup(
|
|
645
|
+
queue: EvolutionQueueItem[],
|
|
646
|
+
wctx: WorkspaceContext,
|
|
647
|
+
logger: PluginLogger,
|
|
648
|
+
): boolean {
|
|
649
|
+
const recentPainContext = readRecentPainContext(wctx);
|
|
650
|
+
const painSourceKey = buildPainSourceKey(recentPainContext);
|
|
651
|
+
|
|
652
|
+
// Bypass dedup when there is no pain context — general idle reflections
|
|
653
|
+
// should not be throttled by the 'no_pain_context' sentinel.
|
|
654
|
+
if (!painSourceKey) return false;
|
|
655
|
+
|
|
656
|
+
const now = Date.now();
|
|
657
|
+
const recentSimilarReflection = hasRecentSimilarReflection(queue, painSourceKey, now);
|
|
658
|
+
|
|
659
|
+
if (recentSimilarReflection) {
|
|
660
|
+
const completedTime = new Date(recentSimilarReflection.completed_at!).getTime();
|
|
661
|
+
logger?.debug?.(`[PD:EvolutionWorker] Skipping sleep_reflection — similar reflection completed ${Math.round((now - completedTime) / 60000)}min ago (same pain pattern: ${painSourceKey})`);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Load and migrate the evolution queue. Returns empty array if file doesn't exist.
|
|
669
|
+
*/
|
|
670
|
+
function loadEvolutionQueue(queuePath: string): EvolutionQueueItem[] {
|
|
671
|
+
let rawQueue: RawQueueItem[] = [];
|
|
672
|
+
try {
|
|
673
|
+
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
674
|
+
} catch {
|
|
675
|
+
// Queue doesn't exist yet - create empty array
|
|
676
|
+
rawQueue = [];
|
|
677
|
+
}
|
|
678
|
+
return migrateQueueToV2(rawQueue);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Build and persist a new sleep_reflection task.
|
|
683
|
+
*/
|
|
684
|
+
function enqueueNewSleepReflectionTask(
|
|
685
|
+
queue: EvolutionQueueItem[],
|
|
686
|
+
recentPainContext: ReturnType<typeof readRecentPainContext>,
|
|
687
|
+
queuePath: string,
|
|
688
|
+
logger: PluginLogger,
|
|
689
|
+
): void {
|
|
690
|
+
const taskId = createEvolutionTaskId('nocturnal', 50, 'idle workspace', 'Sleep-mode reflection', Date.now());
|
|
691
|
+
const nowIso = new Date().toISOString();
|
|
692
|
+
|
|
693
|
+
queue.push({
|
|
694
|
+
id: taskId,
|
|
695
|
+
taskKind: 'sleep_reflection',
|
|
696
|
+
priority: 'medium',
|
|
697
|
+
score: 50,
|
|
698
|
+
source: 'nocturnal',
|
|
699
|
+
reason: 'Sleep-mode reflection triggered by idle workspace',
|
|
700
|
+
trigger_text_preview: 'Idle workspace detected',
|
|
701
|
+
timestamp: nowIso,
|
|
702
|
+
enqueued_at: nowIso,
|
|
703
|
+
status: 'pending',
|
|
704
|
+
traceId: taskId,
|
|
705
|
+
retryCount: 0,
|
|
706
|
+
maxRetries: 1, // sleep_reflection doesn't retry
|
|
707
|
+
recentPainContext,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
711
|
+
logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
|
|
712
|
+
}
|
|
713
|
+
|
|
593
714
|
/**
|
|
594
715
|
* Enqueue a sleep_reflection task if one is not already pending.
|
|
595
716
|
* Phase 2.4: Called when workspace is idle to trigger nocturnal reflection.
|
|
717
|
+
* Phase 3c: Dedup checks recent sleep_reflection tasks by pain source pattern
|
|
718
|
+
* to prevent redundant reflections of the same underlying issue.
|
|
596
719
|
*/
|
|
597
720
|
async function enqueueSleepReflectionTask(
|
|
598
721
|
wctx: WorkspaceContext,
|
|
599
|
-
logger: PluginLogger
|
|
722
|
+
logger: PluginLogger,
|
|
600
723
|
): Promise<void> {
|
|
601
724
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
602
725
|
const releaseLock = await requireQueueLock(queuePath, logger, 'enqueueSleepReflection', EVOLUTION_QUEUE_LOCK_SUFFIX);
|
|
603
726
|
|
|
604
727
|
try {
|
|
605
|
-
|
|
606
|
-
try {
|
|
607
|
-
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
608
|
-
} catch {
|
|
609
|
-
// Queue doesn't exist yet - create empty array
|
|
610
|
-
rawQueue = [];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const queue: EvolutionQueueItem[] = migrateQueueToV2(rawQueue);
|
|
728
|
+
const queue = loadEvolutionQueue(queuePath);
|
|
614
729
|
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
t => t.taskKind === 'sleep_reflection' && (t.status === 'pending' || t.status === 'in_progress')
|
|
618
|
-
);
|
|
619
|
-
if (hasPendingSleepReflection) {
|
|
730
|
+
// Guard 1: Skip if a sleep_reflection task is already pending/in-progress
|
|
731
|
+
if (hasPendingTask(queue, 'sleep_reflection')) {
|
|
620
732
|
logger?.debug?.('[PD:EvolutionWorker] sleep_reflection task already pending/in-progress, skipping');
|
|
621
733
|
return;
|
|
622
734
|
}
|
|
623
735
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
736
|
+
// Guard 2: Dedup — skip if similar reflection completed recently
|
|
737
|
+
if (shouldSkipForDedup(queue, wctx, logger)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
627
740
|
|
|
628
|
-
//
|
|
741
|
+
// Enqueue the new task
|
|
629
742
|
const recentPainContext = readRecentPainContext(wctx);
|
|
630
|
-
|
|
631
|
-
queue.push({
|
|
632
|
-
id: taskId,
|
|
633
|
-
taskKind: 'sleep_reflection',
|
|
634
|
-
priority: 'medium',
|
|
635
|
-
score: 50,
|
|
636
|
-
source: 'nocturnal',
|
|
637
|
-
reason: 'Sleep-mode reflection triggered by idle workspace',
|
|
638
|
-
trigger_text_preview: 'Idle workspace detected',
|
|
639
|
-
timestamp: nowIso,
|
|
640
|
-
enqueued_at: nowIso,
|
|
641
|
-
status: 'pending',
|
|
642
|
-
traceId: taskId,
|
|
643
|
-
retryCount: 0,
|
|
644
|
-
maxRetries: 1, // sleep_reflection doesn't retry
|
|
645
|
-
recentPainContext,
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
649
|
-
logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
|
|
743
|
+
enqueueNewSleepReflectionTask(queue, recentPainContext, queuePath, logger);
|
|
650
744
|
} finally {
|
|
651
745
|
releaseLock();
|
|
652
746
|
}
|
|
@@ -1565,7 +1659,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1565
1659
|
// Phase 2: If no trajectory data, try pain-context fallback
|
|
1566
1660
|
if (!snapshotData && sleepTask.recentPainContext) {
|
|
1567
1661
|
logger?.warn?.(`[PD:EvolutionWorker] Using pain-context fallback for ${sleepTask.id}: trajectory snapshot unavailable, will try session summary from extractor`);
|
|
1568
|
-
snapshotData = buildFallbackNocturnalSnapshot(sleepTask, extractor) ?? undefined;
|
|
1662
|
+
snapshotData = buildFallbackNocturnalSnapshot(sleepTask, extractor, logger) ?? undefined;
|
|
1569
1663
|
}
|
|
1570
1664
|
|
|
1571
1665
|
const snapshotValidation = validateNocturnalSnapshotIngress(snapshotData);
|
|
@@ -553,8 +553,8 @@ export class HealthQueryService {
|
|
|
553
553
|
const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
|
|
554
554
|
if (!fs.existsSync(streamPath)) return [];
|
|
555
555
|
|
|
556
|
-
|
|
557
|
-
let lines: string[]
|
|
556
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
557
|
+
let lines: string[];
|
|
558
558
|
try {
|
|
559
559
|
const raw = fs.readFileSync(streamPath, 'utf8').trim();
|
|
560
560
|
if (!raw) return [];
|
|
@@ -565,8 +565,9 @@ export class HealthQueryService {
|
|
|
565
565
|
|
|
566
566
|
const records: RecentPrincipleChange[] = [];
|
|
567
567
|
for (const line of lines) {
|
|
568
|
-
|
|
569
|
-
|
|
568
|
+
|
|
569
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
570
|
+
let event: EvolutionStreamRecord | null;
|
|
570
571
|
try {
|
|
571
572
|
event = JSON.parse(line) as EvolutionStreamRecord;
|
|
572
573
|
} catch {
|
|
@@ -784,6 +785,7 @@ export class HealthQueryService {
|
|
|
784
785
|
}
|
|
785
786
|
|
|
786
787
|
|
|
788
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
787
789
|
private getEventDedupKey(entry: EventLogEntry): string {
|
|
788
790
|
const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
|
|
789
791
|
if (eventId) {
|
|
@@ -854,6 +856,7 @@ export class HealthQueryService {
|
|
|
854
856
|
}
|
|
855
857
|
|
|
856
858
|
|
|
859
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
857
860
|
private resolveGateType(row: GateBlockRow): string {
|
|
858
861
|
if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
|
|
859
862
|
return row.gate_type;
|
|
@@ -878,6 +881,7 @@ export class HealthQueryService {
|
|
|
878
881
|
}
|
|
879
882
|
|
|
880
883
|
|
|
884
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
881
885
|
private scoreToStatus(score: number): string {
|
|
882
886
|
if (score >= 70) return 'healthy';
|
|
883
887
|
if (score >= 40) return 'warning';
|
|
@@ -885,6 +889,7 @@ export class HealthQueryService {
|
|
|
885
889
|
}
|
|
886
890
|
|
|
887
891
|
|
|
892
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
888
893
|
private evolutionToStatus(tier: string, points: number): string {
|
|
889
894
|
const lower = tier.toLowerCase();
|
|
890
895
|
if (lower === 'forest' || lower === 'tree') return 'healthy';
|
|
@@ -893,6 +898,7 @@ export class HealthQueryService {
|
|
|
893
898
|
}
|
|
894
899
|
|
|
895
900
|
|
|
901
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
896
902
|
private safeListFiles(dirPath: string, predicate: (_name: string) => boolean): string[] {
|
|
897
903
|
if (!fs.existsSync(dirPath)) return [];
|
|
898
904
|
try {
|
|
@@ -905,6 +911,7 @@ export class HealthQueryService {
|
|
|
905
911
|
}
|
|
906
912
|
|
|
907
913
|
|
|
914
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
908
915
|
private readJsonFile<T>(filePath: string, fallback: T): T {
|
|
909
916
|
if (!fs.existsSync(filePath)) return fallback;
|
|
910
917
|
try {
|
|
@@ -915,11 +922,13 @@ export class HealthQueryService {
|
|
|
915
922
|
}
|
|
916
923
|
|
|
917
924
|
|
|
925
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
918
926
|
private asNumber(value: unknown, fallback: number): number {
|
|
919
927
|
return Number.isFinite(value) ? Number(value) : fallback;
|
|
920
928
|
}
|
|
921
929
|
|
|
922
930
|
|
|
931
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
923
932
|
private asNullableNumber(value: unknown): number | null {
|
|
924
933
|
if (Number.isFinite(value)) return Number(value);
|
|
925
934
|
if (typeof value === 'string' && value.trim().length > 0) {
|
|
@@ -954,7 +963,7 @@ export class HealthQueryService {
|
|
|
954
963
|
dailyGfiPeak,
|
|
955
964
|
today,
|
|
956
965
|
);
|
|
957
|
-
} catch
|
|
966
|
+
} catch {
|
|
958
967
|
// Non-critical: GFI sync failure should not block queries
|
|
959
968
|
}
|
|
960
969
|
}
|
|
@@ -1015,7 +1024,7 @@ export class HealthQueryService {
|
|
|
1015
1024
|
}
|
|
1016
1025
|
|
|
1017
1026
|
return latest;
|
|
1018
|
-
} catch
|
|
1027
|
+
} catch {
|
|
1019
1028
|
// Non-critical: failure to read session files should not crash the service
|
|
1020
1029
|
return null;
|
|
1021
1030
|
}
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
} from '../nocturnal-service.js';
|
|
37
37
|
import { type TrinityStageFailure, type TrinityResult } from '../../core/nocturnal-trinity.js';
|
|
38
38
|
import type { TrinityRuntimeAdapter } from '../../core/nocturnal-trinity.js';
|
|
39
|
-
import type { NocturnalSessionSnapshot } from '../../core/nocturnal-trajectory-extractor.js';
|
|
40
39
|
import type { RecentPainContext } from '../evolution-worker.js';
|
|
41
40
|
import * as fs from 'fs';
|
|
42
41
|
import * as path from 'path';
|