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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/nocturnal-train.ts +1 -0
- package/src/core/event-log.ts +3 -0
- package/src/core/evolution-engine.ts +1 -0
- package/src/core/nocturnal-trinity-types.ts +124 -0
- package/src/core/session-tracker.ts +1 -0
- package/src/core/training-program.ts +1 -0
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/index.ts +2 -1
- package/src/service/central-sync-service.ts +2 -0
- package/src/service/evolution-dedup.ts +74 -0
- package/src/service/evolution-pain-context.ts +79 -0
- package/src/service/evolution-queue-lock.ts +47 -0
- package/src/service/evolution-queue-migration.ts +173 -0
- package/src/service/evolution-worker.ts +4 -0
- package/src/service/subagent-workflow/workflow-manager-base.ts +1 -0
- package/tests/core/pain-score.property.test.ts +205 -0
- package/tests/integration/chaos-resilience.test.ts +348 -0
- package/tests/integration/gate-real-io.e2e.test.ts +251 -0
- package/tests/integration/pain-diagnostician-loop.e2e.test.ts +380 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +8 -2
- package/tests/integration/trajectory-lifecycle.e2e.test.ts +523 -0
- package/vitest.config.ts +23 -4
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/core/event-log.ts
CHANGED
|
@@ -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 {
|
|
@@ -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
|
}
|