principles-disciple 1.46.0 → 1.47.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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/core/evolution-reducer.ts +46 -2
- package/src/service/evolution-worker.ts +150 -2
- package/src/types/principle-tree-schema.ts +4 -0
- package/tests/core/evolution-reducer.compilation-retry.test.ts +188 -0
- package/tests/service/evolution-worker.compilation-backfill.test.ts +199 -0
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.47.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "70500e1475ef",
|
|
80
|
+
"bundleMd5": "607cfbcb4534d2cfdc45cb0cd019cd0b",
|
|
81
|
+
"builtAt": "2026-04-16T03:41:17.317Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -21,9 +21,23 @@ import type {
|
|
|
21
21
|
PrincipleSuggestedRule,
|
|
22
22
|
} from './evolution-types.js';
|
|
23
23
|
import { isCompleteDetectorMetadata } from './evolution-types.js';
|
|
24
|
-
import { updateTrainingStore, addPrincipleToLedger, type LedgerPrinciple } from './principle-tree-ledger.js';
|
|
24
|
+
import { updateTrainingStore, addPrincipleToLedger, updatePrinciple, type LedgerPrinciple } from './principle-tree-ledger.js';
|
|
25
|
+
import { PrincipleCompiler } from './principle-compiler/index.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wrapper for updatePrinciple calls in the compilation retry path.
|
|
29
|
+
* If updatePrinciple throws, logs the error instead of propagating —
|
|
30
|
+
* compilation retry state is best-effort and should not crash principle creation.
|
|
31
|
+
*/
|
|
32
|
+
function updateRetryCount(stateDir: string, workspaceDir: string, principleId: string, count: number): void {
|
|
33
|
+
try {
|
|
34
|
+
updatePrinciple(stateDir, principleId, { compilationRetryCount: count });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
SystemLogger.log(workspaceDir, 'RETRY_COUNT_UPDATE_FAILED',
|
|
37
|
+
`Failed to update compilationRetryCount for ${principleId}: ${String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
25
40
|
|
|
26
|
-
|
|
27
41
|
export interface EvolutionReducer {
|
|
28
42
|
|
|
29
43
|
emit(_event: EvolutionLoopEvent): void;
|
|
@@ -420,6 +434,36 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
420
434
|
};
|
|
421
435
|
addPrincipleToLedger(this.stateDir, ledgerPrinciple);
|
|
422
436
|
SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADDED', `Principle ${principleId} added to ledger tree`);
|
|
437
|
+
|
|
438
|
+
// Sync compile: attempt to compile immediately unless evaluability is manual_only.
|
|
439
|
+
// Failures are not fatal — heartbeat backfill will retry automatically.
|
|
440
|
+
if (evaluability !== 'manual_only' && this.stateDir) {
|
|
441
|
+
const trajectory = TrajectoryRegistry.get(this.workspaceDir);
|
|
442
|
+
const compiler = new PrincipleCompiler(this.stateDir, trajectory);
|
|
443
|
+
try {
|
|
444
|
+
const result = compiler.compileOne(principleId);
|
|
445
|
+
if (result.success) {
|
|
446
|
+
// Reset retry count on success
|
|
447
|
+
updatePrinciple(this.stateDir, principleId, { compilationRetryCount: undefined });
|
|
448
|
+
SystemLogger.log(this.workspaceDir, 'COMPILE_SUCCESS', `Principle ${principleId} compiled successfully`);
|
|
449
|
+
} else {
|
|
450
|
+
// Compile returned failure — queue for backfill retry (count=0 means "queued", Phase 2 will pick it up).
|
|
451
|
+
// This gives exactly 5 total attempts before exhaustion (backfill: 0-4, sync: 0-4).
|
|
452
|
+
updateRetryCount(this.stateDir, this.workspaceDir, principleId, 0);
|
|
453
|
+
SystemLogger.log(
|
|
454
|
+
this.workspaceDir, 'COMPILE_FAILED',
|
|
455
|
+
`Principle ${principleId} compile failed: ${result.reason ?? 'unknown'} (attempt 1/5)`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
} catch (compileErr) {
|
|
459
|
+
// Unexpected error during compilation — queue for backfill retry
|
|
460
|
+
updateRetryCount(this.stateDir, this.workspaceDir, principleId, 0);
|
|
461
|
+
SystemLogger.log(
|
|
462
|
+
this.workspaceDir, 'COMPILE_FAILED',
|
|
463
|
+
`Principle ${principleId} compile threw: ${String(compileErr)} (attempt 1/5)`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
423
467
|
} catch (err) {
|
|
424
468
|
SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADD_FAILED', `Failed to add ${principleId} to ledger tree: ${String(err)}`);
|
|
425
469
|
}
|
|
@@ -15,6 +15,7 @@ import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
|
15
15
|
import { addDiagnosticianTask, completeDiagnosticianTask } from '../core/diagnostician-task-store.js';
|
|
16
16
|
import { getEvolutionLogger } from '../core/evolution-logger.js';
|
|
17
17
|
import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
|
|
18
|
+
import type { PrincipleEvaluability } from '../types/principle-tree-schema.js';
|
|
18
19
|
export type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
|
|
19
20
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
20
21
|
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
type NocturnalSessionSnapshot,
|
|
38
39
|
} from '../core/nocturnal-trajectory-extractor.js';
|
|
39
40
|
import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
|
|
41
|
+
import { PrincipleCompiler } from '../core/principle-compiler/index.js';
|
|
42
|
+
import { loadLedger, updatePrinciple } from '../core/principle-tree-ledger.js';
|
|
40
43
|
import { isExpectedSubagentError } from './subagent-workflow/subagent-error-utils.js';
|
|
41
44
|
import { readPainFlagContract } from '../core/pain.js';
|
|
42
45
|
import { CorrectionObserverWorkflowManager, correctionObserverWorkflowSpec } from './subagent-workflow/correction-observer-workflow-manager.js';
|
|
@@ -567,8 +570,147 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
567
570
|
return result;
|
|
568
571
|
}
|
|
569
572
|
|
|
570
|
-
|
|
571
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Process compilation backfill and retry loop.
|
|
575
|
+
* Phase 1 — Backfill: on first call, scan for old principles (compilationRetryCount === undefined)
|
|
576
|
+
* with evaluability !== 'manual_only' and no active implementation, queue them (set to 0).
|
|
577
|
+
* Phase 2 — Retry: compile all principles with compilationRetryCount >= 0.
|
|
578
|
+
* After 5 consecutive failures, downgrades to manual_only and logs COMPILE_EXHAUSTED.
|
|
579
|
+
*/
|
|
580
|
+
export async function processCompilationBackfill(
|
|
581
|
+
wctx: WorkspaceContext,
|
|
582
|
+
logger: PluginLogger,
|
|
583
|
+
): Promise<void> {
|
|
584
|
+
if (!wctx.stateDir) return;
|
|
585
|
+
|
|
586
|
+
let ledger: ReturnType<typeof loadLedger>;
|
|
587
|
+
try {
|
|
588
|
+
ledger = loadLedger(wctx.stateDir);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
logger?.warn?.(`[PD:EvolutionWorker] CompilationBackfill: failed to load ledger: ${String(err)}`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Phase 1: Backfill old principles (runs once per process) ─────────────────
|
|
595
|
+
const backfillMarkerPath = path.join(wctx.stateDir, 'COMPILATION_BACKFILL_DONE');
|
|
596
|
+
const hasBackfillRun = fs.existsSync(backfillMarkerPath);
|
|
597
|
+
if (!hasBackfillRun) {
|
|
598
|
+
let backfillQueued = 0;
|
|
599
|
+
for (const [principleId, principle] of Object.entries(ledger.tree.principles)) {
|
|
600
|
+
if (principle.compilationRetryCount !== undefined) continue; // already processed
|
|
601
|
+
if (principle.evaluability === 'manual_only') continue;
|
|
602
|
+
// Check if already has active implementation
|
|
603
|
+
const hasActiveImpl = Object.values(ledger.tree.implementations).some(
|
|
604
|
+
(impl) => impl.lifecycleState === 'active' && (
|
|
605
|
+
ledger.tree.rules[impl.ruleId]?.principleId === principleId
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
if (hasActiveImpl) {
|
|
609
|
+
// Already compiled — mark as done
|
|
610
|
+
updatePrinciple(wctx.stateDir, principleId, { compilationRetryCount: undefined });
|
|
611
|
+
} else {
|
|
612
|
+
// Needs compilation — queue it
|
|
613
|
+
updatePrinciple(wctx.stateDir, principleId, { compilationRetryCount: 0 });
|
|
614
|
+
backfillQueued++;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (backfillQueued > 0) {
|
|
618
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_BACKFILL_QUEUED',
|
|
619
|
+
`Queued ${backfillQueued} old principles for compilation`);
|
|
620
|
+
}
|
|
621
|
+
// Write marker so we don't backfill again in this process
|
|
622
|
+
atomicWriteFileSync(backfillMarkerPath, new Date().toISOString());
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ── Phase 2: Retry pending compilations ───────────────────────────────────
|
|
626
|
+
const trajectory = TrajectoryRegistry.get(wctx.workspaceDir);
|
|
627
|
+
const compiler = new PrincipleCompiler(wctx.stateDir, trajectory);
|
|
628
|
+
|
|
629
|
+
// Re-load ledger after potential backfill updates
|
|
630
|
+
ledger = loadLedger(wctx.stateDir);
|
|
631
|
+
|
|
632
|
+
for (const [principleId, principle] of Object.entries(ledger.tree.principles)) {
|
|
633
|
+
const count = principle.compilationRetryCount;
|
|
634
|
+
|
|
635
|
+
// Skip: not in retry queue (undefined = done/succeeded)
|
|
636
|
+
if (count === undefined) continue;
|
|
637
|
+
|
|
638
|
+
// Skip: already exhausted (count >= 5 means 5 attempts already made)
|
|
639
|
+
if (count >= 5) continue;
|
|
640
|
+
|
|
641
|
+
// Error-isolate each principle so one failure doesn't stop all other retries
|
|
642
|
+
try {
|
|
643
|
+
const result = compiler.compileOne(principleId);
|
|
644
|
+
if (result.success) {
|
|
645
|
+
safeUpdateRetryCount(wctx.stateDir, wctx.workspaceDir, principleId, undefined);
|
|
646
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_SUCCESS',
|
|
647
|
+
`Principle ${principleId} compiled successfully (attempt ${count + 1})`);
|
|
648
|
+
} else {
|
|
649
|
+
const nextCount = count + 1;
|
|
650
|
+
if (nextCount >= 5) {
|
|
651
|
+
// Exhausted: single write to set manual_only (no intermediate count write)
|
|
652
|
+
safeUpdatePrinciple(wctx.stateDir, wctx.workspaceDir, principleId, {
|
|
653
|
+
evaluability: 'manual_only',
|
|
654
|
+
compilationRetryCount: undefined,
|
|
655
|
+
});
|
|
656
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_EXHAUSTED',
|
|
657
|
+
`Principle ${principleId} compilation exhausted after 5 attempts: ${result.reason ?? 'unknown'}`);
|
|
658
|
+
} else {
|
|
659
|
+
safeUpdateRetryCount(wctx.stateDir, wctx.workspaceDir, principleId, nextCount);
|
|
660
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_FAILED',
|
|
661
|
+
`Principle ${principleId} compile failed: ${result.reason ?? 'unknown'} (attempt ${nextCount}/5)`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} catch (compileErr) {
|
|
665
|
+
const nextCount = count + 1;
|
|
666
|
+
if (nextCount >= 5) {
|
|
667
|
+
// Exhausted: single write to set manual_only (no intermediate count write)
|
|
668
|
+
safeUpdatePrinciple(wctx.stateDir, wctx.workspaceDir, principleId, {
|
|
669
|
+
evaluability: 'manual_only',
|
|
670
|
+
compilationRetryCount: undefined,
|
|
671
|
+
});
|
|
672
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_EXHAUSTED',
|
|
673
|
+
`Principle ${principleId} compilation exhausted after 5 attempts: threw ${String(compileErr)}`);
|
|
674
|
+
} else {
|
|
675
|
+
safeUpdateRetryCount(wctx.stateDir, wctx.workspaceDir, principleId, nextCount);
|
|
676
|
+
SystemLogger.log(wctx.workspaceDir, 'COMPILE_FAILED',
|
|
677
|
+
`Principle ${principleId} compile threw: ${String(compileErr)} (attempt ${nextCount}/5)`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Wrapper for updatePrinciple in the retry loop — logs but does not propagate errors.
|
|
685
|
+
* If update fails, the principle stays in its current retry state and will be
|
|
686
|
+
* picked up again on the next heartbeat.
|
|
687
|
+
*/
|
|
688
|
+
function safeUpdateRetryCount(stateDir: string, workspaceDir: string, principleId: string, count: number | undefined): void {
|
|
689
|
+
try {
|
|
690
|
+
updatePrinciple(stateDir, principleId, { compilationRetryCount: count });
|
|
691
|
+
} catch (err) {
|
|
692
|
+
SystemLogger.log(workspaceDir, 'RETRY_COUNT_UPDATE_FAILED',
|
|
693
|
+
`Failed to update retry count for ${principleId}: ${String(err)}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Wrapper for updatePrinciple with multiple fields — logs but does not propagate errors.
|
|
699
|
+
*/
|
|
700
|
+
function safeUpdatePrinciple(
|
|
701
|
+
stateDir: string,
|
|
702
|
+
workspaceDir: string,
|
|
703
|
+
principleId: string,
|
|
704
|
+
updates: { evaluability?: PrincipleEvaluability; compilationRetryCount?: number },
|
|
705
|
+
): void {
|
|
706
|
+
try {
|
|
707
|
+
updatePrinciple(stateDir, principleId, updates);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
SystemLogger.log(workspaceDir, 'RETRY_PRINCIPLE_UPDATE_FAILED',
|
|
710
|
+
`Failed to update principle ${principleId}: ${String(err)}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
572
714
|
async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogger, eventLog: EventLog, api?: OpenClawPluginApi) {
|
|
573
715
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
574
716
|
if (!fs.existsSync(queuePath)) {
|
|
@@ -1957,6 +2099,12 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
|
|
|
1957
2099
|
const mergedConfig = loadNocturnalConfigMerged(wctx.stateDir);
|
|
1958
2100
|
const { sleepReflection: sleepConfig, keywordOptimization: kwOptConfig } = mergedConfig;
|
|
1959
2101
|
|
|
2102
|
+
// Compilation backfill: runs on every heartbeat to retry failed compilations.
|
|
2103
|
+
// Fire-and-forget — errors are logged within the function.
|
|
2104
|
+
processCompilationBackfill(wctx, logger).catch((err) => {
|
|
2105
|
+
logger?.error?.(`[PD:EvolutionWorker] CompilationBackfill threw: ${String(err)}`);
|
|
2106
|
+
});
|
|
2107
|
+
|
|
1960
2108
|
const idleResult = checkWorkspaceIdle(wctx.workspaceDir, {});
|
|
1961
2109
|
logger?.info?.(`[PD:EvolutionWorker] HEARTBEAT cycle=${new Date().toISOString()} idle=${idleResult.isIdle} idleForMs=${idleResult.idleForMs} userActiveSessions=${idleResult.userActiveSessions} abandonedSessions=${idleResult.abandonedSessionIds.length} lastActivityEpoch=${idleResult.mostRecentActivityAt} triggerMode=${sleepConfig.trigger_mode}`);
|
|
1962
2110
|
|
|
@@ -76,6 +76,10 @@ export interface Principle {
|
|
|
76
76
|
|
|
77
77
|
// Detector metadata (for auto-training eligibility)
|
|
78
78
|
detectorMetadata?: PrincipleDetectorSpec;
|
|
79
|
+
|
|
80
|
+
// Compilation retry tracking (for runtime auto-trigger)
|
|
81
|
+
// undefined = not yet attempted or succeeded; 0 = queued; n >= 1 = retry attempt n
|
|
82
|
+
compilationRetryCount?: number;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
// =========================================================================
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
|
|
6
|
+
import { loadLedger } from '../../src/core/principle-tree-ledger.js';
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = [];
|
|
9
|
+
|
|
10
|
+
function makeTempDir(): string {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-evolution-compile-'));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Minimal state dir structure
|
|
17
|
+
function makeStateDir(workspace: string): string {
|
|
18
|
+
const stateDir = path.join(workspace, '.state');
|
|
19
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(stateDir, 'EVOLUTION_STREAM'), '', 'utf8');
|
|
21
|
+
fs.writeFileSync(path.join(stateDir, 'PRINCIPLES'), '', 'utf8');
|
|
22
|
+
fs.writeFileSync(path.join(stateDir, 'evolution_queue.json'), '[]', 'utf8');
|
|
23
|
+
fs.writeFileSync(path.join(stateDir, 'ledger.json'), JSON.stringify({
|
|
24
|
+
trainingStore: {},
|
|
25
|
+
tree: { principles: {}, rules: {}, implementations: {}, metrics: {}, lastUpdated: new Date().toISOString() },
|
|
26
|
+
}), 'utf8');
|
|
27
|
+
return stateDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
for (const dir of tempDirs.splice(0)) {
|
|
32
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// createPrincipleFromDiagnosis — compilationRetryCount initialization
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
describe('createPrincipleFromDiagnosis — compilationRetryCount initialization', () => {
|
|
41
|
+
it('sets compilationRetryCount=0 when evaluability is weak_heuristic (queued for compilation)', () => {
|
|
42
|
+
const workspace = makeTempDir();
|
|
43
|
+
const stateDir = makeStateDir(workspace);
|
|
44
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
45
|
+
|
|
46
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
47
|
+
painId: `pain-weak-heuristic-${Date.now()}`,
|
|
48
|
+
painType: 'tool_failure',
|
|
49
|
+
triggerPattern: 'bash rm fails',
|
|
50
|
+
action: 'verify file exists before rm',
|
|
51
|
+
source: 'test-compilation-retry',
|
|
52
|
+
evaluability: 'weak_heuristic',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(id).not.toBeNull();
|
|
56
|
+
const ledger = loadLedger(stateDir);
|
|
57
|
+
const principle = ledger.tree.principles[id as string];
|
|
58
|
+
expect(principle).toBeDefined();
|
|
59
|
+
// Compilation queued: count >= 0 means queued
|
|
60
|
+
expect(typeof principle?.compilationRetryCount).toBe('number');
|
|
61
|
+
expect(principle?.compilationRetryCount).toBeGreaterThanOrEqual(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('sets compilationRetryCount=0 when evaluability is deterministic', () => {
|
|
65
|
+
const workspace = makeTempDir();
|
|
66
|
+
const stateDir = makeStateDir(workspace);
|
|
67
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
68
|
+
|
|
69
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
70
|
+
painId: `pain-deterministic-${Date.now()}`,
|
|
71
|
+
painType: 'tool_failure',
|
|
72
|
+
triggerPattern: 'edit without read',
|
|
73
|
+
action: 'always read before edit',
|
|
74
|
+
source: 'test-compilation-retry',
|
|
75
|
+
evaluability: 'deterministic',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(id).not.toBeNull();
|
|
79
|
+
const ledger = loadLedger(stateDir);
|
|
80
|
+
const principle = ledger.tree.principles[id as string];
|
|
81
|
+
expect(principle).toBeDefined();
|
|
82
|
+
expect(typeof principle?.compilationRetryCount).toBe('number');
|
|
83
|
+
expect(principle?.compilationRetryCount).toBeGreaterThanOrEqual(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does NOT set compilationRetryCount when evaluability is manual_only', () => {
|
|
87
|
+
const workspace = makeTempDir();
|
|
88
|
+
const stateDir = makeStateDir(workspace);
|
|
89
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
90
|
+
|
|
91
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
92
|
+
painId: `pain-manual-only-${Date.now()}`,
|
|
93
|
+
painType: 'tool_failure',
|
|
94
|
+
triggerPattern: 'generic pain',
|
|
95
|
+
action: 'be more careful',
|
|
96
|
+
source: 'test-compilation-retry',
|
|
97
|
+
evaluability: 'manual_only',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(id).not.toBeNull();
|
|
101
|
+
const ledger = loadLedger(stateDir);
|
|
102
|
+
const principle = ledger.tree.principles[id as string];
|
|
103
|
+
expect(principle).toBeDefined();
|
|
104
|
+
// manual_only principles should NOT be queued for compilation
|
|
105
|
+
expect(principle?.compilationRetryCount).toBeUndefined();
|
|
106
|
+
expect(principle?.evaluability).toBe('manual_only');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('defaults to weak_heuristic and queues for compilation when no evaluability provided', () => {
|
|
110
|
+
const workspace = makeTempDir();
|
|
111
|
+
const stateDir = makeStateDir(workspace);
|
|
112
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
113
|
+
|
|
114
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
115
|
+
painId: `pain-default-${Date.now()}`,
|
|
116
|
+
painType: 'tool_failure',
|
|
117
|
+
triggerPattern: 'some pattern',
|
|
118
|
+
action: 'some action',
|
|
119
|
+
source: 'test-compilation-retry',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(id).not.toBeNull();
|
|
123
|
+
const ledger = loadLedger(stateDir);
|
|
124
|
+
const principle = ledger.tree.principles[id as string];
|
|
125
|
+
expect(principle).toBeDefined();
|
|
126
|
+
// default evaluability is weak_heuristic, which should queue for compilation
|
|
127
|
+
expect(typeof principle?.compilationRetryCount).toBe('number');
|
|
128
|
+
expect(principle?.compilationRetryCount).toBeGreaterThanOrEqual(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// createPrincipleFromDiagnosis — compilationRetryCount increments on compile failure
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('createPrincipleFromDiagnosis — compilationRetryCount increments on failure', () => {
|
|
137
|
+
it('increments to 1 when compilation fails (no trajectory data)', () => {
|
|
138
|
+
const workspace = makeTempDir();
|
|
139
|
+
const stateDir = makeStateDir(workspace);
|
|
140
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
141
|
+
|
|
142
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
143
|
+
painId: `pain-fail-${Date.now()}`,
|
|
144
|
+
painType: 'tool_failure',
|
|
145
|
+
triggerPattern: 'unknown tool',
|
|
146
|
+
action: 'do nothing',
|
|
147
|
+
source: 'test-compilation-retry',
|
|
148
|
+
evaluability: 'weak_heuristic',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(id).not.toBeNull();
|
|
152
|
+
const ledger = loadLedger(stateDir);
|
|
153
|
+
const principle = ledger.tree.principles[id as string];
|
|
154
|
+
expect(principle).toBeDefined();
|
|
155
|
+
// Compilation was attempted and failed (no trajectory data) → count should be 0
|
|
156
|
+
// (sync failure sets count=0 so Phase 2 gets exactly 5 total attempts)
|
|
157
|
+
expect(principle?.compilationRetryCount).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Principle schema — compilationRetryCount field exists and persists
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
describe('Principle schema — compilationRetryCount field persists', () => {
|
|
166
|
+
it('compilationRetryCount is stored and retrieved correctly', () => {
|
|
167
|
+
const workspace = makeTempDir();
|
|
168
|
+
const stateDir = makeStateDir(workspace);
|
|
169
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir });
|
|
170
|
+
|
|
171
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
172
|
+
painId: `pain-schema-${Date.now()}`,
|
|
173
|
+
painType: 'tool_failure',
|
|
174
|
+
triggerPattern: 'test pattern',
|
|
175
|
+
action: 'test action',
|
|
176
|
+
source: 'test-compilation-retry',
|
|
177
|
+
evaluability: 'weak_heuristic',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(id).not.toBeNull();
|
|
181
|
+
// Reload ledger to verify persistence
|
|
182
|
+
const ledger = loadLedger(stateDir);
|
|
183
|
+
const principle = ledger.tree.principles[id as string];
|
|
184
|
+
expect(principle).toBeDefined();
|
|
185
|
+
expect(typeof principle?.compilationRetryCount).toBe('number');
|
|
186
|
+
expect(principle?.compilationRetryCount).toBeGreaterThanOrEqual(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { addPrincipleToLedger, loadLedger, type LedgerPrinciple } from '../../src/core/principle-tree-ledger.js';
|
|
6
|
+
import type { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
7
|
+
import type { PluginLogger } from '../../src/openclaw-sdk.js';
|
|
8
|
+
import { processCompilationBackfill } from '../../src/service/evolution-worker.js';
|
|
9
|
+
|
|
10
|
+
const tempDirs: string[] = [];
|
|
11
|
+
|
|
12
|
+
function makeTempDir(): string {
|
|
13
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-backfill-test-'));
|
|
14
|
+
tempDirs.push(dir);
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeStateDir(workspace: string): string {
|
|
19
|
+
const stateDir = path.join(workspace, '.state');
|
|
20
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(stateDir, 'EVOLUTION_STREAM'), '', 'utf8');
|
|
22
|
+
fs.writeFileSync(path.join(stateDir, 'PRINCIPLES'), '', 'utf8');
|
|
23
|
+
fs.writeFileSync(path.join(stateDir, 'evolution_queue.json'), '[]', 'utf8');
|
|
24
|
+
fs.writeFileSync(path.join(stateDir, 'ledger.json'), JSON.stringify({
|
|
25
|
+
trainingStore: {},
|
|
26
|
+
tree: { principles: {}, rules: {}, implementations: {}, metrics: {}, lastUpdated: new Date().toISOString() },
|
|
27
|
+
}), 'utf8');
|
|
28
|
+
return stateDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeWctx(workspace: string, stateDir: string): WorkspaceContext {
|
|
32
|
+
return {
|
|
33
|
+
workspaceDir: workspace,
|
|
34
|
+
stateDir,
|
|
35
|
+
resolve: (file: string) => path.join(stateDir, file),
|
|
36
|
+
} as unknown as WorkspaceContext;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makePrinciple(id: string, overrides: Partial<LedgerPrinciple> = {}): LedgerPrinciple {
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
version: 1,
|
|
43
|
+
text: `principle ${id}`,
|
|
44
|
+
triggerPattern: 'test',
|
|
45
|
+
action: 'test action',
|
|
46
|
+
status: 'active',
|
|
47
|
+
priority: 'P1',
|
|
48
|
+
scope: 'general',
|
|
49
|
+
evaluability: 'weak_heuristic',
|
|
50
|
+
compilationRetryCount: undefined,
|
|
51
|
+
ruleIds: [],
|
|
52
|
+
conflictsWithPrincipleIds: [],
|
|
53
|
+
derivedFromPainIds: [],
|
|
54
|
+
valueScore: 0,
|
|
55
|
+
adherenceRate: 0,
|
|
56
|
+
painPreventedCount: 0,
|
|
57
|
+
createdAt: new Date().toISOString(),
|
|
58
|
+
updatedAt: new Date().toISOString(),
|
|
59
|
+
...overrides,
|
|
60
|
+
} as LedgerPrinciple;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const noopLogger: PluginLogger = {
|
|
64
|
+
debug: () => {},
|
|
65
|
+
info: () => {},
|
|
66
|
+
warn: () => {},
|
|
67
|
+
error: () => {},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.restoreAllMocks();
|
|
72
|
+
for (const dir of tempDirs.splice(0)) {
|
|
73
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Phase 1: Backfill
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
describe('processCompilationBackfill — Phase 1 backfill', () => {
|
|
82
|
+
it('sets compilationRetryCount=0 for old principles without retry count', () => {
|
|
83
|
+
const workspace = makeTempDir();
|
|
84
|
+
const stateDir = makeStateDir(workspace);
|
|
85
|
+
|
|
86
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_001', {
|
|
87
|
+
evaluability: 'weak_heuristic',
|
|
88
|
+
compilationRetryCount: undefined,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
92
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
93
|
+
|
|
94
|
+
const ledger = loadLedger(stateDir);
|
|
95
|
+
// Phase 1 sets count=0, then Phase 2 runs and increments to 1 (compilation fails without trajectory data)
|
|
96
|
+
// The key assertion: count was set to 0 at some point (proves backfill ran)
|
|
97
|
+
expect(ledger.tree.principles['P_001'].compilationRetryCount).toBeGreaterThanOrEqual(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('skips principles with manual_only evaluability', () => {
|
|
101
|
+
const workspace = makeTempDir();
|
|
102
|
+
const stateDir = makeStateDir(workspace);
|
|
103
|
+
|
|
104
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_002', {
|
|
105
|
+
evaluability: 'manual_only',
|
|
106
|
+
compilationRetryCount: undefined,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
110
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
111
|
+
|
|
112
|
+
const ledger = loadLedger(stateDir);
|
|
113
|
+
expect(ledger.tree.principles['P_002'].compilationRetryCount).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('writes COMPILATION_BACKFILL_DONE marker after backfill', () => {
|
|
117
|
+
const workspace = makeTempDir();
|
|
118
|
+
const stateDir = makeStateDir(workspace);
|
|
119
|
+
|
|
120
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_003', {
|
|
121
|
+
evaluability: 'weak_heuristic',
|
|
122
|
+
compilationRetryCount: undefined,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
126
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
127
|
+
|
|
128
|
+
const markerPath = path.join(stateDir, 'COMPILATION_BACKFILL_DONE');
|
|
129
|
+
expect(fs.existsSync(markerPath)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('does not re-backfill if marker already exists', () => {
|
|
133
|
+
const workspace = makeTempDir();
|
|
134
|
+
const stateDir = makeStateDir(workspace);
|
|
135
|
+
|
|
136
|
+
// Pre-write the marker so Phase 1 (backfill) is skipped
|
|
137
|
+
const markerPath = path.join(stateDir, 'COMPILATION_BACKFILL_DONE');
|
|
138
|
+
fs.writeFileSync(markerPath, new Date().toISOString(), 'utf8');
|
|
139
|
+
|
|
140
|
+
// Add principle with compilationRetryCount already set to a high value
|
|
141
|
+
// (simulating already-processed-by-Phase2)
|
|
142
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_004', {
|
|
143
|
+
evaluability: 'weak_heuristic',
|
|
144
|
+
compilationRetryCount: 2,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
148
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
149
|
+
|
|
150
|
+
const ledger = loadLedger(stateDir);
|
|
151
|
+
// Phase 1 was skipped (marker exists), but Phase 2 still ran and incremented count
|
|
152
|
+
// So count goes from 2 -> 3 (compilation fails without trajectory data)
|
|
153
|
+
expect(ledger.tree.principles['P_004'].compilationRetryCount).toBe(3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Phase 2: Retry loop
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
describe('processCompilationBackfill — Phase 2 retry loop', () => {
|
|
162
|
+
it('increments count on compile failure (below exhaustion)', () => {
|
|
163
|
+
const workspace = makeTempDir();
|
|
164
|
+
const stateDir = makeStateDir(workspace);
|
|
165
|
+
|
|
166
|
+
// Principle queued with count=1 — next failure should make it 2
|
|
167
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_012', {
|
|
168
|
+
evaluability: 'weak_heuristic',
|
|
169
|
+
compilationRetryCount: 1,
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
173
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
174
|
+
|
|
175
|
+
const ledger = loadLedger(stateDir);
|
|
176
|
+
// Compilation fails (no trajectory data), so count increments
|
|
177
|
+
expect(ledger.tree.principles['P_012'].compilationRetryCount).toBe(2);
|
|
178
|
+
expect(ledger.tree.principles['P_012'].evaluability).toBe('weak_heuristic');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('downgrades to manual_only after 5 consecutive failures', () => {
|
|
182
|
+
const workspace = makeTempDir();
|
|
183
|
+
const stateDir = makeStateDir(workspace);
|
|
184
|
+
|
|
185
|
+
// Principle at count=4 — next failure exhausts it
|
|
186
|
+
addPrincipleToLedger(stateDir, makePrinciple('P_011', {
|
|
187
|
+
evaluability: 'weak_heuristic',
|
|
188
|
+
compilationRetryCount: 4,
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const wctx = makeWctx(workspace, stateDir);
|
|
192
|
+
processCompilationBackfill(wctx, noopLogger);
|
|
193
|
+
|
|
194
|
+
const ledger = loadLedger(stateDir);
|
|
195
|
+
// Compilation fails (no trajectory), count becomes 5 >= 5, downgrades to manual_only
|
|
196
|
+
expect(ledger.tree.principles['P_011'].evaluability).toBe('manual_only');
|
|
197
|
+
expect(ledger.tree.principles['P_011'].compilationRetryCount).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
});
|