principles-disciple 1.33.0 → 1.34.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.33.0",
5
+ "version": "1.34.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "buildFingerprint": {
79
- "gitSha": "15c19a4dc3f2",
80
- "bundleMd5": "684e47fd5c521d722150a93813fddd02",
81
- "builtAt": "2026-04-14T03:01:11.396Z"
79
+ "gitSha": "cab0dbd8e6e7",
80
+ "bundleMd5": "1505a7119addd2ee24059f2473cdb1ca",
81
+ "builtAt": "2026-04-14T10:58:07.896Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -260,16 +260,16 @@ export class EvolutionLogger {
260
260
  logCompleted(params: {
261
261
  traceId: string;
262
262
  taskId: string;
263
- resolution: 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle';
263
+ resolution: 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'diagnostician_timeout';
264
264
  durationMs?: number;
265
265
  principlesGenerated?: number;
266
266
  }): void {
267
267
 
268
- // eslint-disable-next-line @typescript-eslint/init-declarations
268
+
269
269
  let summary: string;
270
270
  if (params.resolution === 'marker_detected' || params.resolution === 'late_marker_principle_created') {
271
271
  summary = `任务 ${params.taskId} 完成,已生成 ${params.principlesGenerated || 0} 条原则`;
272
- } else if (params.resolution === 'auto_completed_timeout' || params.resolution === 'late_marker_no_principle') {
272
+ } else if (params.resolution === 'auto_completed_timeout' || params.resolution === 'diagnostician_timeout' || params.resolution === 'late_marker_no_principle') {
273
273
  summary = `任务 ${params.taskId} 超时自动完成`;
274
274
  } else {
275
275
  summary = `任务 ${params.taskId} 已完成`;
@@ -56,7 +56,7 @@ interface WatchdogResult {
56
56
  details: string[];
57
57
  }
58
58
 
59
- // eslint-disable-next-line complexity
59
+
60
60
  async function runWorkflowWatchdog(
61
61
  wctx: WorkspaceContext,
62
62
  api: OpenClawPluginApi | null,
@@ -98,7 +98,7 @@ async function runWorkflowWatchdog(
98
98
 
99
99
  // ── Watchdog helpers (extracted from runWorkflowWatchdog for complexity) ──
100
100
 
101
- // eslint-disable-next-line complexity
101
+
102
102
  async function cleanupStaleWorkflowSession(
103
103
  wf: WorkflowRow,
104
104
  subagentRuntime: { deleteSession: (opts: { sessionKey: string; deleteTranscript: boolean }) => Promise<void> } | undefined,
@@ -174,7 +174,7 @@ function runWorkflowWatchdogCheckUncleared(allWorkflows: WorkflowRow[], details:
174
174
  }
175
175
  }
176
176
 
177
- // eslint-disable-next-line complexity
177
+
178
178
  function runWorkflowWatchdogCheckNocturnal(allWorkflows: WorkflowRow[], details: string[]): void {
179
179
  for (const wf of allWorkflows) {
180
180
  if (wf.workflow_type !== 'nocturnal' || wf.state !== 'completed') continue;
@@ -208,7 +208,12 @@ let timeoutId: NodeJS.Timeout | null = null;
208
208
  * Old queue items (without taskKind) are migrated to pain_diagnosis for compatibility.
209
209
  */
210
210
  export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
211
- export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation';
211
+ export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'diagnostician_timeout';
212
+
213
+ /** Timeout for pain_diagnosis tasks (30 min) — separate from sleep_reflection timeout.
214
+ * Pain diagnostics run via HEARTBEAT (main session LLM), not as a subagent.
215
+ * If the agent is persistently busy, we don't want the task to starve indefinitely. */
216
+ const PAIN_DIAGNOSIS_TIMEOUT_MS = 30 * 60 * 1000;
212
217
 
213
218
  /**
214
219
  * Recent pain context attached to sleep_reflection tasks.
@@ -370,7 +375,7 @@ function isSessionAtOrBeforeTriggerTime(
370
375
  return true;
371
376
  }
372
377
 
373
- // eslint-disable-next-line complexity
378
+
374
379
  function buildFallbackNocturnalSnapshot(
375
380
  sleepTask: EvolutionQueueItem,
376
381
  extractor?: ReturnType<typeof createNocturnalTrajectoryExtractor> | null,
@@ -782,7 +787,7 @@ interface ParsedPainValues {
782
787
 
783
788
 
784
789
 
785
- // eslint-disable-next-line complexity
790
+
786
791
  async function doEnqueuePainTask(
787
792
  wctx: WorkspaceContext, logger: PluginLogger, painFlagPath: string,
788
793
  result: WorkerStatusReport['pain_flag'], v: ParsedPainValues,
@@ -856,7 +861,7 @@ async function doEnqueuePainTask(
856
861
  return result;
857
862
  }
858
863
 
859
- // eslint-disable-next-line complexity
864
+
860
865
  async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Promise<WorkerStatusReport['pain_flag']> {
861
866
  const result: WorkerStatusReport['pain_flag'] = { exists: false, score: null, source: null, enqueued: false, skipped_reason: null };
862
867
  try {
@@ -1031,7 +1036,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
1031
1036
 
1032
1037
 
1033
1038
 
1034
- // eslint-disable-next-line complexity
1039
+
1035
1040
  async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogger, eventLog: EventLog, api?: OpenClawPluginApi) {
1036
1041
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
1037
1042
  if (!fs.existsSync(queuePath)) {
@@ -1309,8 +1314,8 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1309
1314
  }
1310
1315
 
1311
1316
  const age = Date.now() - startedAt.getTime();
1312
- if (age > timeout) {
1313
- const timeoutMinutes = Math.round(timeout / 60000);
1317
+ if (age > PAIN_DIAGNOSIS_TIMEOUT_MS) {
1318
+ const timeoutMinutes = Math.round(PAIN_DIAGNOSIS_TIMEOUT_MS / 60000);
1314
1319
 
1315
1320
  const timeoutCompleteMarker = path.join(wctx.stateDir, `.evolution_complete_${task.id}`);
1316
1321
  const timeoutReportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
@@ -1358,13 +1363,13 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1358
1363
  } catch { /* report may not exist, not critical */ }
1359
1364
  task.resolution = principleCreated ? 'late_marker_principle_created' : 'late_marker_no_principle';
1360
1365
  } else {
1361
- if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout`);
1366
+ if (logger) logger.info(`[PD:EvolutionWorker] Pain diagnosis task ${task.id} timed out after ${timeoutMinutes} minutes`);
1362
1367
  // #190: Clean up diagnostician report file even on timeout (may have been written late)
1363
1368
  try {
1364
1369
  const autoTimeoutReportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
1365
1370
  if (fs.existsSync(autoTimeoutReportPath)) fs.unlinkSync(autoTimeoutReportPath);
1366
1371
  } catch { /* report may not exist, not critical */ }
1367
- task.resolution = 'auto_completed_timeout';
1372
+ task.resolution = 'diagnostician_timeout';
1368
1373
  }
1369
1374
 
1370
1375
  // Critical: mark task as completed so it doesn't get re-processed
@@ -1390,7 +1395,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1390
1395
  sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
1391
1396
  taskId: task.id,
1392
1397
  outcome: 'timeout',
1393
- summary: `Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout.`
1398
+ summary: `Pain diagnosis task ${task.id} timed out after ${timeoutMinutes} minutes.`
1394
1399
  });
1395
1400
  queueChanged = true;
1396
1401
  }
@@ -1925,7 +1930,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1925
1930
  }
1926
1931
 
1927
1932
 
1928
- // eslint-disable-next-line complexity
1933
+
1929
1934
  async function processDetectionQueue(wctx: WorkspaceContext, api: OpenClawPluginApi, eventLog: EventLog) {
1930
1935
  const {logger} = api;
1931
1936
  try {
@@ -2113,7 +2118,7 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
2113
2118
  api: null,
2114
2119
  _startedWorkspaces: new Set<string>(),
2115
2120
 
2116
- // eslint-disable-next-line complexity
2121
+
2117
2122
  start(ctx: OpenClawPluginServiceContext): void {
2118
2123
  const workspaceDir = ctx?.workspaceDir;
2119
2124
  const logger = ctx?.logger || console;
@@ -2150,7 +2155,7 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
2150
2155
  // Periodic trigger tracking
2151
2156
  let heartbeatCounter = 0;
2152
2157
 
2153
- // eslint-disable-next-line complexity
2158
+
2154
2159
  async function runCycle(): Promise<void> {
2155
2160
  const cycleStart = Date.now();
2156
2161
  heartbeatCounter++;
@@ -0,0 +1,350 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+
6
+ vi.mock('../../src/core/dictionary-service.js', () => ({
7
+ DictionaryService: {
8
+ get: vi.fn(() => ({ flush: vi.fn() })),
9
+ },
10
+ }));
11
+
12
+ vi.mock('../../src/core/session-tracker.js', () => ({
13
+ initPersistence: vi.fn(),
14
+ flushAllSessions: vi.fn(),
15
+ listSessions: vi.fn(() => []),
16
+ }));
17
+
18
+ const { mockStartWorkflow, mockGetWorkflowDebugSummary } = vi.hoisted(() => ({
19
+ mockStartWorkflow: vi.fn(),
20
+ mockGetWorkflowDebugSummary: vi.fn(),
21
+ }));
22
+
23
+ vi.mock('../../src/service/subagent-workflow/nocturnal-workflow-manager.js', () => ({
24
+ NocturnalWorkflowManager: class {
25
+ startWorkflow = mockStartWorkflow;
26
+ getWorkflowDebugSummary = mockGetWorkflowDebugSummary;
27
+ },
28
+ nocturnalWorkflowSpec: {
29
+ workflowType: 'nocturnal',
30
+ transport: 'runtime_direct',
31
+ timeoutMs: 15 * 60 * 1000,
32
+ ttlMs: 30 * 60 * 1000,
33
+ },
34
+ }));
35
+
36
+ const { mockGetNocturnalSessionSnapshot, mockListRecentNocturnalCandidateSessions } = vi.hoisted(() => ({
37
+ mockGetNocturnalSessionSnapshot: vi.fn(),
38
+ mockListRecentNocturnalCandidateSessions: vi.fn(() => []),
39
+ }));
40
+
41
+ vi.mock('../../src/core/nocturnal-trajectory-extractor.js', async () => {
42
+ const actual = await vi.importActual<typeof import('../../src/core/nocturnal-trajectory-extractor.js')>(
43
+ '../../src/core/nocturnal-trajectory-extractor.js'
44
+ );
45
+ return {
46
+ ...actual,
47
+ createNocturnalTrajectoryExtractor: vi.fn(() => ({
48
+ getNocturnalSessionSnapshot: mockGetNocturnalSessionSnapshot,
49
+ listRecentNocturnalCandidateSessions: mockListRecentNocturnalCandidateSessions,
50
+ })),
51
+ };
52
+ });
53
+
54
+ import { EvolutionWorkerService } from '../../src/service/evolution-worker.js';
55
+ import { safeRmDir } from '../test-utils.js';
56
+
57
+ function createMockApi() {
58
+ return {
59
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
60
+ runtime: {
61
+ agent: { runEmbeddedPiAgent: vi.fn() },
62
+ system: {
63
+ requestHeartbeatNow: vi.fn(),
64
+ runHeartbeatOnce: vi.fn(),
65
+ },
66
+ },
67
+ } as any;
68
+ }
69
+
70
+ // Poll every 100ms for fast test execution
71
+ const fastPollConfig = { get: (k: string) => k === 'intervals.worker_poll_ms' ? 100 : undefined };
72
+
73
+ function readQueue(stateDir: string) {
74
+ return JSON.parse(fs.readFileSync(path.join(stateDir, 'evolution_queue.json'), 'utf8'));
75
+ }
76
+
77
+ describe('EvolutionWorkerService timeout mechanisms', () => {
78
+ beforeEach(() => {
79
+ vi.useFakeTimers();
80
+ vi.clearAllMocks();
81
+ EvolutionWorkerService.api = null;
82
+ });
83
+
84
+ afterEach(() => {
85
+ vi.useRealTimers();
86
+ EvolutionWorkerService.api = null;
87
+ });
88
+
89
+ // ── Pain diagnosis timeout (30 min) ──
90
+
91
+ it('times out pain_diagnosis task after 30 minutes → resolution = diagnostician_timeout', async () => {
92
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-timeout-pain-'));
93
+ const stateDir = path.join(workspaceDir, '.state');
94
+ fs.mkdirSync(stateDir, { recursive: true });
95
+
96
+ // Create an in_progress pain_diagnosis task that started 31 minutes ago
97
+ const startedAt = new Date(Date.now() - 31 * 60 * 1000).toISOString();
98
+ fs.writeFileSync(
99
+ path.join(stateDir, 'evolution_queue.json'),
100
+ JSON.stringify([
101
+ {
102
+ id: 'timeout-test-30min',
103
+ taskKind: 'pain_diagnosis',
104
+ priority: 'high',
105
+ score: 90,
106
+ source: 'tool_failure',
107
+ reason: 'Test timeout mechanism',
108
+ timestamp: startedAt,
109
+ enqueued_at: startedAt,
110
+ status: 'in_progress',
111
+ session_id: 'test',
112
+ agent_id: 'main',
113
+ started_at: startedAt,
114
+ assigned_session_key: 'heartbeat:diagnostician:timeout-test-30min',
115
+ retryCount: 0,
116
+ maxRetries: 3,
117
+ task: 'Diagnose systemic pain [ID: timeout-test-30min]',
118
+ },
119
+ ], null, 2),
120
+ 'utf8'
121
+ );
122
+
123
+ const mockApi = createMockApi();
124
+ EvolutionWorkerService.api = mockApi;
125
+
126
+ try {
127
+ EvolutionWorkerService.start({
128
+ workspaceDir,
129
+ stateDir,
130
+ logger: mockApi.logger,
131
+ config: fastPollConfig,
132
+ api: mockApi,
133
+ } as any);
134
+
135
+ // Wait for the worker to process
136
+ await vi.advanceTimersByTimeAsync(5000);
137
+
138
+ const queue = readQueue(stateDir);
139
+ const task = queue.find((t: any) => t.id === 'timeout-test-30min');
140
+
141
+ expect(task.status).toBe('completed');
142
+ expect(task.resolution).toBe('diagnostician_timeout');
143
+ expect(task.completed_at).toBeDefined();
144
+ } finally {
145
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
146
+ safeRmDir(workspaceDir);
147
+ }
148
+ });
149
+
150
+ it('does not timeout pain_diagnosis task under 30 minutes', async () => {
151
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-no-timeout-'));
152
+ const stateDir = path.join(workspaceDir, '.state');
153
+ fs.mkdirSync(stateDir, { recursive: true });
154
+
155
+ // Create an in_progress pain_diagnosis task that started 10 minutes ago
156
+ const startedAt = new Date(Date.now() - 10 * 60 * 1000).toISOString();
157
+ fs.writeFileSync(
158
+ path.join(stateDir, 'evolution_queue.json'),
159
+ JSON.stringify([
160
+ {
161
+ id: 'no-timeout-10min',
162
+ taskKind: 'pain_diagnosis',
163
+ priority: 'high',
164
+ score: 80,
165
+ source: 'human_intervention',
166
+ reason: 'Should not timeout yet',
167
+ timestamp: startedAt,
168
+ enqueued_at: startedAt,
169
+ status: 'in_progress',
170
+ session_id: 'test',
171
+ agent_id: 'main',
172
+ started_at: startedAt,
173
+ assigned_session_key: 'heartbeat:diagnostician:no-timeout-10min',
174
+ retryCount: 0,
175
+ maxRetries: 3,
176
+ task: 'Diagnose systemic pain [ID: no-timeout-10min]',
177
+ },
178
+ ], null, 2),
179
+ 'utf8'
180
+ );
181
+
182
+ const mockApi = createMockApi();
183
+ EvolutionWorkerService.api = mockApi;
184
+
185
+ try {
186
+ EvolutionWorkerService.start({
187
+ workspaceDir,
188
+ stateDir,
189
+ logger: mockApi.logger,
190
+ config: fastPollConfig,
191
+ api: mockApi,
192
+ } as any);
193
+
194
+ await vi.advanceTimersByTimeAsync(5000);
195
+
196
+ const queue = readQueue(stateDir);
197
+ const task = queue.find((t: any) => t.id === 'no-timeout-10min');
198
+
199
+ // Task should still be in_progress — not yet timed out
200
+ expect(task.status).toBe('in_progress');
201
+ expect(task.resolution).toBeUndefined();
202
+ } finally {
203
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
204
+ safeRmDir(workspaceDir);
205
+ }
206
+ });
207
+
208
+ // ── Sleep reflection timeout (60 min default) ──
209
+
210
+ it('times out sleep_reflection task after 60 minutes → resolution = failed_max_retries', async () => {
211
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-timeout-sleep-'));
212
+ const stateDir = path.join(workspaceDir, '.state');
213
+ fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
214
+ fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
215
+
216
+ // Create an in_progress sleep_reflection task that started 61 minutes ago
217
+ const startedAt = new Date(Date.now() - 61 * 60 * 1000).toISOString();
218
+ fs.writeFileSync(
219
+ path.join(stateDir, 'evolution_queue.json'),
220
+ JSON.stringify([
221
+ {
222
+ id: 'sleep-timeout-60min',
223
+ taskKind: 'sleep_reflection',
224
+ priority: 'medium',
225
+ score: 50,
226
+ source: 'nocturnal',
227
+ reason: 'Test sleep reflection timeout',
228
+ timestamp: startedAt,
229
+ enqueued_at: startedAt,
230
+ status: 'in_progress',
231
+ session_id: 'test',
232
+ agent_id: 'main',
233
+ started_at: startedAt,
234
+ resultRef: 'wf-sleep-timeout',
235
+ retryCount: 0,
236
+ maxRetries: 1,
237
+ recentPainContext: {
238
+ mostRecent: null,
239
+ recentPainCount: 0,
240
+ recentMaxPainScore: 0,
241
+ },
242
+ },
243
+ ], null, 2),
244
+ 'utf8'
245
+ );
246
+
247
+ mockStartWorkflow.mockResolvedValue({
248
+ workflowId: 'wf-sleep-timeout',
249
+ childSessionKey: 'child-sleep',
250
+ state: 'terminal_error',
251
+ });
252
+ mockGetWorkflowDebugSummary.mockResolvedValue({
253
+ state: 'terminal_error',
254
+ metadata: {},
255
+ recentEvents: [{ reason: 'Test: simulating stuck sleep reflection', payload: {} }],
256
+ });
257
+
258
+ const mockApi = createMockApi();
259
+ EvolutionWorkerService.api = mockApi;
260
+
261
+ try {
262
+ EvolutionWorkerService.start({
263
+ workspaceDir,
264
+ stateDir,
265
+ logger: mockApi.logger,
266
+ config: fastPollConfig,
267
+ api: mockApi,
268
+ } as any);
269
+
270
+ await vi.advanceTimersByTimeAsync(5000);
271
+
272
+ const queue = readQueue(stateDir);
273
+ const task = queue.find((t: any) => t.id === 'sleep-timeout-60min');
274
+
275
+ expect(task.status).toBe('failed');
276
+ expect(task.resolution).toBe('failed_max_retries');
277
+ expect(task.completed_at).toBeDefined();
278
+ } finally {
279
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
280
+ safeRmDir(workspaceDir);
281
+ }
282
+ });
283
+
284
+ // ── Report file cleanup on timeout ──
285
+
286
+ it('cleans up .diagnostician_report_*.json file on pain_diagnosis timeout', async () => {
287
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-timeout-cleanup-'));
288
+ const stateDir = path.join(workspaceDir, '.state');
289
+ fs.mkdirSync(stateDir, { recursive: true });
290
+
291
+ // Create a stale diagnostician report file
292
+ const reportPath = path.join(stateDir, '.diagnostician_report_timeout-cleanup.json');
293
+ fs.writeFileSync(reportPath, JSON.stringify({ test: 'stale report' }), 'utf8');
294
+ expect(fs.existsSync(reportPath)).toBe(true);
295
+
296
+ // Create an in_progress pain_diagnosis task that started 31 minutes ago
297
+ const startedAt = new Date(Date.now() - 31 * 60 * 1000).toISOString();
298
+ fs.writeFileSync(
299
+ path.join(stateDir, 'evolution_queue.json'),
300
+ JSON.stringify([
301
+ {
302
+ id: 'timeout-cleanup',
303
+ taskKind: 'pain_diagnosis',
304
+ priority: 'high',
305
+ score: 70,
306
+ source: 'tool_failure',
307
+ reason: 'Test report cleanup on timeout',
308
+ timestamp: startedAt,
309
+ enqueued_at: startedAt,
310
+ status: 'in_progress',
311
+ session_id: 'test',
312
+ agent_id: 'main',
313
+ started_at: startedAt,
314
+ assigned_session_key: 'heartbeat:diagnostician:timeout-cleanup',
315
+ retryCount: 0,
316
+ maxRetries: 3,
317
+ task: 'Diagnose systemic pain [ID: timeout-cleanup]',
318
+ },
319
+ ], null, 2),
320
+ 'utf8'
321
+ );
322
+
323
+ const mockApi = createMockApi();
324
+ EvolutionWorkerService.api = mockApi;
325
+
326
+ try {
327
+ EvolutionWorkerService.start({
328
+ workspaceDir,
329
+ stateDir,
330
+ logger: mockApi.logger,
331
+ config: fastPollConfig,
332
+ api: mockApi,
333
+ } as any);
334
+
335
+ await vi.advanceTimersByTimeAsync(5000);
336
+
337
+ // Verify the report file was cleaned up
338
+ expect(fs.existsSync(reportPath)).toBe(false);
339
+
340
+ // Verify the task was marked as completed with timeout resolution
341
+ const queue = readQueue(stateDir);
342
+ const task = queue.find((t: any) => t.id === 'timeout-cleanup');
343
+ expect(task.status).toBe('completed');
344
+ expect(task.resolution).toBe('diagnostician_timeout');
345
+ } finally {
346
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
347
+ safeRmDir(workspaceDir);
348
+ }
349
+ });
350
+ });