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
|
@@ -70,7 +70,7 @@ export const DEFAULT_ALLOWED_MARGIN = 0.05;
|
|
|
70
70
|
* Allowed worker profiles for Phase 7 shadow rollout.
|
|
71
71
|
* Only bounded local workers eligible. local-reader first, local-editor deferred.
|
|
72
72
|
*/
|
|
73
|
-
// eslint-disable-next-line
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 shadow rollout profile validation
|
|
74
74
|
const ALLOWED_ROLLOUT_PROFILES: readonly TrainableWorkerProfile[] = ['local-reader'];
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -233,7 +233,7 @@ function writeRegistry(stateDir: string, registry: PromotionRegistry): void {
|
|
|
233
233
|
*/
|
|
234
234
|
function withPromotionRegistryLock<T>(
|
|
235
235
|
stateDir: string,
|
|
236
|
-
|
|
236
|
+
|
|
237
237
|
fn: (_registry: PromotionRegistry) => T
|
|
238
238
|
): T {
|
|
239
239
|
const registryPath = getRegistryPath(stateDir);
|
|
@@ -318,7 +318,7 @@ export function evaluatePromotionGate(
|
|
|
318
318
|
): PromotionGateResult {
|
|
319
319
|
const {
|
|
320
320
|
checkpointId,
|
|
321
|
-
// eslint-disable-next-line
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 profile-based targeting
|
|
322
322
|
targetProfile: _targetProfile,
|
|
323
323
|
baselineMetrics,
|
|
324
324
|
minDelta = DEFAULT_MIN_DELTA,
|
|
@@ -388,9 +388,9 @@ export function evaluatePromotionGate(
|
|
|
388
388
|
// PREFER real shadow evidence over eval verdict proxy
|
|
389
389
|
// Shadow evidence comes from actual runtime routing decisions
|
|
390
390
|
const shadowStats = computeShadowStats(stateDir, { checkpointId });
|
|
391
|
-
|
|
391
|
+
|
|
392
392
|
let arbiterRejectRate: number;
|
|
393
|
-
|
|
393
|
+
|
|
394
394
|
let arbiterRejectSource: 'shadow' | 'eval-proxy';
|
|
395
395
|
|
|
396
396
|
if (shadowStats && shadowStats.isStatisticallySignificant) {
|
|
@@ -424,9 +424,9 @@ export function evaluatePromotionGate(
|
|
|
424
424
|
|
|
425
425
|
// --- Check 6: Executability reject rate constraint ---
|
|
426
426
|
// PREFER real shadow evidence: escalation rate + profile rejection rate
|
|
427
|
-
|
|
427
|
+
|
|
428
428
|
let executabilityRejectRate: number;
|
|
429
|
-
|
|
429
|
+
|
|
430
430
|
let executabilityRejectSource: 'shadow' | 'eval-proxy';
|
|
431
431
|
|
|
432
432
|
if (shadowStats && shadowStats.isStatisticallySignificant) {
|
|
@@ -484,7 +484,7 @@ export function evaluatePromotionGate(
|
|
|
484
484
|
qualityCheck.passed;
|
|
485
485
|
|
|
486
486
|
// --- Suggest state based on checks ---
|
|
487
|
-
|
|
487
|
+
|
|
488
488
|
let suggestedState: PromotionState | undefined;
|
|
489
489
|
if (allPassed) {
|
|
490
490
|
suggestedState = 'candidate_only';
|
|
@@ -585,7 +585,7 @@ export function advancePromotion(
|
|
|
585
585
|
// - rejected → candidate_only/shadow_ready: allowed via re-evaluation
|
|
586
586
|
// (new eval data may reverse a previous rejection)
|
|
587
587
|
//
|
|
588
|
-
|
|
588
|
+
|
|
589
589
|
let targetState: PromotionState;
|
|
590
590
|
if (!gateResult.passes) {
|
|
591
591
|
targetState = 'rejected';
|
|
@@ -69,9 +69,9 @@ export interface ReplayReport {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export interface CandidateEvaluator {
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
evaluate(sample: unknown): { passed: boolean; reason?: string; decision: string };
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export class ReplayEngine {
|
|
@@ -112,7 +112,7 @@ export class ReplayEngine {
|
|
|
112
112
|
return samples;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
runSingleSample(sample: ReplaySample, evaluator: CandidateEvaluator): ReplayResult {
|
|
117
117
|
const evaluation = evaluator.evaluate(sample);
|
|
118
118
|
return {
|
|
@@ -202,12 +202,12 @@ export class ReplayEngine {
|
|
|
202
202
|
throw new Error(`Implementation ${implementation.id} does not export evaluate().`);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
const evaluate = moduleExports.evaluate as (
|
|
207
207
|
_input: RuleHostInput,
|
|
208
208
|
_helpers: RuleHostHelpers,
|
|
209
209
|
) => RuleHostResult;
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
|
|
212
212
|
return {
|
|
213
213
|
evaluate: (sample: unknown) => {
|
|
@@ -284,7 +284,7 @@ export class ReplayEngine {
|
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
private _selectToolCall(
|
|
289
289
|
snapshot: NocturnalSessionSnapshot,
|
|
290
290
|
classification: SampleClassification,
|
|
@@ -314,7 +314,7 @@ export class ReplayEngine {
|
|
|
314
314
|
return byNewest[0] ?? null;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
|
|
318
318
|
private _matchGateBlock(
|
|
319
319
|
gateBlocks: NocturnalGateBlock[],
|
|
320
320
|
toolCall: NocturnalToolCall,
|
|
@@ -351,7 +351,7 @@ export class ReplayEngine {
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
|
|
355
355
|
private _estimateLineChanges(toolCall: NocturnalToolCall): number {
|
|
356
356
|
if (toolCall.toolName === 'edit' || toolCall.toolName === 'write') {
|
|
357
357
|
return 20;
|
|
@@ -359,7 +359,7 @@ export class ReplayEngine {
|
|
|
359
359
|
return 0;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
|
|
363
363
|
private _inferBashRisk(toolCall: NocturnalToolCall): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
364
364
|
if (toolCall.toolName !== 'bash' && toolCall.toolName !== 'run_shell_command') {
|
|
365
365
|
return 'unknown';
|
|
@@ -371,7 +371,7 @@ export class ReplayEngine {
|
|
|
371
371
|
return toolCall.outcome === 'success' ? 'safe' : 'normal';
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
private _scoreEvaluation(
|
|
376
376
|
sample: ReplaySample,
|
|
377
377
|
result: RuleHostResult,
|
|
@@ -465,7 +465,7 @@ export class ReplayEngine {
|
|
|
465
465
|
};
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
|
|
469
469
|
private _determineDecision(
|
|
470
470
|
pain: ClassificationSummary,
|
|
471
471
|
success: ClassificationSummary,
|
|
@@ -495,7 +495,7 @@ export class ReplayEngine {
|
|
|
495
495
|
});
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
|
|
499
499
|
private _deriveExpectedOutcome(
|
|
500
500
|
record: NocturnalDatasetRecord,
|
|
501
501
|
): ReplaySample['expectedOutcome'] {
|
|
@@ -93,7 +93,7 @@ export function getTargetFileLineCount(absoluteFilePath: string): number | null
|
|
|
93
93
|
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
94
94
|
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
export function calculatePercentageThreshold(
|
|
98
98
|
targetLineCount: number,
|
|
99
99
|
percentage: number,
|
|
@@ -76,7 +76,7 @@ export interface LoadedImplementation {
|
|
|
76
76
|
implId: string;
|
|
77
77
|
ruleId: string;
|
|
78
78
|
meta: RuleHostMeta;
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
evaluate: (_input: RuleHostInput) => RuleHostResult;
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
}
|
package/src/core/rule-host.ts
CHANGED
|
@@ -36,9 +36,9 @@ import type {
|
|
|
36
36
|
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
37
37
|
|
|
38
38
|
export interface RuleHostLogger {
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
warn?: (_message: string) => void;
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export class RuleHost {
|
|
@@ -69,7 +69,7 @@ export class RuleHost {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Merge decisions from all active implementations
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
let blocked: RuleHostResult | undefined;
|
|
74
74
|
const approvals: RuleHostResult[] = [];
|
|
75
75
|
|
|
@@ -218,12 +218,12 @@ export class RuleHost {
|
|
|
218
218
|
|
|
219
219
|
// Return a loaded implementation that wraps the compiled evaluate
|
|
220
220
|
// with the actual helpers from the input at evaluation time
|
|
221
|
-
|
|
221
|
+
|
|
222
222
|
const rawEvaluate = moduleExports.evaluate as (
|
|
223
223
|
_input: RuleHostInput,
|
|
224
224
|
_helpers: ReturnType<typeof createRuleHostHelpers>
|
|
225
225
|
) => RuleHostResult;
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
|
|
228
228
|
return {
|
|
229
229
|
implId: impl.id,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Kept separate to avoid circular dependencies between schema-definitions and migration-runner.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
|
|
8
8
|
/** Minimal interface for better-sqlite3 Database instances. */
|
|
9
9
|
export interface Db {
|
|
@@ -35,7 +35,7 @@ export interface Migration {
|
|
|
35
35
|
name: string;
|
|
36
36
|
/** Which database file this migration applies to */
|
|
37
37
|
db: DbType;
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
/** Apply this migration */
|
|
40
40
|
up: (_db: Db) => void;
|
|
41
41
|
/** Revert this migration */
|
|
@@ -33,6 +33,7 @@ export interface SessionState {
|
|
|
33
33
|
lastErrorSource?: string;
|
|
34
34
|
lastErrorHash: string;
|
|
35
35
|
consecutiveErrors: number;
|
|
36
|
+
lastGfiDecayAt?: number; // Timestamp of last GFI decay (for time-based decay)
|
|
36
37
|
|
|
37
38
|
// Daily statistics (persisted)
|
|
38
39
|
dailyToolCalls: number;
|
|
@@ -82,7 +83,7 @@ export function initPersistence(stateDir: string): void {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// Load all existing sessions
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
loadAllSessions();
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -138,6 +139,14 @@ function persistSession(state: SessionState): void {
|
|
|
138
139
|
|
|
139
140
|
try {
|
|
140
141
|
fs.writeFileSync(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
142
|
+
// Log successful persistence with GFI snapshot for debugging
|
|
143
|
+
if (state.currentGfi > 0) {
|
|
144
|
+
SystemLogger.log(
|
|
145
|
+
state.workspaceDir,
|
|
146
|
+
'GFI_PERSIST',
|
|
147
|
+
`Session ${state.sessionId.slice(0, 8)} persisted: GFI=${state.currentGfi.toFixed(1)}, sources=${JSON.stringify(state.gfiBySource)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
141
150
|
} catch (error) {
|
|
142
151
|
logSessionTrackerWarning(`Failed to persist session ${state.sessionId}`, error);
|
|
143
152
|
}
|
|
@@ -171,7 +180,7 @@ export function flushAllSessions(): void {
|
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
|
|
183
|
+
|
|
175
184
|
function getOrCreateSession(sessionId: string, workspaceDir?: string, sessionKey?: string, trigger?: string): SessionState {
|
|
176
185
|
let state = sessions.get(sessionId);
|
|
177
186
|
if (!state) {
|
|
@@ -194,6 +203,7 @@ function getOrCreateSession(sessionId: string, workspaceDir?: string, sessionKey
|
|
|
194
203
|
lastErrorSource: '',
|
|
195
204
|
lastErrorHash: '',
|
|
196
205
|
consecutiveErrors: 0,
|
|
206
|
+
lastGfiDecayAt: Date.now(),
|
|
197
207
|
dailyToolCalls: 0,
|
|
198
208
|
dailyToolFailures: 0,
|
|
199
209
|
dailyPainSignals: 0,
|
|
@@ -232,7 +242,7 @@ export function trackToolRead(sessionId: string, filePath: string, workspaceDir?
|
|
|
232
242
|
return state;
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
|
|
245
|
+
|
|
236
246
|
export function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined, config?: PainConfig, workspaceDir?: string, sessionKey?: string, trigger?: string): SessionState {
|
|
237
247
|
const state = getOrCreateSession(sessionId, workspaceDir, sessionKey, trigger);
|
|
238
248
|
state.llmTurns += 1;
|
|
@@ -271,7 +281,7 @@ export function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined,
|
|
|
271
281
|
/**
|
|
272
282
|
* Tracks physical friction based on tool execution failures.
|
|
273
283
|
*/
|
|
274
|
-
|
|
284
|
+
|
|
275
285
|
export function trackFriction(
|
|
276
286
|
sessionId: string,
|
|
277
287
|
deltaF: number,
|
|
@@ -305,6 +315,8 @@ export function trackFriction(
|
|
|
305
315
|
state.dailyGfiPeak = Math.max(state.dailyGfiPeak, state.currentGfi);
|
|
306
316
|
|
|
307
317
|
// Schedule persistence
|
|
318
|
+
// Update decay anchor to prevent retroactive decay of the new friction
|
|
319
|
+
state.lastGfiDecayAt = Date.now();
|
|
308
320
|
schedulePersistence(state);
|
|
309
321
|
|
|
310
322
|
return state;
|
|
@@ -516,3 +528,83 @@ export function resetDailyStats(sessionId: string): void {
|
|
|
516
528
|
schedulePersistence(state);
|
|
517
529
|
}
|
|
518
530
|
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Apply time-based decay to GFI using segmented exponential decay.
|
|
534
|
+
*
|
|
535
|
+
* Decay rates:
|
|
536
|
+
* - GFI >= 70 (severe): 3%/min - fast recovery to avoid prolonged blocking
|
|
537
|
+
* - GFI 40-70 (moderate): 2%/min - medium decay
|
|
538
|
+
* - GFI < 40 (mild): 1%/min - slow decay to retain as warning
|
|
539
|
+
*
|
|
540
|
+
* Formula: GFI_new = GFI * (1 - λ)^elapsedMinutes
|
|
541
|
+
*
|
|
542
|
+
* @param sessionId - The session to decay
|
|
543
|
+
* @param elapsedMinutes - Minutes since last decay
|
|
544
|
+
* @returns Updated session state, or undefined if session not found or GFI is 0
|
|
545
|
+
*/
|
|
546
|
+
export function decayGfi(sessionId: string, elapsedMinutes: number): SessionState | undefined {
|
|
547
|
+
const state = sessions.get(sessionId);
|
|
548
|
+
if (!state || state.currentGfi <= 0 || elapsedMinutes <= 0) return undefined;
|
|
549
|
+
|
|
550
|
+
// Determine decay rate based on current GFI level (segmented)
|
|
551
|
+
let decayRate: number;
|
|
552
|
+
if (state.currentGfi >= 70) {
|
|
553
|
+
decayRate = 0.03; // 3%/min for severe friction
|
|
554
|
+
} else if (state.currentGfi >= 40) {
|
|
555
|
+
decayRate = 0.02; // 2%/min for moderate friction
|
|
556
|
+
} else {
|
|
557
|
+
decayRate = 0.01; // 1%/min for mild friction
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Exponential decay: GFI_new = GFI * (1-λ)^Δt
|
|
561
|
+
const decayFactor = Math.pow(1 - decayRate, elapsedMinutes);
|
|
562
|
+
const previousGfi = state.currentGfi;
|
|
563
|
+
state.currentGfi = Math.max(0, state.currentGfi * decayFactor);
|
|
564
|
+
|
|
565
|
+
// Apply same decay factor to all sources
|
|
566
|
+
const ledger = ensureGfiLedger(state);
|
|
567
|
+
for (const source of Object.keys(ledger)) {
|
|
568
|
+
ledger[source] = Math.max(0, ledger[source] * decayFactor);
|
|
569
|
+
// Remove sources that have decayed below 0.1
|
|
570
|
+
if (ledger[source] < 0.1) {
|
|
571
|
+
delete ledger[source];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Round to 1 decimal place
|
|
576
|
+
state.currentGfi = Math.round(state.currentGfi * 10) / 10;
|
|
577
|
+
|
|
578
|
+
// Update last decay timestamp
|
|
579
|
+
state.lastGfiDecayAt = Date.now();
|
|
580
|
+
|
|
581
|
+
// Log if significant decay
|
|
582
|
+
const decayedAmount = previousGfi - state.currentGfi;
|
|
583
|
+
if (decayedAmount >= 1) {
|
|
584
|
+
SystemLogger.log(
|
|
585
|
+
state.workspaceDir,
|
|
586
|
+
'GFI_DECAY',
|
|
587
|
+
`GFI decayed by ${decayedAmount.toFixed(1)} (${elapsedMinutes}min at ${decayRate*100}%/min). ${previousGfi.toFixed(1)} → ${state.currentGfi.toFixed(1)}`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
schedulePersistence(state);
|
|
592
|
+
return state;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check if GFI decay should be applied and return elapsed minutes since last decay.
|
|
597
|
+
* @param sessionId - The session to check
|
|
598
|
+
* @returns Elapsed minutes since last decay, or 0 if no decay needed
|
|
599
|
+
*/
|
|
600
|
+
export function getGfiDecayElapsed(sessionId: string): number {
|
|
601
|
+
const state = sessions.get(sessionId);
|
|
602
|
+
if (!state || state.currentGfi <= 0) return 0;
|
|
603
|
+
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
const lastDecay = state.lastGfiDecayAt || state.lastControlActivityAt || state.lastActivityAt || now;
|
|
606
|
+
const elapsedMs = now - lastDecay;
|
|
607
|
+
|
|
608
|
+
// Return elapsed minutes (floor to whole minutes)
|
|
609
|
+
return Math.floor(elapsedMs / 60000);
|
|
610
|
+
}
|
|
@@ -222,12 +222,12 @@ function writeRegistry(stateDir: string, registry: ShadowRegistry): void {
|
|
|
222
222
|
/**
|
|
223
223
|
* Execute a read-modify-write under an exclusive file lock.
|
|
224
224
|
*/
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
function withShadowRegistryLock<T>(
|
|
227
227
|
stateDir: string,
|
|
228
228
|
fn: (_registry: ShadowRegistry) => T
|
|
229
229
|
): T {
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
const registryPath = getRegistryPath(stateDir);
|
|
232
232
|
return withLock(registryPath, () => {
|
|
233
233
|
const registry = readRegistry(stateDir);
|
|
@@ -341,7 +341,7 @@ export function completeShadowObservation(
|
|
|
341
341
|
* @param failureSignals - Runtime failure signals
|
|
342
342
|
* @returns The updated ShadowObservation, or null if not found
|
|
343
343
|
*/
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
export function completeShadowObservationByTask(
|
|
346
346
|
stateDir: string,
|
|
347
347
|
taskFingerprint: string,
|
|
@@ -25,10 +25,10 @@ export const SystemLogger = {
|
|
|
25
25
|
const logEntry = `[${timestamp}] [${eventType.padEnd(15)}] ${message}\n`;
|
|
26
26
|
|
|
27
27
|
// Use fire-and-forget async append to prevent blocking
|
|
28
|
-
fs.appendFile(logFile, logEntry, 'utf8', (_err) => {
|
|
28
|
+
fs.appendFile(logFile, logEntry, 'utf8', (_err) => {
|
|
29
29
|
// Silently drop errors (e.g. disk full) to not crash the gateway
|
|
30
30
|
});
|
|
31
|
-
} catch (e) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
31
|
+
} catch (e) { // eslint-disable-line @typescript-eslint/no-unused-vars -- Reason: intentionally unused - silently fail if we can't setup the log
|
|
32
32
|
// Silently fail if we can't setup the log
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -44,7 +44,7 @@ export function parseThinkingOsMd(content: string): ThinkingOsDirective[] {
|
|
|
44
44
|
|
|
45
45
|
// Match all <directive ...> ... </directive> blocks
|
|
46
46
|
const directiveRegex = /<directive\s+([^>]*)>([\s\S]*?)<\/directive>/gi;
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
let match: RegExpExecArray | null = null;
|
|
49
49
|
|
|
50
50
|
while ((match = directiveRegex.exec(content)) !== null) {
|
|
@@ -565,9 +565,9 @@ export function processTrainerResult(
|
|
|
565
565
|
* ```
|
|
566
566
|
*/
|
|
567
567
|
export class TrainingProgram {
|
|
568
|
-
|
|
568
|
+
|
|
569
569
|
constructor(private readonly stateDir: string) {}
|
|
570
|
-
|
|
570
|
+
|
|
571
571
|
|
|
572
572
|
/**
|
|
573
573
|
* Create a new training experiment.
|
package/src/core/trajectory.ts
CHANGED
|
@@ -208,7 +208,7 @@ export class TrajectoryDatabase {
|
|
|
208
208
|
const createdAt = input.createdAt ?? nowIso();
|
|
209
209
|
// Extract filePath from paramsJson if provided and is an object with filePath
|
|
210
210
|
const paramsObj = input.paramsJson as Record<string, unknown> | undefined;
|
|
211
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
211
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: _filePath extracted for potential future use but currently unused */
|
|
212
212
|
const _filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
|
|
213
213
|
const rowId = this.withWrite(() => {
|
|
214
214
|
const result = this.db.prepare(`
|
|
@@ -587,7 +587,7 @@ export class TrajectoryDatabase {
|
|
|
587
587
|
const limit = filters.limit ?? 100;
|
|
588
588
|
const offset = filters.offset ?? 0;
|
|
589
589
|
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
let rows: Record<string, unknown>[];
|
|
592
592
|
if (traceId) {
|
|
593
593
|
rows = this.db.prepare(`
|
|
@@ -783,7 +783,7 @@ export class TrajectoryDatabase {
|
|
|
783
783
|
try {
|
|
784
784
|
const params = JSON.parse(row.params_json);
|
|
785
785
|
if (params && typeof params.filePath === 'string') {
|
|
786
|
-
|
|
786
|
+
|
|
787
787
|
filePath = params.filePath;
|
|
788
788
|
}
|
|
789
789
|
} catch {
|
|
@@ -1324,7 +1324,7 @@ export class TrajectoryDatabase {
|
|
|
1324
1324
|
this.importLegacyEvolution();
|
|
1325
1325
|
}
|
|
1326
1326
|
|
|
1327
|
-
|
|
1327
|
+
|
|
1328
1328
|
private migrateSchema(_fromVersion?: number): void {
|
|
1329
1329
|
this.db.exec(`
|
|
1330
1330
|
DROP VIEW IF EXISTS v_daily_metrics;
|
|
@@ -1545,7 +1545,7 @@ export class TrajectoryDatabase {
|
|
|
1545
1545
|
});
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
1548
|
-
|
|
1548
|
+
|
|
1549
1549
|
private recordExportAudit(
|
|
1550
1550
|
exportKind: string,
|
|
1551
1551
|
mode: CorrectionExportMode,
|
|
@@ -1610,7 +1610,7 @@ export class TrajectoryDatabase {
|
|
|
1610
1610
|
for (const entry of fs.readdirSync(this.blobDir)) {
|
|
1611
1611
|
if (referenced.has(entry)) continue;
|
|
1612
1612
|
const fullPath = path.join(this.blobDir, entry);
|
|
1613
|
-
|
|
1613
|
+
|
|
1614
1614
|
let stat: fs.Stats;
|
|
1615
1615
|
try {
|
|
1616
1616
|
stat = fs.statSync(fullPath);
|
|
@@ -1660,9 +1660,9 @@ export class TrajectoryRegistry {
|
|
|
1660
1660
|
this.instances.clear();
|
|
1661
1661
|
}
|
|
1662
1662
|
|
|
1663
|
-
|
|
1663
|
+
|
|
1664
1664
|
static use<T>(workspaceDir: string, fn: (_db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
|
|
1665
|
-
|
|
1665
|
+
|
|
1666
1666
|
const normalized = path.resolve(workspaceDir);
|
|
1667
1667
|
const existing = this.instances.get(normalized);
|
|
1668
1668
|
if (existing) {
|
|
@@ -21,13 +21,13 @@ import {
|
|
|
21
21
|
import type { Principle, PrincipleValueMetrics } from '../types/principle-tree-schema.js';
|
|
22
22
|
import type { Principle as ActivePrinciple } from './evolution-types.js';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
interface PrincipleTreeLedgerAccessor {
|
|
26
26
|
getPrincipleSubtree(_principleId: string): PrincipleSubtree | undefined;
|
|
27
27
|
updatePrinciple(_principleId: string, updates: Partial<Principle>): Principle;
|
|
28
28
|
updatePrincipleValueMetrics(principleId: string, _metrics: PrincipleValueMetrics): PrincipleValueMetrics;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* WorkspaceContext - Centralized management of workspace-specific paths and services.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
|
|
2
|
+
import { validateWorkspaceDir } from './workspace-dir-validation.js';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceResolutionContext {
|
|
5
|
+
workspaceDir?: string;
|
|
6
|
+
agentId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkspaceResolutionOptions {
|
|
10
|
+
source?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
fallbackAgentId?: string;
|
|
13
|
+
logger?: PluginLogger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildResolutionFailureMessage(source: string, attempts: string[]): string {
|
|
17
|
+
const suffix = attempts.length > 0 ? ` Attempts: ${attempts.join(' | ')}` : '';
|
|
18
|
+
return `[PD:WorkspaceDir] ${source}: unable to resolve a valid workspace directory.${suffix}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tryResolveFromAgent(
|
|
22
|
+
api: OpenClawPluginApi,
|
|
23
|
+
agentId: string,
|
|
24
|
+
attempts: string[],
|
|
25
|
+
): string | undefined {
|
|
26
|
+
try {
|
|
27
|
+
const resolved = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
|
|
28
|
+
const issue = validateWorkspaceDir(resolved);
|
|
29
|
+
if (!issue) {
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
attempts.push(`agent:${agentId} invalid (${issue})`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
attempts.push(`agent:${agentId} threw (${String(error)})`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveWorkspaceDir(
|
|
41
|
+
api: OpenClawPluginApi,
|
|
42
|
+
ctx: WorkspaceResolutionContext,
|
|
43
|
+
options: WorkspaceResolutionOptions = {},
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const source = options.source ?? 'unknown';
|
|
46
|
+
const logger = options.logger ?? api.logger;
|
|
47
|
+
const attempts: string[] = [];
|
|
48
|
+
|
|
49
|
+
if (ctx.workspaceDir) {
|
|
50
|
+
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
51
|
+
if (!issue) {
|
|
52
|
+
return ctx.workspaceDir;
|
|
53
|
+
}
|
|
54
|
+
attempts.push(`ctx.workspaceDir invalid (${issue})`);
|
|
55
|
+
} else {
|
|
56
|
+
attempts.push('ctx.workspaceDir missing');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agentCandidates = [ctx.agentId, options.fallbackAgentId]
|
|
60
|
+
.filter((value, index, all): value is string => !!value && all.indexOf(value) === index);
|
|
61
|
+
|
|
62
|
+
for (const agentId of agentCandidates) {
|
|
63
|
+
const resolved = tryResolveFromAgent(api, agentId, attempts);
|
|
64
|
+
if (resolved) {
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const message = buildResolutionFailureMessage(source, attempts);
|
|
70
|
+
if (options.required) {
|
|
71
|
+
logger.error(message);
|
|
72
|
+
throw new Error(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.warn(message);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveRequiredWorkspaceDir(
|
|
80
|
+
api: OpenClawPluginApi,
|
|
81
|
+
ctx: WorkspaceResolutionContext,
|
|
82
|
+
options: Omit<WorkspaceResolutionOptions, 'required'> = {},
|
|
83
|
+
): string {
|
|
84
|
+
return resolveWorkspaceDir(api, ctx, { ...options, required: true }) as string;
|
|
85
|
+
}
|