principles-disciple 1.52.0 → 1.54.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/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +113 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/bootstrap-rules.ts +43 -4
- package/src/core/evolution-hook.ts +74 -0
- package/src/core/file-storage-adapter.ts +203 -0
- package/src/core/init.ts +29 -2
- package/src/core/nocturnal-trinity.ts +230 -0
- package/src/core/observability.ts +242 -0
- package/src/core/pain-lifecycle.ts +38 -0
- package/src/core/pain-signal-adapter.ts +42 -0
- package/src/core/pain-signal.ts +139 -0
- package/src/core/principle-injection.ts +208 -0
- package/src/core/principle-injector.ts +84 -0
- package/src/core/storage-adapter.ts +65 -0
- package/src/core/telemetry-event.ts +109 -0
- package/src/hooks/prompt.ts +18 -3
- package/src/service/evolution-worker.ts +59 -2
- package/tests/core/evolution-hook.test.ts +123 -0
- package/tests/core/file-storage-adapter.test.ts +285 -0
- package/tests/core/nocturnal-trinity.test.ts +236 -0
- package/tests/core/observability.test.ts +383 -0
- package/tests/core/pain-lifecycle.test.ts +37 -0
- package/tests/core/pain-signal-adapter.test.ts +116 -0
- package/tests/core/pain-signal.test.ts +190 -0
- package/tests/core/principle-injection.test.ts +223 -0
- package/tests/core/principle-injector.test.ts +90 -0
- package/tests/core/storage-conformance.test.ts +429 -0
- package/tests/core/telemetry-event.test.ts +119 -0
- package/tests/integration/pain-lifecycle-e2e.test.ts +74 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
phase: "01-basic-visualization"
|
|
3
|
+
plan: "01-GAP-CLOSURE"
|
|
4
|
+
verified: 2026-04-17T15:30:00Z
|
|
5
|
+
status: passed
|
|
6
|
+
score: "4/4 must-haves verified"
|
|
7
|
+
overrides_applied: 0
|
|
8
|
+
re_verification: false
|
|
9
|
+
gaps: []
|
|
10
|
+
deferred: []
|
|
11
|
+
human_verification: []
|
|
12
|
+
requirement_mismatch:
|
|
13
|
+
note: "Requirement IDs provided (SDK-CORE-03, SDK-ADP-07, SDK-ADP-08, SDK-TEST-02, SDK-TEST-03, SDK-MGMT-01, SDK-MGMT-02) are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. This phase (01-basic-visualization) addresses VIZ-04 (Empty state optimization) - a different domain. Requirement coverage cannot be established for IDs that do not belong to this phase."
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Phase 01-basic-visualization: GAP-CLOSURE Verification Report
|
|
17
|
+
|
|
18
|
+
**Phase Goal:** Close verification gaps from Phase 01 by implementing missing i18n fixes for LineChart component and coverage trend empty state.
|
|
19
|
+
**Verified:** 2026-04-17T15:30:00Z
|
|
20
|
+
**Status:** passed
|
|
21
|
+
**Re-verification:** No — initial verification
|
|
22
|
+
|
|
23
|
+
## Goal Achievement
|
|
24
|
+
|
|
25
|
+
### Observable Truths
|
|
26
|
+
|
|
27
|
+
| # | Truth | Status | Evidence |
|
|
28
|
+
| --- | ----- | ------ | -------- |
|
|
29
|
+
| 1 | LineChart interface has emptyText prop for i18n | VERIFIED | `emptyText?: string;` at line 864 in charts.tsx |
|
|
30
|
+
| 2 | LineChart renders emptyText prop instead of hardcoded '暂无数据' | VERIFIED | Conditional render at lines 878-885: `if (!emptyText) return null;` then `{emptyText}` |
|
|
31
|
+
| 3 | Coverage trend section shows EmptyState when no data | VERIFIED | Ternary at line 259: `data.coverageTrend.length >= 1 ? (...) : (<EmptyState ...>)` |
|
|
32
|
+
| 4 | All LineChart usages pass emptyText prop | VERIFIED | Lines 272, 454, 521 all have `emptyText={t('common.noData')}` |
|
|
33
|
+
|
|
34
|
+
**Score:** 4/4 truths verified
|
|
35
|
+
|
|
36
|
+
### Deferred Items
|
|
37
|
+
|
|
38
|
+
None
|
|
39
|
+
|
|
40
|
+
### Required Artifacts
|
|
41
|
+
|
|
42
|
+
| Artifact | Expected | Status | Details |
|
|
43
|
+
| -------- | -------- | ------ | ------- |
|
|
44
|
+
| `charts.tsx:864` | `emptyText?: string` in LineChartProps | VERIFIED | Prop exists at correct line |
|
|
45
|
+
| `charts.tsx:876` | Default value `emptyText = ''` | VERIFIED | Default empty string assigned |
|
|
46
|
+
| `charts.tsx:878-885` | Conditional render using emptyText | VERIFIED | Returns null if no emptyText, renders div with {emptyText} otherwise |
|
|
47
|
+
| `ThinkingModelsPage.tsx:259` | Ternary operator for coverage trend | VERIFIED | `data.coverageTrend.length >= 1 ? (...) : (<EmptyState ...>)` |
|
|
48
|
+
| `ThinkingModelsPage.tsx:272` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
|
|
49
|
+
| `ThinkingModelsPage.tsx:454` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
|
|
50
|
+
| `ThinkingModelsPage.tsx:521` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
|
|
51
|
+
|
|
52
|
+
### Key Link Verification
|
|
53
|
+
|
|
54
|
+
| From | To | Via | Status | Details |
|
|
55
|
+
| ---- | --- | --- | ------ | ------- |
|
|
56
|
+
| LineChart | i18n system | emptyText prop | WIRED | emptyText prop accepts i18n string and renders it |
|
|
57
|
+
| Coverage trend | EmptyState | ternary operator | WIRED | Ternary correctly switches between LineChart and EmptyState |
|
|
58
|
+
| ThinkingModelsPage | common.noData | t() function | WIRED | All LineChart usages pass i18n key |
|
|
59
|
+
| EmptyState | i18n keys | t() function | WIRED | `t('thinkingModels.emptyCoverageTrend')` and `t('thinkingModels.emptyCoverageTrendDesc')` used |
|
|
60
|
+
|
|
61
|
+
### Data-Flow Trace (Level 4)
|
|
62
|
+
|
|
63
|
+
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
|
64
|
+
| -------- | ------------- | ------ | ------------------ | ------ |
|
|
65
|
+
| LineChart emptyText | emptyText prop | Parent component via t('common.noData') | N/A | STATIC — i18n key is static, not dynamic |
|
|
66
|
+
|
|
67
|
+
### Behavioral Spot-Checks
|
|
68
|
+
|
|
69
|
+
| Behavior | Command | Result | Status |
|
|
70
|
+
| -------- | ------- | ------ | ------ |
|
|
71
|
+
| emptyText in interface | `grep -n "emptyText?: string" charts.tsx` | `864: emptyText?: string;` | PASS |
|
|
72
|
+
| No hardcoded Chinese | `grep -n "暂无数据" charts.tsx` | No matches | PASS |
|
|
73
|
+
| Ternary for coverage | `grep -n "coverageTrend.length.*?" ThinkingModelsPage.tsx` | `259: {data.coverageTrend.length >= 1 ?` | PASS |
|
|
74
|
+
| EmptyState for coverage | `grep -n "EmptyState" ThinkingModelsPage.tsx \| head -3` | Lines 276-279 with i18n keys | PASS |
|
|
75
|
+
| All LineChart have emptyText | `grep -B5 -A10 "<LineChart" ThinkingModelsPage.tsx \| grep -c "emptyText"` | 3 | PASS |
|
|
76
|
+
|
|
77
|
+
### Requirements Coverage
|
|
78
|
+
|
|
79
|
+
**IMPORTANT - Requirement Mismatch Found:**
|
|
80
|
+
|
|
81
|
+
| Requirement | Source | Description | Status | Evidence |
|
|
82
|
+
| ----------- | ------ | ----------- | ------ | -------- |
|
|
83
|
+
| SDK-CORE-03 | User-provided | Implement universal PainSignal interface logic | N/A | Does not belong to 01-basic-visualization phase |
|
|
84
|
+
| SDK-ADP-07 | User-provided | Implement Coding domain adapter | N/A | Does not belong to 01-basic-visualization phase |
|
|
85
|
+
| SDK-ADP-08 | User-provided | Implement second domain adapter | N/A | Does not belong to 01-basic-visualization phase |
|
|
86
|
+
| SDK-TEST-02 | User-provided | Implement full Adapter conformance test suite | N/A | Does not belong to 01-basic-visualization phase |
|
|
87
|
+
| SDK-TEST-03 | User-provided | Execute and publish performance benchmarks | N/A | Does not belong to 01-basic-visualization phase |
|
|
88
|
+
| SDK-MGMT-01 | User-provided | Package SDK as @principles/core npm package | N/A | Does not belong to 01-basic-visualization phase |
|
|
89
|
+
| SDK-MGMT-02 | User-provided | Establish Semver versioning and migration guides | N/A | Does not belong to 01-basic-visualization phase |
|
|
90
|
+
| VIZ-04 | GAP-CLOSURE-PLAN | Empty state optimization | VERIFIED | All 4 must_haves from GAP-CLOSURE-PLAN verified |
|
|
91
|
+
|
|
92
|
+
**The 7 requirement IDs provided are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. They do not map to the 01-basic-visualization phase. The phase's actual scope is VIZ-04 (Empty state optimization), which has been verified through must_haves.**
|
|
93
|
+
|
|
94
|
+
### Anti-Patterns Found
|
|
95
|
+
|
|
96
|
+
| File | Line | Pattern | Severity | Impact |
|
|
97
|
+
| ---- | ---- | ------- | -------- | ------ |
|
|
98
|
+
| None | - | No anti-patterns detected | - | - |
|
|
99
|
+
|
|
100
|
+
### Human Verification Required
|
|
101
|
+
|
|
102
|
+
None required — all truths verifiable via static analysis.
|
|
103
|
+
|
|
104
|
+
### Gaps Summary
|
|
105
|
+
|
|
106
|
+
**None.** All 4 must_haves verified. Phase goal achieved.
|
|
107
|
+
|
|
108
|
+
**Notable finding:** The requirement IDs provided by user (SDK-CORE-03, SDK-ADP-07, SDK-ADP-08, SDK-TEST-02, SDK-TEST-03, SDK-MGMT-01, SDK-MGMT-02) are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. This phase (01-basic-visualization) addresses VIZ-04 (Empty state optimization for visualization) - a different scope. Requirement coverage cannot be established for IDs that do not belong to this phase.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
_Verified: 2026-04-17T15:30:00Z_
|
|
113
|
+
_Verifier: Claude (gsd-verifier)_
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* npm run bootstrap-rules (production)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { loadLedger, createRule, updatePrinciple } from './principle-tree-ledger.js';
|
|
16
|
+
import { loadLedger, createRule, updatePrinciple, addPrincipleToLedger } from './principle-tree-ledger.js';
|
|
17
|
+
import type { LedgerPrinciple } from './principle-tree-ledger.js';
|
|
17
18
|
import { loadStore } from './principle-training-state.js';
|
|
19
|
+
import { CORE_THINKING_MODELS } from './init.js';
|
|
18
20
|
|
|
19
21
|
export interface BootstrapResult {
|
|
20
22
|
principleId: string;
|
|
@@ -77,12 +79,49 @@ export function selectPrinciplesForBootstrap(stateDir: string, limit = 3): strin
|
|
|
77
79
|
* @throws Error if no deterministic principles found
|
|
78
80
|
*/
|
|
79
81
|
export function bootstrapRules(stateDir: string, limit = 3): BootstrapResult[] {
|
|
82
|
+
// Migration: if T-01..T-10 exist in Training Store but not in Ledger Tree, backfill.
|
|
83
|
+
// This handles workspaces initialized before Ledger Tree was added.
|
|
84
|
+
const store = loadStore(stateDir);
|
|
85
|
+
let ledger = loadLedger(stateDir);
|
|
86
|
+
const hasTrainingT = Object.keys(store).some((id) => id.startsWith('T-'));
|
|
87
|
+
const hasAnyLedgerT = Object.keys(ledger.tree.principles).some((id) => id.startsWith('T-'));
|
|
88
|
+
if (hasTrainingT && !hasAnyLedgerT) {
|
|
89
|
+
console.warn('[bootstrap] Migrating T-01..T-10 from Training Store to Ledger Tree');
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
for (const [id, entry] of Object.entries(store)) {
|
|
92
|
+
if (!id.startsWith('T-')) continue;
|
|
93
|
+
const model = CORE_THINKING_MODELS.find((m) => m.id === id);
|
|
94
|
+
if (!model) continue;
|
|
95
|
+
const lp: LedgerPrinciple = {
|
|
96
|
+
id,
|
|
97
|
+
version: 1,
|
|
98
|
+
text: model.description,
|
|
99
|
+
coreAxiomId: id,
|
|
100
|
+
triggerPattern: '',
|
|
101
|
+
action: '',
|
|
102
|
+
status: 'active',
|
|
103
|
+
priority: 'P1',
|
|
104
|
+
scope: 'general',
|
|
105
|
+
evaluability: entry.evaluability,
|
|
106
|
+
valueScore: 0,
|
|
107
|
+
adherenceRate: 0,
|
|
108
|
+
painPreventedCount: 0,
|
|
109
|
+
derivedFromPainIds: [],
|
|
110
|
+
ruleIds: [],
|
|
111
|
+
conflictsWithPrincipleIds: [],
|
|
112
|
+
createdAt: now,
|
|
113
|
+
updatedAt: now,
|
|
114
|
+
suggestedRules: [],
|
|
115
|
+
};
|
|
116
|
+
addPrincipleToLedger(stateDir, lp);
|
|
117
|
+
}
|
|
118
|
+
// Reload ledger after migration so subsequent reads see the new data.
|
|
119
|
+
ledger = loadLedger(stateDir);
|
|
120
|
+
}
|
|
121
|
+
|
|
80
122
|
// Select principles for bootstrap
|
|
81
123
|
const selectedPrincipleIds = selectPrinciplesForBootstrap(stateDir, limit);
|
|
82
124
|
|
|
83
|
-
// Load current ledger state
|
|
84
|
-
const ledger = loadLedger(stateDir);
|
|
85
|
-
|
|
86
125
|
const results: BootstrapResult[] = [];
|
|
87
126
|
|
|
88
127
|
for (const principleId of selectedPrincipleIds) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EvolutionHook interface for the Evolution SDK.
|
|
3
|
+
*
|
|
4
|
+
* Provides a callback-based interface for observing evolution lifecycle
|
|
5
|
+
* events: pain detection, principle creation, and principle promotion.
|
|
6
|
+
*
|
|
7
|
+
* Per D-03, this interface contains only the 3 core event methods.
|
|
8
|
+
* Per D-04, consumers implement the interface directly (no EventEmitter).
|
|
9
|
+
* Hooks not needed can use the provided noOpEvolutionHook and override
|
|
10
|
+
* individual methods.
|
|
11
|
+
*/
|
|
12
|
+
import type { PainSignal } from './pain-signal.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Event Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Event payload for principle creation lifecycle events. */
|
|
19
|
+
export interface PrincipleCreatedEvent {
|
|
20
|
+
/** Unique principle identifier */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Principle text ("When X, then Y.") */
|
|
23
|
+
text: string;
|
|
24
|
+
/** What triggered this principle's creation */
|
|
25
|
+
trigger: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Event payload for principle promotion lifecycle events. */
|
|
29
|
+
export interface PrinciplePromotedEvent {
|
|
30
|
+
/** Unique principle identifier */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Previous status tier */
|
|
33
|
+
from: string;
|
|
34
|
+
/** New status tier */
|
|
35
|
+
to: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// EvolutionHook Interface
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Callback interface for observing evolution lifecycle events.
|
|
44
|
+
*
|
|
45
|
+
* Implement all 3 methods, or spread noOpEvolutionHook and override
|
|
46
|
+
* only the methods you need:
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const myHook: EvolutionHook = {
|
|
51
|
+
* ...noOpEvolutionHook,
|
|
52
|
+
* onPainDetected(signal) { console.log(signal); },
|
|
53
|
+
* };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export interface EvolutionHook {
|
|
57
|
+
/** Called when a pain signal is detected and recorded. */
|
|
58
|
+
onPainDetected(signal: PainSignal): void;
|
|
59
|
+
/** Called when a new principle candidate is created. */
|
|
60
|
+
onPrincipleCreated(event: PrincipleCreatedEvent): void;
|
|
61
|
+
/** Called when a principle is promoted to a higher tier. */
|
|
62
|
+
onPrinciplePromoted(event: PrinciplePromotedEvent): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// No-op Helper
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/** No-op implementation -- consumers can spread and override individual methods. */
|
|
70
|
+
export const noOpEvolutionHook: EvolutionHook = {
|
|
71
|
+
onPainDetected(_signal: PainSignal): void {},
|
|
72
|
+
onPrincipleCreated(_event: PrincipleCreatedEvent): void {},
|
|
73
|
+
onPrinciplePromoted(_event: PrinciplePromotedEvent): void {},
|
|
74
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileStorageAdapter — file-backed implementation of StorageAdapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps principle-tree-ledger functions with the async StorageAdapter
|
|
5
|
+
* contract. Uses withLockAsync for thread-safe mutateLedger with
|
|
6
|
+
* retry with exponential backoff for lock acquisition (5 retries).
|
|
7
|
+
* Write failures are logged via SystemLogger and re-thrown.
|
|
8
|
+
*
|
|
9
|
+
* Guarantees:
|
|
10
|
+
* - Atomic writes via atomicWriteFileSync (temp + rename)
|
|
11
|
+
* - Thread-safe concurrent access via file locks
|
|
12
|
+
* - Consistent read-after-write visibility
|
|
13
|
+
* - Write failures logged to SystemLogger and re-thrown
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import type { StorageAdapter } from './storage-adapter.js';
|
|
18
|
+
import type { HybridLedgerStore } from './principle-tree-ledger.js';
|
|
19
|
+
import { TREE_NAMESPACE } from './principle-tree-ledger.js';
|
|
20
|
+
import {
|
|
21
|
+
loadLedger as loadLedgerFromFile,
|
|
22
|
+
saveLedgerAsync,
|
|
23
|
+
} from './principle-tree-ledger.js';
|
|
24
|
+
import { withLockAsync, type LockOptions, LockAcquisitionError } from '../utils/file-lock.js';
|
|
25
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
26
|
+
import { SystemLogger } from './system-logger.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Configuration
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** Maximum retries for lock acquisition in mutateLedger. */
|
|
33
|
+
const MUTATE_RETRY_COUNT = 5;
|
|
34
|
+
|
|
35
|
+
/** Base delay in ms for exponential backoff between retries. */
|
|
36
|
+
const MUTATE_BACKOFF_BASE_MS = 50;
|
|
37
|
+
|
|
38
|
+
/** Maximum backoff delay in ms. */
|
|
39
|
+
const MUTATE_BACKOFF_MAX_MS = 500;
|
|
40
|
+
|
|
41
|
+
const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Internal helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serialize the hybrid ledger store to JSON.
|
|
49
|
+
* Mirrors the unexported serializeLedger from principle-tree-ledger.ts.
|
|
50
|
+
*/
|
|
51
|
+
function serializeStore(store: HybridLedgerStore): string {
|
|
52
|
+
return JSON.stringify(
|
|
53
|
+
{
|
|
54
|
+
...store.trainingStore,
|
|
55
|
+
[TREE_NAMESPACE]: {
|
|
56
|
+
...store.tree,
|
|
57
|
+
lastUpdated: new Date().toISOString(),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
null,
|
|
61
|
+
2,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Ensure the parent directory exists before writing. */
|
|
66
|
+
function ensureParentDir(filePath: string): void {
|
|
67
|
+
const dir = path.dirname(filePath);
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// FileStorageAdapter
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* File-system backed storage adapter for the principle ledger.
|
|
79
|
+
*
|
|
80
|
+
* Delegates read/write operations to principle-tree-ledger while providing
|
|
81
|
+
* the async StorageAdapter interface. The mutateLedger method uses
|
|
82
|
+
* withLockAsync with exponential backoff retry for robust concurrent access.
|
|
83
|
+
*/
|
|
84
|
+
export class FileStorageAdapter implements StorageAdapter {
|
|
85
|
+
private readonly stateDir: string;
|
|
86
|
+
private readonly workspaceDir: string | undefined;
|
|
87
|
+
|
|
88
|
+
constructor(stateDir: string, workspaceDir?: string) {
|
|
89
|
+
this.stateDir = stateDir;
|
|
90
|
+
this.workspaceDir = workspaceDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve the ledger file path for this state directory. */
|
|
94
|
+
private get filePath(): string {
|
|
95
|
+
return path.join(this.stateDir, PRINCIPLE_TRAINING_FILE);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load the current ledger state from the file system.
|
|
100
|
+
*
|
|
101
|
+
* Returns an empty store if no persisted state exists (first run).
|
|
102
|
+
* Uses the synchronous loadLedger from principle-tree-ledger which
|
|
103
|
+
* handles missing/corrupted files gracefully.
|
|
104
|
+
*/
|
|
105
|
+
async loadLedger(): Promise<HybridLedgerStore> {
|
|
106
|
+
return loadLedgerFromFile(this.stateDir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Persist the full ledger state atomically.
|
|
111
|
+
*
|
|
112
|
+
* Delegates to principle-tree-ledger's saveLedgerAsync which uses
|
|
113
|
+
* withLockAsync internally. Logs failures via SystemLogger.
|
|
114
|
+
*/
|
|
115
|
+
async saveLedger(store: HybridLedgerStore): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
await saveLedgerAsync(this.stateDir, store);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
SystemLogger.log(
|
|
120
|
+
this.workspaceDir,
|
|
121
|
+
'STORAGE_WRITE_FAILED',
|
|
122
|
+
`FileStorageAdapter.saveLedger failed: ${String(err)}`,
|
|
123
|
+
);
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Perform a read-modify-write cycle with automatic locking and retry.
|
|
130
|
+
*
|
|
131
|
+
* Uses withLockAsync to acquire a file lock, reads the current store,
|
|
132
|
+
* applies the mutate function, then writes the modified store atomically.
|
|
133
|
+
* On lock acquisition failure, retries up to MUTATE_RETRY_COUNT (5) times
|
|
134
|
+
* with exponential backoff + jitter to reduce contention.
|
|
135
|
+
*
|
|
136
|
+
* Write failures are logged to SystemLogger and re-thrown so callers
|
|
137
|
+
* can decide how to handle persistence errors.
|
|
138
|
+
*/
|
|
139
|
+
async mutateLedger<T>(mutate: (store: HybridLedgerStore) => T | Promise<T>): Promise<T> {
|
|
140
|
+
let lastError: Error | undefined;
|
|
141
|
+
|
|
142
|
+
for (let attempt = 0; attempt < MUTATE_RETRY_COUNT; attempt++) {
|
|
143
|
+
try {
|
|
144
|
+
const lockOptions: LockOptions = {
|
|
145
|
+
maxRetries: 3,
|
|
146
|
+
baseRetryDelayMs: 10,
|
|
147
|
+
maxRetryDelayMs: 200,
|
|
148
|
+
lockStaleMs: 10_000,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const ledgerPath = this.filePath;
|
|
152
|
+
|
|
153
|
+
return await withLockAsync(ledgerPath, async () => {
|
|
154
|
+
const store = loadLedgerFromFile(this.stateDir);
|
|
155
|
+
const result = await mutate(store);
|
|
156
|
+
|
|
157
|
+
// Write directly — we already hold the lock, so we must NOT
|
|
158
|
+
// call saveLedger/saveLedgerAsync (they try to acquire the same lock).
|
|
159
|
+
try {
|
|
160
|
+
ensureParentDir(ledgerPath);
|
|
161
|
+
atomicWriteFileSync(ledgerPath, serializeStore(store));
|
|
162
|
+
} catch (writeErr) {
|
|
163
|
+
SystemLogger.log(
|
|
164
|
+
this.workspaceDir,
|
|
165
|
+
'STORAGE_WRITE_FAILED',
|
|
166
|
+
`FileStorageAdapter.mutateLedger write failed: ${String(writeErr)}`,
|
|
167
|
+
);
|
|
168
|
+
throw writeErr;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}, lockOptions);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
lastError = err as Error;
|
|
175
|
+
|
|
176
|
+
// Only retry on lock acquisition errors
|
|
177
|
+
if (err instanceof LockAcquisitionError && attempt < MUTATE_RETRY_COUNT - 1) {
|
|
178
|
+
const delay = Math.min(
|
|
179
|
+
MUTATE_BACKOFF_BASE_MS * Math.pow(2, attempt),
|
|
180
|
+
MUTATE_BACKOFF_MAX_MS,
|
|
181
|
+
);
|
|
182
|
+
// Add jitter (0-20%) to avoid thundering herd
|
|
183
|
+
const jitter = delay * 0.2 * Math.random();
|
|
184
|
+
const totalDelay = Math.floor(delay + jitter);
|
|
185
|
+
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Non-retryable error or exhausted retries
|
|
191
|
+
SystemLogger.log(
|
|
192
|
+
this.workspaceDir,
|
|
193
|
+
'STORAGE_MUTATE_FAILED',
|
|
194
|
+
`FileStorageAdapter.mutateLedger failed after ${attempt + 1} attempts: ${String(err)}`,
|
|
195
|
+
);
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Should not reach here, but satisfy the type checker
|
|
201
|
+
throw lastError ?? new Error('FileStorageAdapter.mutateLedger: unexpected state');
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/core/init.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
|
|
|
5
5
|
import { PD_DIRS } from './paths.js';
|
|
6
6
|
import { defaultContextConfig } from '../types.js';
|
|
7
7
|
import { loadStore, setPrincipleState, type PrincipleTrainingState } from './principle-training-state.js';
|
|
8
|
+
import { addPrincipleToLedger } from './principle-tree-ledger.js';
|
|
9
|
+
import type { LedgerPrinciple } from './principle-tree-ledger.js';
|
|
8
10
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
9
11
|
import { createDefaultKeywordStore, saveKeywordStore } from './empathy-keyword-matcher.js';
|
|
10
12
|
|
|
@@ -150,7 +152,7 @@ function copyRecursiveSync(srcDir: string, destDir: string, api: OpenClawPluginA
|
|
|
150
152
|
* Core thinking model definitions (T-01 through T-10).
|
|
151
153
|
* These are the built-in cognitive patterns that every workspace should have.
|
|
152
154
|
*/
|
|
153
|
-
const CORE_THINKING_MODELS: Array<{
|
|
155
|
+
export const CORE_THINKING_MODELS: Array<{
|
|
154
156
|
id: string;
|
|
155
157
|
name: string;
|
|
156
158
|
description: string;
|
|
@@ -190,7 +192,7 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
|
|
|
190
192
|
for (const model of CORE_THINKING_MODELS) {
|
|
191
193
|
const state: PrincipleTrainingState = {
|
|
192
194
|
principleId: model.id,
|
|
193
|
-
evaluability: '
|
|
195
|
+
evaluability: 'manual_only',
|
|
194
196
|
applicableOpportunityCount: 0,
|
|
195
197
|
observedViolationCount: 0,
|
|
196
198
|
complianceRate: 0,
|
|
@@ -202,6 +204,31 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
|
|
|
202
204
|
internalizationStatus: 'needs_training',
|
|
203
205
|
};
|
|
204
206
|
setPrincipleState(stateDir, state);
|
|
207
|
+
|
|
208
|
+
// Also write to Ledger Tree so bootstrapRules() can find them
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
const ledgerPrinciple: LedgerPrinciple = {
|
|
211
|
+
id: model.id,
|
|
212
|
+
version: 1,
|
|
213
|
+
text: model.description,
|
|
214
|
+
coreAxiomId: model.id,
|
|
215
|
+
triggerPattern: '',
|
|
216
|
+
action: '',
|
|
217
|
+
status: 'active',
|
|
218
|
+
priority: 'P1',
|
|
219
|
+
scope: 'general',
|
|
220
|
+
evaluability: 'manual_only',
|
|
221
|
+
valueScore: 0,
|
|
222
|
+
adherenceRate: 0,
|
|
223
|
+
painPreventedCount: 0,
|
|
224
|
+
derivedFromPainIds: [],
|
|
225
|
+
ruleIds: [],
|
|
226
|
+
conflictsWithPrincipleIds: [],
|
|
227
|
+
createdAt: now,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
suggestedRules: [],
|
|
230
|
+
};
|
|
231
|
+
addPrincipleToLedger(stateDir, ledgerPrinciple);
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
logger.info(`[PD] Initialized ${CORE_THINKING_MODELS.length} core thinking models: T-01 through T-10`);
|