principles-disciple 1.7.5 → 1.7.8
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/dist/commands/context.js +5 -15
- package/dist/commands/evolution-status.js +29 -48
- package/dist/commands/export.js +61 -8
- package/dist/commands/nocturnal-review.d.ts +24 -0
- package/dist/commands/nocturnal-review.js +265 -0
- package/dist/commands/nocturnal-rollout.d.ts +27 -0
- package/dist/commands/nocturnal-rollout.js +671 -0
- package/dist/commands/nocturnal-train.d.ts +25 -0
- package/dist/commands/nocturnal-train.js +919 -0
- package/dist/commands/pain.js +8 -21
- package/dist/config/defaults/runtime.d.ts +40 -0
- package/dist/config/defaults/runtime.js +44 -0
- package/dist/config/errors.d.ts +84 -0
- package/dist/config/errors.js +94 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/constants/diagnostician.d.ts +0 -4
- package/dist/constants/diagnostician.js +0 -4
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/adaptive-thresholds.d.ts +186 -0
- package/dist/core/adaptive-thresholds.js +300 -0
- package/dist/core/config.d.ts +2 -38
- package/dist/core/config.js +6 -61
- package/dist/core/control-ui-db.d.ts +27 -0
- package/dist/core/control-ui-db.js +18 -0
- package/dist/core/event-log.d.ts +1 -2
- package/dist/core/event-log.js +0 -3
- package/dist/core/evolution-engine.js +1 -21
- package/dist/core/evolution-reducer.d.ts +7 -1
- package/dist/core/evolution-reducer.js +56 -4
- package/dist/core/evolution-types.d.ts +61 -9
- package/dist/core/evolution-types.js +31 -9
- package/dist/core/external-training-contract.d.ts +276 -0
- package/dist/core/external-training-contract.js +269 -0
- package/dist/core/local-worker-routing.d.ts +175 -0
- package/dist/core/local-worker-routing.js +525 -0
- package/dist/core/model-deployment-registry.d.ts +218 -0
- package/dist/core/model-deployment-registry.js +503 -0
- package/dist/core/model-training-registry.d.ts +295 -0
- package/dist/core/model-training-registry.js +475 -0
- package/dist/core/nocturnal-arbiter.d.ts +159 -0
- package/dist/core/nocturnal-arbiter.js +534 -0
- package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
- package/dist/core/nocturnal-candidate-scoring.js +266 -0
- package/dist/core/nocturnal-compliance.d.ts +175 -0
- package/dist/core/nocturnal-compliance.js +824 -0
- package/dist/core/nocturnal-dataset.d.ts +224 -0
- package/dist/core/nocturnal-dataset.js +443 -0
- package/dist/core/nocturnal-executability.d.ts +85 -0
- package/dist/core/nocturnal-executability.js +331 -0
- package/dist/core/nocturnal-export.d.ts +124 -0
- package/dist/core/nocturnal-export.js +275 -0
- package/dist/core/nocturnal-paths.d.ts +124 -0
- package/dist/core/nocturnal-paths.js +214 -0
- package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
- package/dist/core/nocturnal-trajectory-extractor.js +307 -0
- package/dist/core/nocturnal-trinity.d.ts +311 -0
- package/dist/core/nocturnal-trinity.js +880 -0
- package/dist/core/path-resolver.js +2 -1
- package/dist/core/paths.d.ts +6 -0
- package/dist/core/paths.js +6 -0
- package/dist/core/principle-training-state.d.ts +121 -0
- package/dist/core/principle-training-state.js +321 -0
- package/dist/core/promotion-gate.d.ts +238 -0
- package/dist/core/promotion-gate.js +529 -0
- package/dist/core/session-tracker.d.ts +10 -0
- package/dist/core/session-tracker.js +14 -0
- package/dist/core/shadow-observation-registry.d.ts +217 -0
- package/dist/core/shadow-observation-registry.js +308 -0
- package/dist/core/training-program.d.ts +233 -0
- package/dist/core/training-program.js +433 -0
- package/dist/core/trajectory.d.ts +155 -1
- package/dist/core/trajectory.js +292 -8
- package/dist/core/workspace-context.d.ts +0 -6
- package/dist/core/workspace-context.js +0 -12
- package/dist/hooks/bash-risk.d.ts +57 -0
- package/dist/hooks/bash-risk.js +137 -0
- package/dist/hooks/edit-verification.d.ts +62 -0
- package/dist/hooks/edit-verification.js +256 -0
- package/dist/hooks/gate-block-helper.d.ts +44 -0
- package/dist/hooks/gate-block-helper.js +119 -0
- package/dist/hooks/gate.d.ts +18 -0
- package/dist/hooks/gate.js +62 -751
- package/dist/hooks/gfi-gate.d.ts +40 -0
- package/dist/hooks/gfi-gate.js +113 -0
- package/dist/hooks/pain.js +6 -9
- package/dist/hooks/progressive-trust-gate.d.ts +51 -0
- package/dist/hooks/progressive-trust-gate.js +89 -0
- package/dist/hooks/prompt.d.ts +11 -11
- package/dist/hooks/prompt.js +167 -77
- package/dist/hooks/subagent.js +43 -6
- package/dist/hooks/thinking-checkpoint.d.ts +37 -0
- package/dist/hooks/thinking-checkpoint.js +51 -0
- package/dist/http/principles-console-route.js +13 -3
- package/dist/i18n/commands.js +8 -8
- package/dist/index.js +129 -28
- package/dist/service/central-database.js +2 -1
- package/dist/service/control-ui-query-service.d.ts +1 -1
- package/dist/service/control-ui-query-service.js +3 -3
- package/dist/service/evolution-query-service.d.ts +1 -1
- package/dist/service/evolution-query-service.js +5 -5
- package/dist/service/evolution-worker.d.ts +52 -4
- package/dist/service/evolution-worker.js +328 -16
- package/dist/service/nocturnal-runtime.d.ts +183 -0
- package/dist/service/nocturnal-runtime.js +352 -0
- package/dist/service/nocturnal-service.d.ts +163 -0
- package/dist/service/nocturnal-service.js +787 -0
- package/dist/service/nocturnal-target-selector.d.ts +145 -0
- package/dist/service/nocturnal-target-selector.js +315 -0
- package/dist/service/phase3-input-filter.d.ts +48 -12
- package/dist/service/phase3-input-filter.js +84 -18
- package/dist/service/runtime-summary-service.d.ts +34 -10
- package/dist/service/runtime-summary-service.js +87 -48
- package/dist/tools/deep-reflect.js +2 -1
- package/dist/types/event-types.d.ts +4 -10
- package/dist/types/runtime-summary.d.ts +47 -0
- package/dist/types/runtime-summary.js +1 -0
- package/dist/types.d.ts +0 -3
- package/dist/types.js +0 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
- package/templates/pain_settings.json +0 -6
- package/dist/commands/trust.d.ts +0 -4
- package/dist/commands/trust.js +0 -78
- package/dist/core/trust-engine.d.ts +0 -96
- package/dist/core/trust-engine.js +0 -286
|
@@ -11,7 +11,61 @@ import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
|
11
11
|
import { acquireLockAsync, releaseLock } from '../utils/file-lock.js';
|
|
12
12
|
import { getEvolutionLogger } from '../core/evolution-logger.js';
|
|
13
13
|
import { DIAGNOSTICIAN_PROTOCOL_SUMMARY } from '../constants/diagnostician.js';
|
|
14
|
+
import { LockUnavailableError } from '../config/index.js';
|
|
15
|
+
import { checkWorkspaceIdle, checkCooldown } from './nocturnal-runtime.js';
|
|
16
|
+
import { executeNocturnalReflectionAsync } from './nocturnal-service.js';
|
|
17
|
+
import { OpenClawTrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
|
|
14
18
|
let intervalId = null;
|
|
19
|
+
let timeoutId = null;
|
|
20
|
+
/**
|
|
21
|
+
* Default values for new V2 fields when migrating legacy items.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_TASK_KIND = 'pain_diagnosis';
|
|
24
|
+
const DEFAULT_PRIORITY = 'medium';
|
|
25
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
26
|
+
/**
|
|
27
|
+
* Migrate a legacy queue item to V2 schema.
|
|
28
|
+
* Old items without taskKind are assumed to be pain_diagnosis for backward compatibility.
|
|
29
|
+
*/
|
|
30
|
+
function migrateToV2(item) {
|
|
31
|
+
return {
|
|
32
|
+
id: item.id,
|
|
33
|
+
taskKind: item.taskKind || DEFAULT_TASK_KIND,
|
|
34
|
+
priority: item.priority || DEFAULT_PRIORITY,
|
|
35
|
+
source: item.source,
|
|
36
|
+
traceId: item.traceId,
|
|
37
|
+
task: item.task,
|
|
38
|
+
score: item.score,
|
|
39
|
+
reason: item.reason,
|
|
40
|
+
timestamp: item.timestamp,
|
|
41
|
+
enqueued_at: item.enqueued_at,
|
|
42
|
+
started_at: item.started_at,
|
|
43
|
+
completed_at: item.completed_at,
|
|
44
|
+
assigned_session_key: item.assigned_session_key,
|
|
45
|
+
trigger_text_preview: item.trigger_text_preview,
|
|
46
|
+
status: item.status || 'pending',
|
|
47
|
+
resolution: item.resolution,
|
|
48
|
+
session_id: item.session_id,
|
|
49
|
+
agent_id: item.agent_id,
|
|
50
|
+
retryCount: item.retryCount || 0,
|
|
51
|
+
maxRetries: item.maxRetries || DEFAULT_MAX_RETRIES,
|
|
52
|
+
lastError: item.lastError,
|
|
53
|
+
resultRef: item.resultRef,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if an item is a legacy (pre-V2) queue item.
|
|
58
|
+
*/
|
|
59
|
+
function isLegacyQueueItem(item) {
|
|
60
|
+
return item && typeof item === 'object' && !('taskKind' in item);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Migrate entire queue to V2 schema if needed.
|
|
64
|
+
* Returns a new array with all items migrated to V2 format.
|
|
65
|
+
*/
|
|
66
|
+
function migrateQueueToV2(queue) {
|
|
67
|
+
return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item) : item);
|
|
68
|
+
}
|
|
15
69
|
const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
|
|
16
70
|
// P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
|
|
17
71
|
export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
@@ -84,8 +138,8 @@ async function requireQueueLock(resourcePath, logger, scope, lockSuffix = EVOLUT
|
|
|
84
138
|
try {
|
|
85
139
|
return await acquireQueueLock(resourcePath, logger, lockSuffix);
|
|
86
140
|
}
|
|
87
|
-
catch {
|
|
88
|
-
throw new
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw new LockUnavailableError(resourcePath, scope, { cause: err });
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
export function extractEvolutionTaskId(task) {
|
|
@@ -128,6 +182,97 @@ export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
|
128
182
|
return false;
|
|
129
183
|
});
|
|
130
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Read recent pain context from PAIN_FLAG file.
|
|
187
|
+
* Returns structured pain metadata for attaching to sleep_reflection tasks.
|
|
188
|
+
* Returns null if no pain flag exists.
|
|
189
|
+
*/
|
|
190
|
+
function readRecentPainContext(wctx) {
|
|
191
|
+
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
192
|
+
if (!fs.existsSync(painFlagPath)) {
|
|
193
|
+
return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const rawPain = fs.readFileSync(painFlagPath, 'utf8');
|
|
197
|
+
const lines = rawPain.split('\n');
|
|
198
|
+
let score = 0;
|
|
199
|
+
let source = '';
|
|
200
|
+
let reason = '';
|
|
201
|
+
let timestamp = '';
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
if (line.startsWith('score:'))
|
|
204
|
+
score = parseInt(line.split(':', 2)[1].trim(), 10) || 0;
|
|
205
|
+
if (line.startsWith('source:'))
|
|
206
|
+
source = line.split(':', 2)[1].trim();
|
|
207
|
+
if (line.startsWith('reason:'))
|
|
208
|
+
reason = line.slice('reason:'.length).trim();
|
|
209
|
+
if (line.startsWith('timestamp:'))
|
|
210
|
+
timestamp = line.slice('timestamp:'.length).trim();
|
|
211
|
+
}
|
|
212
|
+
if (score > 0) {
|
|
213
|
+
return {
|
|
214
|
+
mostRecent: { score, source, reason, timestamp },
|
|
215
|
+
recentPainCount: 1,
|
|
216
|
+
recentMaxPainScore: score,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Best effort — non-fatal
|
|
222
|
+
}
|
|
223
|
+
return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Enqueue a sleep_reflection task if one is not already pending.
|
|
227
|
+
* Phase 2.4: Called when workspace is idle to trigger nocturnal reflection.
|
|
228
|
+
*/
|
|
229
|
+
async function enqueueSleepReflectionTask(wctx, logger) {
|
|
230
|
+
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
231
|
+
const releaseLock = await requireQueueLock(queuePath, logger, 'enqueueSleepReflection', EVOLUTION_QUEUE_LOCK_SUFFIX);
|
|
232
|
+
try {
|
|
233
|
+
let rawQueue = [];
|
|
234
|
+
try {
|
|
235
|
+
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Queue doesn't exist yet - create empty array
|
|
239
|
+
rawQueue = [];
|
|
240
|
+
}
|
|
241
|
+
const queue = migrateQueueToV2(rawQueue);
|
|
242
|
+
// Check if a sleep_reflection task is already pending
|
|
243
|
+
const hasPendingSleepReflection = queue.some(t => t.taskKind === 'sleep_reflection' && (t.status === 'pending' || t.status === 'in_progress'));
|
|
244
|
+
if (hasPendingSleepReflection) {
|
|
245
|
+
logger?.debug?.('[PD:EvolutionWorker] sleep_reflection task already pending/in-progress, skipping');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const taskId = createEvolutionTaskId('nocturnal', 50, 'idle workspace', 'Sleep-mode reflection', now);
|
|
250
|
+
const nowIso = new Date(now).toISOString();
|
|
251
|
+
// Attach recent pain context if available
|
|
252
|
+
const recentPainContext = readRecentPainContext(wctx);
|
|
253
|
+
queue.push({
|
|
254
|
+
id: taskId,
|
|
255
|
+
taskKind: 'sleep_reflection',
|
|
256
|
+
priority: 'medium',
|
|
257
|
+
score: 50,
|
|
258
|
+
source: 'nocturnal',
|
|
259
|
+
reason: 'Sleep-mode reflection triggered by idle workspace',
|
|
260
|
+
trigger_text_preview: 'Idle workspace detected',
|
|
261
|
+
timestamp: nowIso,
|
|
262
|
+
enqueued_at: nowIso,
|
|
263
|
+
status: 'pending',
|
|
264
|
+
traceId: taskId,
|
|
265
|
+
retryCount: 0,
|
|
266
|
+
maxRetries: 1, // sleep_reflection doesn't retry
|
|
267
|
+
recentPainContext,
|
|
268
|
+
});
|
|
269
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
270
|
+
logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
releaseLock();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
131
276
|
async function checkPainFlag(wctx, logger) {
|
|
132
277
|
try {
|
|
133
278
|
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
@@ -188,8 +333,11 @@ async function checkPainFlag(wctx, logger) {
|
|
|
188
333
|
const taskId = createEvolutionTaskId(source, score, preview, reason, now);
|
|
189
334
|
const nowIso = new Date(now).toISOString();
|
|
190
335
|
const effectiveTraceId = traceId || taskId;
|
|
336
|
+
// V2: New queue items include all V2 fields with pain_diagnosis defaults
|
|
191
337
|
queue.push({
|
|
192
338
|
id: taskId,
|
|
339
|
+
taskKind: 'pain_diagnosis', // V2: All pain-flag triggered tasks are pain_diagnosis
|
|
340
|
+
priority: score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low', // V2: Priority based on score
|
|
193
341
|
score,
|
|
194
342
|
source,
|
|
195
343
|
reason,
|
|
@@ -200,6 +348,8 @@ async function checkPainFlag(wctx, logger) {
|
|
|
200
348
|
session_id: sessionId || undefined,
|
|
201
349
|
agent_id: agentId || undefined,
|
|
202
350
|
traceId: effectiveTraceId,
|
|
351
|
+
retryCount: 0, // V2: No retries yet
|
|
352
|
+
maxRetries: 3, // V2: Default max retries
|
|
203
353
|
});
|
|
204
354
|
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
205
355
|
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
|
|
@@ -232,16 +382,17 @@ async function checkPainFlag(wctx, logger) {
|
|
|
232
382
|
logger.warn(`[PD:EvolutionWorker] Error processing pain flag: ${String(err)}`);
|
|
233
383
|
}
|
|
234
384
|
}
|
|
235
|
-
async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
385
|
+
async function processEvolutionQueue(wctx, logger, eventLog, api) {
|
|
236
386
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
237
387
|
if (!fs.existsSync(queuePath))
|
|
238
388
|
return;
|
|
239
389
|
const releaseLock = await requireQueueLock(queuePath, logger, 'processEvolutionQueue');
|
|
240
390
|
const evoLogger = getEvolutionLogger(wctx.workspaceDir, wctx.trajectory);
|
|
391
|
+
let lockReleased = false;
|
|
241
392
|
try {
|
|
242
|
-
let
|
|
393
|
+
let rawQueue = [];
|
|
243
394
|
try {
|
|
244
|
-
|
|
395
|
+
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
245
396
|
}
|
|
246
397
|
catch (e) {
|
|
247
398
|
// Backup corrupted file instead of silently discarding
|
|
@@ -260,11 +411,36 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
260
411
|
}
|
|
261
412
|
return;
|
|
262
413
|
}
|
|
263
|
-
|
|
414
|
+
// V2: Migrate queue to current schema if needed
|
|
415
|
+
const queue = migrateQueueToV2(rawQueue);
|
|
416
|
+
let queueChanged = rawQueue.some(isLegacyQueueItem);
|
|
264
417
|
const config = wctx.config;
|
|
265
418
|
const timeout = config.get('intervals.task_timeout_ms') || (60 * 60 * 1000); // Default 1 hour
|
|
266
|
-
//
|
|
267
|
-
|
|
419
|
+
// V2: Recover stuck in_progress sleep_reflection tasks.
|
|
420
|
+
// If the worker crashes or the result write-back fails after Phase 1 claimed
|
|
421
|
+
// the task, it stays in_progress indefinitely. Detect via timeout and mark
|
|
422
|
+
// as failed so a fresh task can be enqueued on the next idle cycle.
|
|
423
|
+
for (const task of queue.filter(t => t.status === 'in_progress' && t.taskKind === 'sleep_reflection')) {
|
|
424
|
+
const startedAt = new Date(task.started_at || task.timestamp);
|
|
425
|
+
const age = Date.now() - startedAt.getTime();
|
|
426
|
+
if (age > timeout) {
|
|
427
|
+
task.status = 'failed';
|
|
428
|
+
task.completed_at = new Date().toISOString();
|
|
429
|
+
task.resolution = 'failed_max_retries';
|
|
430
|
+
task.lastError = `sleep_reflection timed out after ${Math.round(timeout / 60000)} minutes`;
|
|
431
|
+
task.retryCount = (task.retryCount ?? 0) + 1;
|
|
432
|
+
queueChanged = true;
|
|
433
|
+
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${task.id} timed out after ${Math.round(age / 60000)} minutes, marking as failed`);
|
|
434
|
+
evoLogger.logCompleted({
|
|
435
|
+
traceId: task.traceId || task.id,
|
|
436
|
+
taskId: task.id,
|
|
437
|
+
resolution: 'manual',
|
|
438
|
+
durationMs: age,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Check in_progress tasks for completion (only pain_diagnosis gets HEARTBEAT treatment)
|
|
443
|
+
for (const task of queue.filter(t => t.status === 'in_progress' && t.taskKind === 'pain_diagnosis')) {
|
|
268
444
|
const startedAt = new Date(task.started_at || task.timestamp);
|
|
269
445
|
// Condition 1: Check for marker file (created by diagnostician on completion)
|
|
270
446
|
const completeMarker = path.join(wctx.stateDir, `.evolution_complete_${task.id}`);
|
|
@@ -336,9 +512,21 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
336
512
|
queueChanged = true;
|
|
337
513
|
}
|
|
338
514
|
}
|
|
339
|
-
|
|
515
|
+
// V2: Process pain_diagnosis tasks FIRST (quick, inside lock),
|
|
516
|
+
// then sleep_reflection tasks (slow, lock released during execution).
|
|
517
|
+
// This order ensures pain tasks are never starved by long-running
|
|
518
|
+
// nocturnal reflection — sleep_reflection can safely return early
|
|
519
|
+
// because pain_diagnosis has already been handled.
|
|
520
|
+
const pendingTasks = queue.filter(t => t.status === 'pending' && t.taskKind === 'pain_diagnosis');
|
|
340
521
|
if (pendingTasks.length > 0) {
|
|
341
|
-
|
|
522
|
+
// V2: Also sort by priority within same score
|
|
523
|
+
const priorityWeight = { high: 3, medium: 2, low: 1 };
|
|
524
|
+
const highestScoreTask = pendingTasks.sort((a, b) => {
|
|
525
|
+
const scoreDiff = b.score - a.score;
|
|
526
|
+
if (scoreDiff !== 0)
|
|
527
|
+
return scoreDiff;
|
|
528
|
+
return (priorityWeight[b.priority] || 2) - (priorityWeight[a.priority] || 2);
|
|
529
|
+
})[0];
|
|
342
530
|
const nowIso = new Date().toISOString();
|
|
343
531
|
const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
|
|
344
532
|
`Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
|
|
@@ -408,6 +596,107 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
408
596
|
SystemLogger.log(wctx.workspaceDir, 'HEARTBEAT_WRITE_FAILED', `Task ${highestScoreTask.id} HEARTBEAT write failed: ${String(heartbeatErr)}`);
|
|
409
597
|
}
|
|
410
598
|
}
|
|
599
|
+
// Phase 2.4: Process sleep_reflection tasks AFTER pain_diagnosis.
|
|
600
|
+
// Claim tasks inside the lock, execute reflection outside the lock,
|
|
601
|
+
// then re-acquire the lock to write results. This prevents the long-running
|
|
602
|
+
// nocturnal reflection from blocking all other queue consumers.
|
|
603
|
+
// Safe to return early here because pain_diagnosis was already handled above.
|
|
604
|
+
const sleepReflectionTasks = queue.filter(t => t.status === 'pending' && t.taskKind === 'sleep_reflection');
|
|
605
|
+
if (sleepReflectionTasks.length > 0) {
|
|
606
|
+
// --- Phase 1: Claim tasks (inside lock) ---
|
|
607
|
+
for (const sleepTask of sleepReflectionTasks) {
|
|
608
|
+
sleepTask.status = 'in_progress';
|
|
609
|
+
sleepTask.started_at = new Date().toISOString();
|
|
610
|
+
}
|
|
611
|
+
queueChanged = true;
|
|
612
|
+
// Write claimed state (includes any pain changes from above) and release lock
|
|
613
|
+
if (queueChanged) {
|
|
614
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
615
|
+
}
|
|
616
|
+
releaseLock();
|
|
617
|
+
for (const sleepTask of sleepReflectionTasks) {
|
|
618
|
+
try {
|
|
619
|
+
logger?.info?.(`[PD:EvolutionWorker] Processing sleep_reflection task ${sleepTask.id}`);
|
|
620
|
+
// Build runtime adapter for real Trinity execution if api is available
|
|
621
|
+
const runtimeAdapter = api ? new OpenClawTrinityRuntimeAdapter(api) : undefined;
|
|
622
|
+
// Call the nocturnal reflection service
|
|
623
|
+
const result = await executeNocturnalReflectionAsync(wctx.workspaceDir, wctx.stateDir, {
|
|
624
|
+
runtimeAdapter,
|
|
625
|
+
});
|
|
626
|
+
if (result.success && result.artifact) {
|
|
627
|
+
sleepTask.status = 'completed';
|
|
628
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
629
|
+
sleepTask.resolution = 'marker_detected';
|
|
630
|
+
sleepTask.resultRef = result.diagnostics.persistedPath;
|
|
631
|
+
logger?.info?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} completed successfully`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// Record failure with skip reason
|
|
635
|
+
const skipReason = result.skipReason || (result.noTargetSelected ? 'no_target' : 'validation_failed');
|
|
636
|
+
sleepTask.status = 'failed';
|
|
637
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
638
|
+
sleepTask.resolution = 'failed_max_retries';
|
|
639
|
+
sleepTask.lastError = `Nocturnal reflection failed: ${result.validationFailures.join('; ') || skipReason}`;
|
|
640
|
+
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
641
|
+
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} failed: ${sleepTask.lastError}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (taskErr) {
|
|
645
|
+
sleepTask.status = 'failed';
|
|
646
|
+
sleepTask.completed_at = new Date().toISOString();
|
|
647
|
+
sleepTask.resolution = 'failed_max_retries';
|
|
648
|
+
sleepTask.lastError = String(taskErr);
|
|
649
|
+
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
650
|
+
logger?.error?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} threw: ${taskErr}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// --- Phase 3: Write results back (re-acquire lock) ---
|
|
654
|
+
try {
|
|
655
|
+
const resultLock = await requireQueueLock(queuePath, logger, 'sleepReflectionResult');
|
|
656
|
+
try {
|
|
657
|
+
// Re-read queue to merge with any changes made while lock was released
|
|
658
|
+
let freshQueue = [];
|
|
659
|
+
try {
|
|
660
|
+
freshQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
661
|
+
}
|
|
662
|
+
catch { /* empty queue if corrupted */ }
|
|
663
|
+
// Merge: update tasks by ID
|
|
664
|
+
for (const sleepTask of sleepReflectionTasks) {
|
|
665
|
+
const idx = freshQueue.findIndex((t) => t.id === sleepTask.id);
|
|
666
|
+
if (idx >= 0) {
|
|
667
|
+
freshQueue[idx] = sleepTask;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2), 'utf8');
|
|
671
|
+
// Log completions to EvolutionLogger
|
|
672
|
+
for (const sleepTask of sleepReflectionTasks) {
|
|
673
|
+
if (sleepTask.status === 'completed' || sleepTask.status === 'failed') {
|
|
674
|
+
evoLogger.logCompleted({
|
|
675
|
+
traceId: sleepTask.traceId || sleepTask.id,
|
|
676
|
+
taskId: sleepTask.id,
|
|
677
|
+
resolution: sleepTask.status === 'completed'
|
|
678
|
+
? (sleepTask.resolution === 'marker_detected' ? 'marker_detected' : 'manual')
|
|
679
|
+
: 'manual',
|
|
680
|
+
durationMs: sleepTask.started_at
|
|
681
|
+
? Date.now() - new Date(sleepTask.started_at).getTime()
|
|
682
|
+
: undefined,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
resultLock();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (resultLockErr) {
|
|
692
|
+
// If we can't re-acquire lock, results are in memory but not persisted.
|
|
693
|
+
// Tasks will appear stuck as in_progress and will be retried on next cycle.
|
|
694
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to write sleep_reflection results back: ${String(resultLockErr)}`);
|
|
695
|
+
}
|
|
696
|
+
// Safe to return — pain_diagnosis was already processed above.
|
|
697
|
+
lockReleased = true;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
411
700
|
if (queueChanged) {
|
|
412
701
|
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
413
702
|
}
|
|
@@ -417,7 +706,9 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
417
706
|
logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
|
|
418
707
|
}
|
|
419
708
|
finally {
|
|
420
|
-
|
|
709
|
+
if (!lockReleased) {
|
|
710
|
+
releaseLock();
|
|
711
|
+
}
|
|
421
712
|
}
|
|
422
713
|
}
|
|
423
714
|
async function processDetectionQueue(wctx, api, eventLog) {
|
|
@@ -577,14 +868,16 @@ export async function registerEvolutionTaskSession(workspaceResolve, taskId, ses
|
|
|
577
868
|
return false;
|
|
578
869
|
const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
|
|
579
870
|
try {
|
|
580
|
-
let
|
|
871
|
+
let rawQueue;
|
|
581
872
|
try {
|
|
582
|
-
|
|
873
|
+
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
583
874
|
}
|
|
584
875
|
catch (parseErr) {
|
|
585
876
|
logger?.warn?.(`[PD:EvolutionWorker] Failed to parse EVOLUTION_QUEUE for session registration: ${queuePath} - ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
586
877
|
return false;
|
|
587
878
|
}
|
|
879
|
+
// V2: Migrate queue to current schema
|
|
880
|
+
const queue = migrateQueueToV2(rawQueue);
|
|
588
881
|
const task = queue.find((item) => item.id === taskId && item.status === 'in_progress');
|
|
589
882
|
if (!task) {
|
|
590
883
|
logger?.warn?.(`[PD:EvolutionWorker] Could not find in-progress evolution task ${taskId} for session assignment`);
|
|
@@ -625,8 +918,25 @@ export const EvolutionWorkerService = {
|
|
|
625
918
|
const interval = config.get('intervals.worker_poll_ms') || (15 * 60 * 1000);
|
|
626
919
|
intervalId = setInterval(() => {
|
|
627
920
|
void (async () => {
|
|
921
|
+
// V2: Nocturnal idle check — logs workspace idle state on each cycle.
|
|
922
|
+
// This makes nocturnal-runtime a visible part of the worker lifecycle.
|
|
923
|
+
// Phase 2.4: Enqueue sleep_reflection when workspace is idle and not in cooldown.
|
|
924
|
+
const idleResult = checkWorkspaceIdle(wctx.workspaceDir, {});
|
|
925
|
+
if (idleResult.isIdle) {
|
|
926
|
+
logger?.debug?.(`[PD:EvolutionWorker] Workspace idle (${idleResult.idleForMs}ms since last activity)`);
|
|
927
|
+
// Phase 2.4: Enqueue sleep_reflection task if not in global cooldown
|
|
928
|
+
const cooldown = checkCooldown(wctx.stateDir);
|
|
929
|
+
if (!cooldown.globalCooldownActive && !cooldown.quotaExhausted) {
|
|
930
|
+
enqueueSleepReflectionTask(wctx, logger).catch((err) => {
|
|
931
|
+
logger?.error?.(`[PD:EvolutionWorker] Failed to enqueue sleep_reflection task: ${String(err)}`);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
logger?.debug?.(`[PD:EvolutionWorker] Workspace active (last activity ${idleResult.idleForMs}ms ago)`);
|
|
937
|
+
}
|
|
628
938
|
await checkPainFlag(wctx, logger);
|
|
629
|
-
await processEvolutionQueue(wctx, logger, eventLog);
|
|
939
|
+
await processEvolutionQueue(wctx, logger, eventLog, api ?? undefined);
|
|
630
940
|
if (api) {
|
|
631
941
|
await processDetectionQueue(wctx, api, eventLog);
|
|
632
942
|
}
|
|
@@ -638,10 +948,10 @@ export const EvolutionWorkerService = {
|
|
|
638
948
|
logger.error(`[PD:EvolutionWorker] Error in worker interval: ${String(err)}`);
|
|
639
949
|
});
|
|
640
950
|
}, interval);
|
|
641
|
-
setTimeout(() => {
|
|
951
|
+
timeoutId = setTimeout(() => {
|
|
642
952
|
void (async () => {
|
|
643
953
|
await checkPainFlag(wctx, logger);
|
|
644
|
-
await processEvolutionQueue(wctx, logger, eventLog);
|
|
954
|
+
await processEvolutionQueue(wctx, logger, eventLog, api ?? undefined);
|
|
645
955
|
if (api) {
|
|
646
956
|
await processDetectionQueue(wctx, api, eventLog);
|
|
647
957
|
}
|
|
@@ -657,6 +967,8 @@ export const EvolutionWorkerService = {
|
|
|
657
967
|
ctx.logger.info('[PD:EvolutionWorker] Stopping background service...');
|
|
658
968
|
if (intervalId)
|
|
659
969
|
clearInterval(intervalId);
|
|
970
|
+
if (timeoutId)
|
|
971
|
+
clearTimeout(timeoutId);
|
|
660
972
|
flushAllSessions();
|
|
661
973
|
}
|
|
662
974
|
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nocturnal Runtime Service — Idle Detection Source of Truth
|
|
3
|
+
* ===========================================================
|
|
4
|
+
*
|
|
5
|
+
* This module is the authoritative source for workspace idle state used by the
|
|
6
|
+
* nocturnal reflection pipeline. It must NOT use `.last_active.json` as the primary
|
|
7
|
+
* source of truth.
|
|
8
|
+
*
|
|
9
|
+
* SOURCE OF TRUTH HIERARCHY (ordered by priority):
|
|
10
|
+
* 1. SessionState.lastActivityAt — via listSessions(workspaceDir)
|
|
11
|
+
* 2. trajectory timestamps — secondary guardrail only, NOT primary
|
|
12
|
+
* 3. nocturnal-runtime.json — cooldown/quota bookkeeping (ephemeral state)
|
|
13
|
+
*
|
|
14
|
+
* DESIGN CONSTRAINTS:
|
|
15
|
+
* - No `.last_active.json` as primary idle source
|
|
16
|
+
* - trajectory timestamps are a guardrail, not the primary source
|
|
17
|
+
* - cooldown/quota state is persisted in nocturnal-runtime.json
|
|
18
|
+
* - abandoned sessions (>2h inactive) must not block nocturnal flow
|
|
19
|
+
*/
|
|
20
|
+
/** File name for nocturnal runtime bookkeeping */
|
|
21
|
+
export declare const NOCTURNAL_RUNTIME_FILE = "nocturnal-runtime.json";
|
|
22
|
+
/** Default idle threshold: workspace is considered idle if no activity for this duration (ms) */
|
|
23
|
+
export declare const DEFAULT_IDLE_THRESHOLD_MS: number;
|
|
24
|
+
/** Default cooldown between nocturnal runs (ms) */
|
|
25
|
+
export declare const DEFAULT_GLOBAL_COOLDOWN_MS: number;
|
|
26
|
+
/** Default per-principle cooldown (ms) */
|
|
27
|
+
export declare const DEFAULT_PRINCIPLE_COOLDOWN_MS: number;
|
|
28
|
+
/** Default maximum nocturnal runs per quota window */
|
|
29
|
+
export declare const DEFAULT_MAX_RUNS_PER_WINDOW = 3;
|
|
30
|
+
/** Default quota window size (ms) */
|
|
31
|
+
export declare const DEFAULT_QUOTA_WINDOW_MS: number;
|
|
32
|
+
/** Abandoned session threshold: sessions inactive for longer than this are ignored (ms) */
|
|
33
|
+
export declare const DEFAULT_ABANDONED_THRESHOLD_MS: number;
|
|
34
|
+
/**
|
|
35
|
+
* Persisted state for nocturnal runtime bookkeeping.
|
|
36
|
+
* Stored in {stateDir}/nocturnal-runtime.json
|
|
37
|
+
*/
|
|
38
|
+
export interface NocturnalRuntimeState {
|
|
39
|
+
/** Last time a nocturnal run was started (ISO string) */
|
|
40
|
+
lastRunAt?: string;
|
|
41
|
+
/** Last time a nocturnal run completed successfully */
|
|
42
|
+
lastSuccessfulRunAt?: string;
|
|
43
|
+
/** Cooldown end time for global cooldown (ISO string) */
|
|
44
|
+
globalCooldownUntil?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Per-principle cooldown map.
|
|
47
|
+
* Key: principleId, Value: ISO string of cooldown end time
|
|
48
|
+
*/
|
|
49
|
+
principleCooldowns: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Sliding window of recent run timestamps.
|
|
52
|
+
* Used for quota enforcement.
|
|
53
|
+
*/
|
|
54
|
+
recentRunTimestamps: string[];
|
|
55
|
+
/** Metadata about last run (for debugging) */
|
|
56
|
+
lastRunMeta?: {
|
|
57
|
+
targetPrincipleId?: string;
|
|
58
|
+
sampleCount?: number;
|
|
59
|
+
status: 'success' | 'failed' | 'skipped';
|
|
60
|
+
reason?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Result of an idle check */
|
|
64
|
+
export interface IdleCheckResult {
|
|
65
|
+
/** Whether the workspace is currently idle */
|
|
66
|
+
isIdle: boolean;
|
|
67
|
+
/** Most recent activity timestamp across all sessions (epoch ms) */
|
|
68
|
+
mostRecentActivityAt: number;
|
|
69
|
+
/** How long since the last activity (ms) */
|
|
70
|
+
idleForMs: number;
|
|
71
|
+
/** Number of active (non-abandoned) sessions found */
|
|
72
|
+
activeSessionCount: number;
|
|
73
|
+
/** List of abandoned session IDs (inactive > abandoned threshold) */
|
|
74
|
+
abandonedSessionIds: string[];
|
|
75
|
+
/** Whether trajectory guardrail also confirms idle */
|
|
76
|
+
trajectoryGuardrailConfirmsIdle: boolean;
|
|
77
|
+
/** Reason for the idle determination */
|
|
78
|
+
reason: string;
|
|
79
|
+
}
|
|
80
|
+
/** Result of a cooldown check */
|
|
81
|
+
export interface CooldownCheckResult {
|
|
82
|
+
/** Whether the global cooldown is currently active */
|
|
83
|
+
globalCooldownActive: boolean;
|
|
84
|
+
/** When the global cooldown ends (ISO string), null if not in cooldown */
|
|
85
|
+
globalCooldownUntil: string | null;
|
|
86
|
+
/** Remaining ms until global cooldown expires */
|
|
87
|
+
globalCooldownRemainingMs: number;
|
|
88
|
+
/** Whether the principle-specific cooldown is active */
|
|
89
|
+
principleCooldownActive: boolean;
|
|
90
|
+
/** When the principle cooldown ends (ISO string), null if not in cooldown */
|
|
91
|
+
principleCooldownUntil: string | null;
|
|
92
|
+
/** Remaining ms until principle cooldown expires */
|
|
93
|
+
principleCooldownRemainingMs: number;
|
|
94
|
+
/** Whether the quota has been exhausted */
|
|
95
|
+
quotaExhausted: boolean;
|
|
96
|
+
/** Number of runs remaining in current window */
|
|
97
|
+
runsRemaining: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if the workspace is currently idle based on session activity.
|
|
101
|
+
*
|
|
102
|
+
* IDLE DETERMINATION LOGIC:
|
|
103
|
+
* - Collect all sessions for the workspace via listSessions()
|
|
104
|
+
* - Filter out abandoned sessions (inactive > abandonedThresholdMs)
|
|
105
|
+
* - Workspace is idle if: no active sessions OR all active sessions have lastActivityAt older than idleThresholdMs
|
|
106
|
+
* - Abandoned sessions do NOT contribute to idle determination
|
|
107
|
+
*
|
|
108
|
+
* @param workspaceDir - Workspace directory to check
|
|
109
|
+
* @param options.idleThresholdMs - Consider idle if no activity for this duration (default: 30 min)
|
|
110
|
+
* @param options.abandonedThresholdMs - Consider session abandoned if inactive for this duration (default: 2 hr)
|
|
111
|
+
* @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
|
|
112
|
+
* @returns IdleCheckResult with full diagnostic information
|
|
113
|
+
*/
|
|
114
|
+
export declare function checkWorkspaceIdle(workspaceDir: string, options?: {
|
|
115
|
+
idleThresholdMs?: number;
|
|
116
|
+
abandonedThresholdMs?: number;
|
|
117
|
+
}, trajectoryLastActivityAt?: number): IdleCheckResult;
|
|
118
|
+
/**
|
|
119
|
+
* Check if the workspace is currently in a cooldown period.
|
|
120
|
+
*
|
|
121
|
+
* @param stateDir - State directory
|
|
122
|
+
* @param principleId - Optional principle ID to check per-principle cooldown
|
|
123
|
+
* @param options - Cooldown configuration options
|
|
124
|
+
* @returns CooldownCheckResult
|
|
125
|
+
*/
|
|
126
|
+
export declare function checkCooldown(stateDir: string, principleId?: string, options?: {
|
|
127
|
+
globalCooldownMs?: number;
|
|
128
|
+
principleCooldownMs?: number;
|
|
129
|
+
maxRunsPerWindow?: number;
|
|
130
|
+
quotaWindowMs?: number;
|
|
131
|
+
}): CooldownCheckResult;
|
|
132
|
+
/**
|
|
133
|
+
* Record that a nocturnal run has started.
|
|
134
|
+
* Updates global cooldown and quota tracking.
|
|
135
|
+
*
|
|
136
|
+
* @param stateDir - State directory
|
|
137
|
+
* @param principleId - Target principle ID for this run
|
|
138
|
+
*/
|
|
139
|
+
export declare function recordRunStart(stateDir: string, principleId: string): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Record the outcome of a nocturnal run.
|
|
142
|
+
*
|
|
143
|
+
* @param stateDir - State directory
|
|
144
|
+
* @param outcome - 'success', 'failed', or 'skipped'
|
|
145
|
+
* @param details - Optional details about the run
|
|
146
|
+
*/
|
|
147
|
+
export declare function recordRunEnd(stateDir: string, outcome: 'success' | 'failed' | 'skipped', details?: {
|
|
148
|
+
sampleCount?: number;
|
|
149
|
+
reason?: string;
|
|
150
|
+
}): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Clear all cooldowns (for testing or admin reset).
|
|
153
|
+
*
|
|
154
|
+
* @param stateDir - State directory
|
|
155
|
+
*/
|
|
156
|
+
export declare function clearAllCooldowns(stateDir: string): Promise<void>;
|
|
157
|
+
/**
|
|
158
|
+
* Get the current runtime state (for debugging/inspection).
|
|
159
|
+
*
|
|
160
|
+
* @param stateDir - State directory
|
|
161
|
+
* @returns The current NocturnalRuntimeState
|
|
162
|
+
*/
|
|
163
|
+
export declare function getRuntimeState(stateDir: string): Promise<NocturnalRuntimeState>;
|
|
164
|
+
export interface PreflightCheckResult {
|
|
165
|
+
canRun: boolean;
|
|
166
|
+
idle: IdleCheckResult;
|
|
167
|
+
cooldown: CooldownCheckResult;
|
|
168
|
+
/**
|
|
169
|
+
* Human-readable reasons why run is blocked (if canRun is false)
|
|
170
|
+
*/
|
|
171
|
+
blockers: string[];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Combined pre-flight check for whether a nocturnal run should proceed.
|
|
175
|
+
* Integrates idle + cooldown + quota checks.
|
|
176
|
+
*
|
|
177
|
+
* @param workspaceDir - Workspace directory
|
|
178
|
+
* @param stateDir - State directory
|
|
179
|
+
* @param principleId - Target principle ID
|
|
180
|
+
* @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
|
|
181
|
+
* @param idleCheckOverride - Optional override for idle check result (for testing)
|
|
182
|
+
*/
|
|
183
|
+
export declare function checkPreflight(workspaceDir: string, stateDir: string, principleId?: string, trajectoryLastActivityAt?: number, idleCheckOverride?: IdleCheckResult): PreflightCheckResult;
|