principles-disciple 1.38.0 → 1.39.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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.38.0",
5
+ "version": "1.39.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.38.0",
3
+ "version": "1.39.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -325,7 +325,7 @@ export class EvolutionEngine {
325
325
  // ===== 事件管理 =====
326
326
 
327
327
 
328
- // eslint-disable-next-line @typescript-eslint/max-params
328
+
329
329
  private createEvent(
330
330
  type: 'success' | 'failure',
331
331
  taskHash: string,
@@ -388,7 +388,7 @@ export class EvolutionEngine {
388
388
  }
389
389
 
390
390
 
391
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
391
+
392
392
  private createNewScorecard(): EvolutionScorecard {
393
393
  const now = new Date().toISOString();
394
394
  return {
@@ -532,7 +532,7 @@ export class EvolutionEngine {
532
532
  // ===== 工具方法 =====
533
533
 
534
534
 
535
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
535
+
536
536
  private generateId(): string {
537
537
  return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
538
538
  }
@@ -95,7 +95,7 @@ export function recordGateBlockAndReturn(
95
95
  } catch (error: unknown) {
96
96
  logWarn(`[PD_GATE] Failed to record trajectory gate block: ${String(error)}`);
97
97
 
98
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
98
+
99
99
  scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, logWarn, logError);
100
100
  }
101
101
 
@@ -123,7 +123,7 @@ export function recordGateBlockAndReturn(
123
123
 
124
124
  // Write to pain flag file (merge with existing if present)
125
125
  try {
126
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring
126
+
127
127
  const workspaceDir = wctx.workspaceDir;
128
128
  const currentFlag = wctx.eventLog.findLatestPainSignal(sessionId);
129
129
  const currentScore = currentFlag?.score ?? 0;
@@ -183,7 +183,7 @@ This is a mandatory security gate. The operation was blocked because the modific
183
183
  * Failures are logged but do not affect the runtime block decision.
184
184
  */
185
185
 
186
- // eslint-disable-next-line @typescript-eslint/max-params
186
+
187
187
  function scheduleTrajectoryGateBlockRetry(
188
188
  wctx: WorkspaceContext,
189
189
  payload: {
@@ -10,12 +10,8 @@ import { SystemLogger } from '../core/system-logger.js';
10
10
  import { WorkspaceContext } from '../core/workspace-context.js';
11
11
  import type { EventLog } from '../core/event-log.js';
12
12
  import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
13
- import { acquireLockAsync, releaseLock as releaseImportedLock, type LockContext } from '../utils/file-lock.js';
14
13
  import { addDiagnosticianTask, completeDiagnosticianTask } from '../core/diagnostician-task-store.js';
15
14
  import { getEvolutionLogger } from '../core/evolution-logger.js';
16
- import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
17
- export type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
18
- import { LockUnavailableError } from '../config/index.js';
19
15
  import { atomicWriteFileSync } from '../utils/io.js';
20
16
  import { checkWorkspaceIdle, checkCooldown, recordCooldown } from './nocturnal-runtime.js';
21
17
  import { loadCooldownEscalationConfig, loadNocturnalConfigMerged } from './nocturnal-config.js';
@@ -37,6 +33,12 @@ import type { CorrectionObserverPayload } from './subagent-workflow/correction-o
37
33
  import { KeywordOptimizationService } from './keyword-optimization-service.js';
38
34
  import { TrajectoryRegistry } from '../core/trajectory.js';
39
35
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
36
+ import { isLegacyQueueItem, migrateQueueToV2, type RawQueueItem, type TaskKind, type TaskPriority } from './evolution-queue-migration.js';
37
+ export type { TaskKind, TaskPriority } from './evolution-queue-migration.js';
38
+ export { acquireQueueLock, EVOLUTION_QUEUE_LOCK_SUFFIX, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_MS } from './evolution-queue-lock.js';
39
+ import { requireQueueLock, EVOLUTION_QUEUE_LOCK_SUFFIX } from './evolution-queue-lock.js';
40
+ import { readRecentPainContext, buildPainSourceKey, hasRecentSimilarReflection } from './evolution-pain-context.js';
41
+ import { findRecentDuplicateTask } from './evolution-dedup.js';
40
42
  import { classifyFailure, type ClassifiableTaskKind } from './failure-classifier.js';
41
43
  import { recordPersistentFailure, resetFailureState, isTaskKindInCooldown } from './cooldown-strategy.js';
42
44
  import { reconcileStartup } from './startup-reconciler.js';
@@ -261,90 +263,6 @@ export interface EvolutionQueueItem {
261
263
  recentPainContext?: RecentPainContext;
262
264
  }
263
265
 
264
- /**
265
- * Legacy queue item shape (pre-V2) for migration compatibility.
266
- * These items lack taskKind, priority, retryCount, maxRetries, lastError fields.
267
- */
268
- interface LegacyEvolutionQueueItem {
269
- id: string;
270
- task?: string;
271
- score: number;
272
- source: string;
273
- reason: string;
274
- timestamp: string;
275
- enqueued_at?: string;
276
- started_at?: string;
277
- completed_at?: string;
278
- assigned_session_key?: string;
279
- trigger_text_preview?: string;
280
- status?: string;
281
- resolution?: string;
282
- session_id?: string;
283
- agent_id?: string;
284
- traceId?: string;
285
- taskKind?: string;
286
- priority?: string;
287
- retryCount?: number;
288
- maxRetries?: number;
289
- lastError?: string;
290
- resultRef?: string;
291
- }
292
-
293
- /**
294
- * Default values for new V2 fields when migrating legacy items.
295
- */
296
- const DEFAULT_TASK_KIND: TaskKind = 'pain_diagnosis';
297
- const DEFAULT_PRIORITY: TaskPriority = 'medium';
298
- const DEFAULT_MAX_RETRIES = 3;
299
-
300
- /**
301
- * Migrate a legacy queue item to V2 schema.
302
- * Old items without taskKind are assumed to be pain_diagnosis for backward compatibility.
303
- */
304
- function migrateToV2(item: LegacyEvolutionQueueItem): EvolutionQueueItem {
305
- return {
306
- id: item.id,
307
- taskKind: (item.taskKind as TaskKind) || DEFAULT_TASK_KIND,
308
- priority: (item.priority as TaskPriority) || DEFAULT_PRIORITY,
309
- source: item.source,
310
- traceId: item.traceId,
311
- task: item.task,
312
- score: item.score,
313
- reason: item.reason,
314
- timestamp: item.timestamp,
315
- enqueued_at: item.enqueued_at,
316
- started_at: item.started_at,
317
- completed_at: item.completed_at,
318
- assigned_session_key: item.assigned_session_key,
319
- trigger_text_preview: item.trigger_text_preview,
320
- status: (item.status as QueueStatus) || 'pending',
321
- resolution: item.resolution as TaskResolution | undefined,
322
- session_id: item.session_id,
323
- agent_id: item.agent_id,
324
- retryCount: item.retryCount || 0,
325
- maxRetries: item.maxRetries || DEFAULT_MAX_RETRIES,
326
- lastError: item.lastError,
327
- resultRef: item.resultRef,
328
- };
329
- }
330
-
331
- type RawQueueItem = Record<string, unknown>;
332
-
333
- /**
334
- * Check if an item is a legacy (pre-V2) queue item.
335
- */
336
- function isLegacyQueueItem(item: RawQueueItem): boolean {
337
- return item && typeof item === 'object' && !('taskKind' in item);
338
- }
339
-
340
- /**
341
- * Migrate entire queue to V2 schema if needed.
342
- * Returns a new array with all items migrated to V2 format.
343
- */
344
- function migrateQueueToV2(queue: RawQueueItem[]): EvolutionQueueItem[] {
345
- return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item as unknown as LegacyEvolutionQueueItem) : item as unknown as EvolutionQueueItem);
346
- }
347
-
348
266
  function isSessionAtOrBeforeTriggerTime(
349
267
  session: { startedAt: string; updatedAt: string },
350
268
  triggerTimeMs: number,
@@ -429,16 +347,6 @@ function buildFallbackNocturnalSnapshot(
429
347
  };
430
348
  }
431
349
 
432
- const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
433
-
434
- // P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
435
- export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
436
- export const LOCK_MAX_RETRIES = 50;
437
- export const LOCK_RETRY_DELAY_MS = 50;
438
- export const LOCK_STALE_MS = 30_000;
439
-
440
-
441
-
442
350
  export function createEvolutionTaskId(
443
351
  source: string,
444
352
  score: number,
@@ -454,61 +362,12 @@ export function createEvolutionTaskId(
454
362
  .substring(0, 8);
455
363
  }
456
364
 
457
-
458
- export async function acquireQueueLock(resourcePath: string, logger: PluginLogger | { warn?: (message: string) => void; info?: (message: string) => void } | undefined, lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX): Promise<() => void> {
459
- try {
460
- const ctx: LockContext = await acquireLockAsync(resourcePath, {
461
- lockSuffix,
462
- maxRetries: LOCK_MAX_RETRIES,
463
- baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
464
- lockStaleMs: LOCK_STALE_MS,
465
- });
466
- return () => releaseImportedLock(ctx);
467
- } catch (error: unknown) {
468
- const warn = logger?.warn;
469
- warn?.(`[PD:EvolutionWorker] Failed to acquire lock for ${resourcePath}: ${String(error)}`);
470
- throw error;
471
- }
472
- }
473
-
474
-
475
-
476
- async function requireQueueLock(resourcePath: string, logger: PluginLogger | { warn?: (message: string) => void; info?: (message: string) => void } | undefined, scope: string, lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX): Promise<() => void> {
477
- try {
478
- return await acquireQueueLock(resourcePath, logger, lockSuffix);
479
- } catch (err) {
480
- throw new LockUnavailableError(resourcePath, scope, { cause: err });
481
- }
482
- }
483
-
484
365
  export function extractEvolutionTaskId(task: string): string | null {
485
366
  if (!task) return null;
486
367
  const match = /\[ID:\s*([A-Za-z0-9_-]+)\]/.exec(task);
487
368
  return match?.[1] || null;
488
369
  }
489
370
 
490
-
491
-
492
- function findRecentDuplicateTask(
493
- queue: EvolutionQueueItem[],
494
- source: string,
495
- preview: string,
496
- now: number,
497
- reason?: string
498
- ): EvolutionQueueItem | undefined {
499
-
500
-
501
- const key = normalizePainDedupKey(source, preview, reason);
502
- return queue.find((task) => {
503
- if (task.status === 'completed') return false;
504
- const taskTime = new Date(task.enqueued_at || task.timestamp).getTime();
505
- if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS) return false;
506
-
507
-
508
- return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
509
- });
510
- }
511
-
512
371
  /**
513
372
  * Purge stale failed tasks from the queue.
514
373
  * Failed tasks older than the threshold are noise — they won't auto-recover
@@ -550,100 +409,6 @@ export function purgeStaleFailedTasks(
550
409
  return { purged: purged.length, remaining: queue.length, byReason };
551
410
  }
552
411
 
553
- function normalizePainDedupKey(source: string, preview: string, reason?: string): string {
554
- // Include reason in dedup key to match createEvolutionTaskId() behavior
555
- // Different reasons for the same source/preview should create different tasks
556
- const normalizedReason = (reason || '').trim().toLowerCase();
557
- return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
558
- }
559
-
560
-
561
-
562
- export function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number, reason?: string): boolean {
563
- return !!findRecentDuplicateTask(queue, source, preview, now, reason);
564
- }
565
-
566
- export function hasEquivalentPromotedRule(dictionary: { getAllRules(): Record<string, { type: string; phrases?: string[]; pattern?: string; status: string; }> }, phrase: string): boolean {
567
- const normalizedPhrase = phrase.trim().toLowerCase();
568
- return Object.values(dictionary.getAllRules()).some((rule) => {
569
- if (rule.status !== 'active') return false;
570
- if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
571
- return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
572
- }
573
- if (rule.type === 'regex' && typeof rule.pattern === 'string') {
574
- return rule.pattern.trim().toLowerCase() === normalizedPhrase;
575
- }
576
- return false;
577
- });
578
- }
579
-
580
- /**
581
- * Read recent pain context from PAIN_FLAG file.
582
- * Extracts session_id to link to trajectory DB.
583
- * Returns structured pain metadata for attaching to sleep_reflection tasks.
584
- * Returns null if no pain flag exists.
585
- */
586
- export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext {
587
- const contract = readPainFlagContract(wctx.workspaceDir);
588
- if (contract.status !== 'valid') {
589
- return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
590
- }
591
-
592
- try {
593
- const score = parseInt(contract.data.score ?? '0', 10) || 0;
594
- const source = contract.data.source ?? '';
595
- const reason = contract.data.reason ?? '';
596
- const timestamp = contract.data.time ?? '';
597
- const sessionId = contract.data.session_id ?? '';
598
-
599
- if (score > 0) {
600
- return {
601
- mostRecent: { score, source, reason, timestamp, sessionId },
602
- recentPainCount: 1,
603
- recentMaxPainScore: score,
604
- };
605
- }
606
- } catch {
607
- // Best effort — non-fatal
608
- }
609
-
610
- return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
611
- }
612
-
613
- /**
614
- * Build a dedup key from pain context.
615
- * Returns null when no pain context is available (bypasses dedup).
616
- */
617
- function buildPainSourceKey(
618
- painCtx: ReturnType<typeof readRecentPainContext>,
619
- ): string | null {
620
- if (!painCtx.mostRecent) return null;
621
- return `${painCtx.mostRecent.source}::${painCtx.mostRecent.reason?.slice(0, 50) ?? ''}`;
622
- }
623
-
624
- /**
625
- * Check whether a similar sleep_reflection task completed recently.
626
- * Phase 3c: Prevents redundant reflections of the same underlying issue.
627
- */
628
- function hasRecentSimilarReflection(
629
- queue: EvolutionQueueItem[],
630
- painSourceKey: string,
631
- now: number,
632
- ): EvolutionQueueItem | null {
633
- const DEDUP_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours
634
- return queue.find((t) => {
635
- if (t.taskKind !== 'sleep_reflection') return false;
636
- // Only match completed tasks (exclude failed to allow retries)
637
- if (t.status !== 'completed') return false;
638
- if (!t.completed_at) return false;
639
- const age = now - new Date(t.completed_at).getTime();
640
- if (age > DEDUP_WINDOW_MS) return false;
641
- const taskPainKey = buildPainSourceKey(t.recentPainContext ?? { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 });
642
- // If either side has no pain context, they don't match
643
- if (!taskPainKey) return false;
644
- return taskPainKey === painSourceKey;
645
- }) ?? null;
646
- }
647
412
 
648
413
  /**
649
414
  * Check whether a specific task kind has a pending or in-progress entry.
@@ -156,7 +156,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
156
156
  * Subclasses override to add type-specific fields.
157
157
  */
158
158
 
159
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
159
+
160
160
  protected createWorkflowMetadata<TResult>(
161
161
  spec: SubagentWorkflowSpec<TResult>,
162
162
  options: {
@@ -183,7 +183,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
183
183
  * Subclasses override to call store.createWorkflow() with type-specific metadata.
184
184
  */
185
185
 
186
- // eslint-disable-next-line @typescript-eslint/max-params
186
+
187
187
  protected async createWorkflowRecord<TResult>(
188
188
  workflowId: string,
189
189
  childSessionKey: string,
@@ -216,7 +216,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
216
216
  // ── Protected Helpers ────────────────────────────────────────────────────
217
217
 
218
218
 
219
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
219
+
220
220
  protected buildRunParams<TResult>(
221
221
  spec: SubagentWorkflowSpec<TResult>,
222
222
  options: {
@@ -316,7 +316,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
316
316
  error?: string
317
317
  ): Promise<void> {
318
318
 
319
- // eslint-disable-next-line @typescript-eslint/init-declarations
319
+
320
320
  let workflow;
321
321
  try {
322
322
  workflow = this.store.getWorkflow(workflowId);
@@ -528,7 +528,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
528
528
 
529
529
  // ── Private Helpers ───────────────────────────────────────────────────────
530
530
 
531
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
531
+
532
532
  protected generateWorkflowId(): string {
533
533
  // Subclasses override the prefix part via wf_ prefix pattern
534
534
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;