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.
Files changed (129) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +3 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +27 -28
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +209 -104
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +2 -2
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  113. package/templates/pain_settings.json +1 -1
  114. package/tests/build-artifacts.test.ts +4 -58
  115. package/tests/commands/pd-reflect.test.ts +49 -0
  116. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  117. package/tests/core/pain-auto-repair.test.ts +96 -0
  118. package/tests/core/pain-integration.test.ts +483 -0
  119. package/tests/core/pain.test.ts +5 -4
  120. package/tests/core/workspace-dir-service.test.ts +68 -0
  121. package/tests/core/workspace-dir-validation.test.ts +56 -192
  122. package/tests/hooks/pain.test.ts +20 -0
  123. package/tests/http/principles-console-route.test.ts +42 -20
  124. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  125. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  126. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  127. package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
  128. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  129. 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 no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 shadow rollout profile validation
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
- // eslint-disable-next-line no-unused-vars -- Reason: callback parameter name is type documentation, actual value passed at call site
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 no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 profile-based targeting
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
391
+
392
392
  let arbiterRejectRate: number;
393
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
427
+
428
428
  let executabilityRejectRate: number;
429
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all branches
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
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures */
72
+
73
73
  evaluate(sample: unknown): { passed: boolean; reason?: string; decision: string };
74
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Public API method that delegates to evaluator, no instance state needed
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
- /* eslint-disable no-unused-vars -- Reason: type-only parameters in type cast, not used at runtime */
205
+
206
206
  const evaluate = moduleExports.evaluate as (
207
207
  _input: RuleHostInput,
208
208
  _helpers: RuleHostHelpers,
209
209
  ) => RuleHostResult;
210
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: percentage threshold calculation requires all 4 params - refactoring would break API
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
- /* eslint-disable no-unused-vars -- Reason: _input parameter name in interface type definition intentionally unused - actual implementation uses different param names */
79
+
80
80
  evaluate: (_input: RuleHostInput) => RuleHostResult;
81
- /* eslint-enable no-unused-vars */
81
+
82
82
  }
@@ -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
- /* eslint-disable no-unused-vars -- Reason: logger callback param name intentionally unused - callback only invoked for side effects */
39
+
40
40
  warn?: (_message: string) => void;
41
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- undefined is valid zero value, checked before use
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
- /* eslint-disable no-unused-vars -- Reason: type cast params intentionally unused - they're just type annotations, actual function uses different params */
221
+
222
222
  const rawEvaluate = moduleExports.evaluate as (
223
223
  _input: RuleHostInput,
224
224
  _helpers: ReturnType<typeof createRuleHostHelpers>
225
225
  ) => RuleHostResult;
226
- /* eslint-enable no-unused-vars */
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
- /* eslint-disable no-unused-vars -- Reason: interface method param names are part of type signature */
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
- /* eslint-disable no-unused-vars -- Reason: interface callback params are part of type signature */
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: loadAllSessions is defined later in this file, called here for organizational reasons
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: session creation requires all 4 params (sessionId, workspaceDir, sessionKey, trigger) - refactoring would break API
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: LLM output tracking requires all 6 params - refactoring would break API
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: friction tracking requires all 5 params - refactoring would break API
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
- /* eslint-disable no-unused-vars -- Reason: registry param name in type signature intentionally unused - actual function uses different param name */
225
+
226
226
  function withShadowRegistryLock<T>(
227
227
  stateDir: string,
228
228
  fn: (_registry: ShadowRegistry) => T
229
229
  ): T {
230
- /* eslint-enable no-unused-vars */
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
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: shadow observation completion requires all 4 params - refactoring would break API
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) => { // eslint-disable-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: fire-and-forget, errors silently dropped
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, no-unused-vars -- Reason: intentionally unused - silently fail if we can't setup the log
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
- /* eslint-disable @typescript-eslint/init-declarations, @typescript-eslint/no-use-before-define, @typescript-eslint/prefer-destructuring, no-useless-assignment, @typescript-eslint/no-unused-vars */
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
- /* eslint-disable no-unused-vars -- Reason: stateDir is used via this.stateDir in createExperiment method */
568
+
569
569
  constructor(private readonly stateDir: string) {}
570
- /* eslint-enable no-unused-vars */
570
+
571
571
 
572
572
  /**
573
573
  * Create a new training experiment.
@@ -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, no-unused-vars -- Reason: _filePath extracted for potential future use but currently unused */
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
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
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring -- Reason: filePath is a reassignable outer let variable - destructuring would lose the assignment semantics
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
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: _fromVersion reserved for future migration logic */
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
- /* eslint-disable @typescript-eslint/max-params -- Reason: Audit record requires exportKind, mode, approvedOnly, filePath, and rowCount */
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
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch continues
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
- /* eslint-disable no-unused-vars -- Reason: db parameter name in callback type signature */
1663
+
1664
1664
  static use<T>(workspaceDir: string, fn: (_db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
1665
- /* eslint-enable no-unused-vars */
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
- /* eslint-disable no-unused-vars -- Reason: interface method param names intentionally unused - implementations provide actual names */
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
- /* eslint-enable no-unused-vars */
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
+ }