principles-disciple 1.36.0 → 1.37.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.36.0",
5
+ "version": "1.37.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.36.0",
3
+ "version": "1.37.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -333,6 +333,7 @@ Hardware tiers:
333
333
  proc.kill();
334
334
  reject(new Error(`Trainer timed out after ${timeoutMs}ms`));
335
335
  }, timeoutMs);
336
+ timer.unref(); // Don't keep process alive for timeout
336
337
 
337
338
  proc.on('close', (code) => {
338
339
  clearTimeout(timer);
@@ -295,6 +295,9 @@ export class EventLog {
295
295
 
296
296
  private startFlushTimer(): void {
297
297
  this.flushTimer = setInterval(() => this.flush(), this.flushIntervalMs);
298
+ // Don't keep the process alive just for this timer
299
+ // This allows tests and CLI to exit without waiting for flush
300
+ this.flushTimer.unref();
298
301
  }
299
302
 
300
303
  flush(): void {
@@ -469,6 +469,7 @@ export class EvolutionEngine {
469
469
  this.retryTimer = setTimeout(() => {
470
470
  this.processRetryQueue();
471
471
  }, 1000);
472
+ this.retryTimer.unref(); // Don't keep process alive for retry
472
473
  }
473
474
  }
474
475
 
@@ -5,6 +5,8 @@
5
5
  * Extracted to break circular dependency.
6
6
  */
7
7
 
8
+ import type { TrinityArtificerContext } from './nocturnal-artificer.js';
9
+
8
10
  // ---------------------------------------------------------------------------
9
11
  // Dreamer Types
10
12
  // ---------------------------------------------------------------------------
@@ -92,3 +94,125 @@ export interface PhilosopherOutput {
92
94
  /** Timestamp of generation */
93
95
  generatedAt: string;
94
96
  }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Trinity Result Types
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Tournament trace entry for explainability.
104
+ */
105
+ export interface TournamentTraceEntry {
106
+ candidateIndex: number;
107
+ reason: string;
108
+ }
109
+
110
+ /**
111
+ * Analysis of a rejected candidate — why it lost the tournament.
112
+ * Informs training signal for "what to avoid".
113
+ */
114
+ export interface RejectedAnalysis {
115
+ /** Mental model that led to the rejected candidate */
116
+ whyRejected: string;
117
+ /** Observable caution triggers that were missed or ignored */
118
+ warningSignals: string[];
119
+ /** Correct reasoning path that should have been seen */
120
+ correctiveThinking: string;
121
+ }
122
+
123
+ /**
124
+ * Justification for the chosen candidate — why it won the tournament.
125
+ * Informs training signal for "what to do".
126
+ */
127
+ export interface ChosenJustification {
128
+ /** Why this candidate was selected over others */
129
+ whyChosen: string;
130
+ /** 1-3 transferable insights from this decision */
131
+ keyInsights: string[];
132
+ /** When this approach does NOT apply */
133
+ limitations: string[];
134
+ }
135
+
136
+ /**
137
+ * Contrastive analysis: key differences between chosen and rejected paths.
138
+ * Synthesizes the core lesson from the tournament.
139
+ */
140
+ export interface ContrastiveAnalysis {
141
+ /** ONE key insight distinguishing chosen from rejected */
142
+ criticalDifference: string;
143
+ /** Pattern: "When X, do Y" */
144
+ decisionTrigger: string;
145
+ /** How to systematically avoid the rejected path */
146
+ preventionStrategy: string;
147
+ }
148
+
149
+ /**
150
+ * Telemetry about Trinity chain execution.
151
+ */
152
+ export interface TrinityTelemetry {
153
+ chainMode: 'trinity' | 'single-reflector';
154
+ usedStubs: boolean;
155
+ dreamerPassed: boolean;
156
+ philosopherPassed: boolean;
157
+ scribePassed: boolean;
158
+ candidateCount: number;
159
+ selectedCandidateIndex: number;
160
+ stageFailures: string[];
161
+ tournamentTrace?: TournamentTraceEntry[];
162
+ winnerAggregateScore?: number;
163
+ winnerThresholdPassed?: boolean;
164
+ eligibleCandidateCount?: number;
165
+ diversityCheckPassed?: boolean;
166
+ candidateRiskLevels?: string[];
167
+ philosopher6D?: {
168
+ avgScores: {
169
+ principleAlignment: number;
170
+ specificity: number;
171
+ actionability: number;
172
+ executability: number;
173
+ safetyImpact: number;
174
+ uxImpact: number;
175
+ };
176
+ highRiskCount: number;
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Validation failure for a Trinity stage.
182
+ */
183
+ export interface TrinityStageFailure {
184
+ stage: 'dreamer' | 'philosopher' | 'scribe';
185
+ reason: string;
186
+ }
187
+
188
+ /**
189
+ * Result of Trinity chain execution.
190
+ */
191
+ export interface TrinityResult {
192
+ success: boolean;
193
+ artifact?: TrinityDraftArtifact;
194
+ telemetry: TrinityTelemetry;
195
+ failures: TrinityStageFailure[];
196
+ fallbackOccurred: boolean;
197
+ artificerContext?: TrinityArtificerContext;
198
+ }
199
+
200
+ /**
201
+ * Scribe output — final structured artifact draft.
202
+ */
203
+ export interface TrinityDraftArtifact {
204
+ selectedCandidateIndex: number;
205
+ badDecision: string;
206
+ betterDecision: string;
207
+ rationale: string;
208
+ sessionId: string;
209
+ principleId: string;
210
+ sourceSnapshotRef: string;
211
+ telemetry: TrinityTelemetry;
212
+ thinkingModelDelta?: number;
213
+ planningRatioGain?: number;
214
+ artificerContext?: TrinityArtificerContext;
215
+ contrastiveAnalysis?: ContrastiveAnalysis;
216
+ rejectedAnalysis?: RejectedAnalysis;
217
+ chosenJustification?: ChosenJustification;
218
+ }
@@ -166,6 +166,7 @@ function schedulePersistence(state: SessionState): void {
166
166
  persistSession(state);
167
167
  persistTimers.delete(state.sessionId);
168
168
  }, 1000); // 1 second debounce
169
+ timer.unref(); // Don't keep process alive for persistence
169
170
  persistTimers.set(state.sessionId, timer);
170
171
  }
171
172
 
@@ -362,6 +362,7 @@ export async function executeTrainer(
362
362
  proc.kill();
363
363
  reject(new Error(`Trainer timed out after ${timeoutMs}ms`));
364
364
  }, timeoutMs);
365
+ timer.unref(); // Don't keep process alive for timeout
365
366
 
366
367
  proc.on('close', (code) => {
367
368
  clearTimeout(timer);
@@ -210,5 +210,5 @@ function scheduleTrajectoryGateBlockRetry(
210
210
  logWarn(`[PD_GATE] Retrying trajectory gate block persistence (attempt ${attempt + 1}): ${String(error)}`);
211
211
  scheduleTrajectoryGateBlockRetry(wctx, payload, attempt + 1, logWarn, logError);
212
212
  }
213
- }, TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS * attempt);
213
+ }, TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS * attempt).unref();
214
214
  }
package/src/index.ts CHANGED
@@ -87,7 +87,7 @@ const plugin = {
87
87
 
88
88
  // ── Startup Health Check: Verify workspaceDir resolution ──
89
89
  // Catches OpenClaw context bugs early (e.g., missing workspaceDir in tool hooks)
90
- setTimeout(() => {
90
+ const healthCheckTimer = setTimeout(() => {
91
91
  const testCtx = { agentId: 'main' };
92
92
  const toolWorkspaceDir = resolveToolHookWorkspaceDirSafe(testCtx, api, 'startup.health_check');
93
93
  const toolIssue = validateWorkspaceDir(toolWorkspaceDir);
@@ -98,6 +98,7 @@ const plugin = {
98
98
  api.logger.info(`[PD:health] Tool hook workspaceDir OK: "${toolWorkspaceDir}"`);
99
99
  }
100
100
  }, 1000);
101
+ healthCheckTimer.unref(); // Don't keep process alive for health check
101
102
 
102
103
  const language = (api.pluginConfig?.language as string) || 'en';
103
104
 
@@ -57,6 +57,8 @@ export const CentralSyncService: OpenClawPluginService = {
57
57
 
58
58
  // Schedule periodic sync
59
59
  syncInterval = setInterval(runSyncCycle, intervalMs);
60
+ // Don't keep the process alive just for this timer
61
+ syncInterval.unref();
60
62
 
61
63
  logger?.info?.(`[PD:CentralSync] Service started, syncing every ${intervalMs / 1000}s`);
62
64
  },
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Evolution Queue Deduplication Utilities
3
+ *
4
+ * Dedup logic for preventing duplicate pain tasks and redundant reflections.
5
+ * Extracted from evolution-worker.ts.
6
+ */
7
+
8
+ import type { EvolutionQueueItem } from './evolution-queue-migration.js';
9
+
10
+ /**
11
+ * Dedup window for pain queue tasks (30 minutes).
12
+ */
13
+ export const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
14
+
15
+ /**
16
+ * Maximum length for dedup key components to prevent memory/performance issues
17
+ * from extremely long source or preview strings during queue scanning.
18
+ */
19
+ const MAX_DEDUP_KEY_COMPONENT_LENGTH = 200;
20
+
21
+ function normalizePainDedupKey(source: string, preview: string, reason?: string): string {
22
+ const truncate = (s: string) => s.slice(0, MAX_DEDUP_KEY_COMPONENT_LENGTH);
23
+ const normalizedReason = (reason || '').trim().toLowerCase().slice(0, 50);
24
+ return `${truncate(source.trim().toLowerCase())}::${truncate(preview.trim().toLowerCase())}::${normalizedReason}`;
25
+ }
26
+
27
+ export function findRecentDuplicateTask(
28
+ queue: EvolutionQueueItem[],
29
+ source: string,
30
+ preview: string,
31
+ now: number,
32
+ reason?: string
33
+ ): EvolutionQueueItem | undefined {
34
+ const key = normalizePainDedupKey(source, preview, reason);
35
+ return queue.find((task) => {
36
+ if (task.status === 'completed') return false;
37
+ const taskTime = new Date(task.enqueued_at || task.timestamp).getTime();
38
+ if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS) return false;
39
+ return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Check if a similar pain task was enqueued recently.
45
+ */
46
+ export function hasRecentDuplicateTask(
47
+ queue: EvolutionQueueItem[],
48
+ source: string,
49
+ preview: string,
50
+ now: number,
51
+ reason?: string
52
+ ): boolean {
53
+ return !!findRecentDuplicateTask(queue, source, preview, now, reason);
54
+ }
55
+
56
+ /**
57
+ * Check if a phrase matches an active promoted rule.
58
+ */
59
+ export function hasEquivalentPromotedRule(
60
+ dictionary: { getAllRules(): Record<string, { type: string; phrases?: string[]; pattern?: string; status: string; }> },
61
+ phrase: string
62
+ ): boolean {
63
+ const normalizedPhrase = phrase.trim().toLowerCase();
64
+ return Object.values(dictionary.getAllRules()).some((rule) => {
65
+ if (rule.status !== 'active') return false;
66
+ if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
67
+ return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
68
+ }
69
+ if (rule.type === 'regex' && typeof rule.pattern === 'string') {
70
+ return rule.pattern.trim().toLowerCase() === normalizedPhrase;
71
+ }
72
+ return false;
73
+ });
74
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Evolution Pain Context Reader
3
+ *
4
+ * Reads and processes pain signal context for task enrichment.
5
+ * Extracted from evolution-worker.ts.
6
+ */
7
+
8
+ import type { WorkspaceContext } from '../core/workspace-context.js';
9
+ import { readPainFlagContract } from '../core/pain.js';
10
+ import type { EvolutionQueueItem } from './evolution-queue-migration.js';
11
+ import type { RecentPainContext } from './evolution-queue-migration.js';
12
+
13
+ /**
14
+ * Read recent pain context from PAIN_FLAG file.
15
+ * Extracts session_id to link to trajectory DB.
16
+ * Returns structured pain metadata for attaching to sleep_reflection tasks.
17
+ * Returns null if no pain flag exists.
18
+ */
19
+ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext {
20
+ const contract = readPainFlagContract(wctx.workspaceDir);
21
+ if (contract.status !== 'valid') {
22
+ return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
23
+ }
24
+
25
+ try {
26
+ const score = parseInt(contract.data.score ?? '0', 10) || 0;
27
+ const source = contract.data.source ?? '';
28
+ const reason = contract.data.reason ?? '';
29
+ const timestamp = contract.data.time ?? '';
30
+ const sessionId = contract.data.session_id ?? '';
31
+
32
+ if (score > 0) {
33
+ return {
34
+ mostRecent: { score, source, reason, timestamp, sessionId },
35
+ recentPainCount: 1,
36
+ recentMaxPainScore: score,
37
+ };
38
+ }
39
+ } catch {
40
+ // Best effort — non-fatal
41
+ }
42
+
43
+ return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
44
+ }
45
+
46
+ /**
47
+ * Build a dedup key from pain context.
48
+ * Returns null when no pain context is available (bypasses dedup).
49
+ */
50
+ export function buildPainSourceKey(
51
+ painCtx: ReturnType<typeof readRecentPainContext>,
52
+ ): string | null {
53
+ if (!painCtx.mostRecent) return null;
54
+ return `${painCtx.mostRecent.source}::${painCtx.mostRecent.reason?.slice(0, 50) ?? ''}`;
55
+ }
56
+
57
+ /**
58
+ * Check whether a similar sleep_reflection task completed recently.
59
+ * Phase 3c: Prevents redundant reflections of the same underlying issue.
60
+ */
61
+ export function hasRecentSimilarReflection(
62
+ queue: EvolutionQueueItem[],
63
+ painSourceKey: string,
64
+ now: number,
65
+ ): EvolutionQueueItem | null {
66
+ const DEDUP_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours
67
+ return queue.find((t) => {
68
+ if (t.taskKind !== 'sleep_reflection') return false;
69
+ // Only match completed tasks (exclude failed to allow retries)
70
+ if (t.status !== 'completed') return false;
71
+ if (!t.completed_at) return false;
72
+ const age = now - new Date(t.completed_at).getTime();
73
+ if (age > DEDUP_WINDOW_MS) return false;
74
+ const taskPainKey = buildPainSourceKey(t.recentPainContext ?? { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 });
75
+ // If either side has no pain context, they don't match
76
+ if (!taskPainKey) return false;
77
+ return taskPainKey === painSourceKey;
78
+ }) ?? null;
79
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Evolution Queue Lock Utilities
3
+ *
4
+ * File locking for safe concurrent queue access.
5
+ * Extracted from evolution-worker.ts.
6
+ */
7
+
8
+ import { acquireLockAsync, releaseLock as releaseImportedLock, type LockContext } from '../utils/file-lock.js';
9
+ import { LockUnavailableError } from '../config/index.js';
10
+
11
+ export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
12
+ export const LOCK_MAX_RETRIES = 50;
13
+ export const LOCK_RETRY_DELAY_MS = 50;
14
+ export const LOCK_STALE_MS = 30_000;
15
+
16
+ export async function acquireQueueLock(
17
+ resourcePath: string,
18
+ logger: { warn?: (message: string) => void; info?: (message: string) => void } | undefined,
19
+ lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX,
20
+ ): Promise<() => void> {
21
+ try {
22
+ const ctx: LockContext = await acquireLockAsync(resourcePath, {
23
+ lockSuffix,
24
+ maxRetries: LOCK_MAX_RETRIES,
25
+ baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
26
+ lockStaleMs: LOCK_STALE_MS,
27
+ });
28
+ return () => releaseImportedLock(ctx);
29
+ } catch (error: unknown) {
30
+ const warn = logger?.warn;
31
+ warn?.(`[PD:EvolutionWorker] Failed to acquire lock for ${resourcePath}: ${String(error)}`);
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ export async function requireQueueLock(
37
+ resourcePath: string,
38
+ logger: { warn?: (message: string) => void; info?: (message: string) => void } | undefined,
39
+ scope: string,
40
+ lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX,
41
+ ): Promise<() => void> {
42
+ try {
43
+ return await acquireQueueLock(resourcePath, logger, lockSuffix);
44
+ } catch (err) {
45
+ throw new LockUnavailableError(resourcePath, scope, { cause: err });
46
+ }
47
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Evolution Queue Migration — V1 to V2 Schema
3
+ *
4
+ * Pure transformation functions and shared queue types.
5
+ */
6
+
7
+ import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
8
+
9
+ // Re-export TaskKind and TaskPriority for convenience
10
+ export type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
11
+
12
+ /**
13
+ * Queue item status values.
14
+ */
15
+ export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
16
+
17
+ /**
18
+ * Task resolution strings.
19
+ */
20
+ export type TaskResolution =
21
+ | 'marker_detected'
22
+ | 'auto_completed_timeout'
23
+ | 'failed_max_retries'
24
+ | 'runtime_unavailable'
25
+ | 'canceled'
26
+ | 'late_marker_principle_created'
27
+ | 'late_marker_no_principle'
28
+ | 'stub_fallback'
29
+ | 'skipped_thin_violation';
30
+
31
+ /**
32
+ * Recent pain context for sleep_reflection tasks.
33
+ * Attached to queue items to provide pain signal context.
34
+ */
35
+ export interface RecentPainContext {
36
+ mostRecent: { score: number; source: string; reason: string; timestamp: string; sessionId: string } | null;
37
+ recentPainCount: number;
38
+ recentMaxPainScore: number;
39
+ }
40
+
41
+ /**
42
+ * Default values for new V2 fields when migrating legacy items.
43
+ */
44
+ export const DEFAULT_TASK_KIND: TaskKind = 'pain_diagnosis';
45
+ export const DEFAULT_PRIORITY: TaskPriority = 'medium';
46
+ export const DEFAULT_MAX_RETRIES = 3;
47
+
48
+ /**
49
+ * Legacy (pre-V2) queue item schema.
50
+ */
51
+ export interface LegacyEvolutionQueueItem {
52
+ id: string;
53
+ source: string;
54
+ traceId?: string;
55
+ task?: string;
56
+ score: number;
57
+ reason: string;
58
+ timestamp: string;
59
+ enqueued_at?: string;
60
+ started_at?: string;
61
+ completed_at?: string;
62
+ assigned_session_key?: string;
63
+ trigger_text_preview?: string;
64
+ status?: string;
65
+ resolution?: string;
66
+ session_id?: string;
67
+ agent_id?: string;
68
+ taskKind?: string;
69
+ priority?: string;
70
+ retryCount?: number;
71
+ maxRetries?: number;
72
+ lastError?: string;
73
+ resultRef?: string;
74
+ }
75
+
76
+ /**
77
+ * V2 queue item schema.
78
+ */
79
+ export interface EvolutionQueueItem {
80
+ id: string;
81
+ taskKind: TaskKind;
82
+ priority: TaskPriority;
83
+ source: string;
84
+ traceId?: string;
85
+ task?: string;
86
+ score: number;
87
+ reason: string;
88
+ timestamp: string;
89
+ enqueued_at?: string;
90
+ started_at?: string;
91
+ completed_at?: string;
92
+ assigned_session_key?: string;
93
+ trigger_text_preview?: string;
94
+ status: QueueStatus;
95
+ resolution?: TaskResolution;
96
+ session_id?: string;
97
+ agent_id?: string;
98
+ retryCount: number;
99
+ maxRetries: number;
100
+ lastError?: string;
101
+ resultRef?: string;
102
+ recentPainContext?: RecentPainContext;
103
+ }
104
+
105
+ export type RawQueueItem = Record<string, unknown>;
106
+
107
+ /**
108
+ * Migrate a legacy queue item to V2 schema.
109
+ * Old items without taskKind are assumed to be pain_diagnosis for backward compatibility.
110
+ */
111
+ export function migrateToV2(item: LegacyEvolutionQueueItem): {
112
+ id: string;
113
+ taskKind: TaskKind;
114
+ priority: TaskPriority;
115
+ source: string;
116
+ traceId?: string;
117
+ task?: string;
118
+ score: number;
119
+ reason: string;
120
+ timestamp: string;
121
+ enqueued_at?: string;
122
+ started_at?: string;
123
+ completed_at?: string;
124
+ assigned_session_key?: string;
125
+ trigger_text_preview?: string;
126
+ status: QueueStatus;
127
+ resolution?: TaskResolution;
128
+ session_id?: string;
129
+ agent_id?: string;
130
+ retryCount: number;
131
+ maxRetries: number;
132
+ lastError?: string;
133
+ resultRef?: string;
134
+ } {
135
+ return {
136
+ id: item.id,
137
+ taskKind: (item.taskKind as TaskKind) || DEFAULT_TASK_KIND,
138
+ priority: (item.priority as TaskPriority) || DEFAULT_PRIORITY,
139
+ source: item.source,
140
+ traceId: item.traceId,
141
+ task: item.task,
142
+ score: item.score,
143
+ reason: item.reason,
144
+ timestamp: item.timestamp,
145
+ enqueued_at: item.enqueued_at,
146
+ started_at: item.started_at,
147
+ completed_at: item.completed_at,
148
+ assigned_session_key: item.assigned_session_key,
149
+ trigger_text_preview: item.trigger_text_preview,
150
+ status: (item.status as QueueStatus) || 'pending',
151
+ resolution: item.resolution as TaskResolution | undefined,
152
+ session_id: item.session_id,
153
+ agent_id: item.agent_id,
154
+ retryCount: item.retryCount || 0,
155
+ maxRetries: item.maxRetries || DEFAULT_MAX_RETRIES,
156
+ lastError: item.lastError,
157
+ resultRef: item.resultRef,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Check if an item is a legacy (pre-V2) queue item.
163
+ */
164
+ export function isLegacyQueueItem(item: RawQueueItem): boolean {
165
+ return item && typeof item === 'object' && !('taskKind' in item);
166
+ }
167
+
168
+ /**
169
+ * Migrate entire queue to V2 schema if needed.
170
+ */
171
+ export function migrateQueueToV2(queue: RawQueueItem[]): ReturnType<typeof migrateToV2>[] {
172
+ return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item as unknown as LegacyEvolutionQueueItem) : item as unknown as ReturnType<typeof migrateToV2>);
173
+ }
@@ -2563,6 +2563,7 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
2563
2563
  }
2564
2564
 
2565
2565
  timeoutId = setTimeout(runCycle, interval);
2566
+ timeoutId.unref();
2566
2567
  }
2567
2568
 
2568
2569
  timeoutId = setTimeout(() => {
@@ -2578,11 +2579,14 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
2578
2579
  }
2579
2580
  // processPromotion removed (D-06)
2580
2581
  timeoutId = setTimeout(runCycle, interval);
2582
+ timeoutId.unref();
2581
2583
  })().catch((err) => {
2582
2584
  if (logger) logger.error(`[PD:EvolutionWorker] Startup worker cycle failed: ${String(err)}`);
2583
2585
  timeoutId = setTimeout(runCycle, interval);
2586
+ timeoutId.unref();
2584
2587
  });
2585
2588
  }, initialDelay);
2589
+ timeoutId.unref();
2586
2590
  },
2587
2591
 
2588
2592
  stop(ctx: OpenClawPluginServiceContext): void {
@@ -303,6 +303,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
303
303
  await this.notifyWaitResult(workflowId, 'error', errMsg);
304
304
  }
305
305
  }, 100);
306
+ timeout.unref(); // Don't keep process alive for wait poll
306
307
 
307
308
  this.activeWorkflows.set(workflowId, timeout);
308
309
  }