principles-disciple 1.23.0 → 1.25.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 +4 -4
- package/package.json +1 -1
- package/scripts/diagnose-nocturnal.mjs +400 -0
- package/src/service/evolution-worker.ts +14 -0
- package/src/service/nocturnal-runtime.ts +14 -9
- package/src/service/nocturnal-service.ts +11 -4
- package/src/service/nocturnal-target-selector.ts +4 -2
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +8 -2
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.25.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "6c8a61389b46",
|
|
80
|
+
"bundleMd5": "2803099a748622cc50286ad4c55be551",
|
|
81
|
+
"builtAt": "2026-04-13T02:02:51.311Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nocturnal Pipeline Diagnostic Script
|
|
5
|
+
* ======================================
|
|
6
|
+
* Checks every link in the Nocturnal reflection chain:
|
|
7
|
+
* Heartbeat → Idle Detection → Queue → Snapshot → Workflow → Trinity → Arbiter → Persistence
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/diagnose-nocturnal.mjs [--workspace /path/to/workspace]
|
|
11
|
+
*
|
|
12
|
+
* Output: Structured report with pass/fail for each checkpoint.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
16
|
+
import { join, dirname } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
const PLUGIN_DIR = join(__dirname, '..');
|
|
23
|
+
|
|
24
|
+
// ─── Argument parsing ───
|
|
25
|
+
function parseArgs() {
|
|
26
|
+
let workspaceDir = null;
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
if (argv[i] === '--workspace' && argv[i + 1]) {
|
|
30
|
+
workspaceDir = argv[++i];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Auto-detect workspace from current git working directory
|
|
34
|
+
if (!workspaceDir) {
|
|
35
|
+
try {
|
|
36
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
37
|
+
workspaceDir = gitRoot;
|
|
38
|
+
} catch {
|
|
39
|
+
workspaceDir = process.cwd();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { workspaceDir };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Report helpers ───
|
|
46
|
+
const results = [];
|
|
47
|
+
let checksPassed = 0;
|
|
48
|
+
let checksFailed = 0;
|
|
49
|
+
let checksWarned = 0;
|
|
50
|
+
|
|
51
|
+
function check(name, fn) {
|
|
52
|
+
try {
|
|
53
|
+
const result = fn();
|
|
54
|
+
if (result && result.status === 'warn') {
|
|
55
|
+
checksWarned++;
|
|
56
|
+
results.push({ name, status: 'warn', detail: result.detail || '' });
|
|
57
|
+
} else {
|
|
58
|
+
checksPassed++;
|
|
59
|
+
results.push({ name, status: 'pass', detail: typeof result === 'string' ? result : '' });
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
checksFailed++;
|
|
63
|
+
results.push({ name, status: 'fail', detail: err.message || String(err) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printReport() {
|
|
68
|
+
console.log('\n' + '='.repeat(60));
|
|
69
|
+
console.log(' NOCTURNAL PIPELINE DIAGNOSTIC REPORT');
|
|
70
|
+
console.log(' ' + new Date().toISOString());
|
|
71
|
+
console.log('='.repeat(60));
|
|
72
|
+
|
|
73
|
+
for (const r of results) {
|
|
74
|
+
const icon = r.status === 'pass' ? '✅' : r.status === 'warn' ? '⚠️ ' : '❌';
|
|
75
|
+
console.log(`\n${icon} ${r.name}`);
|
|
76
|
+
if (r.detail) {
|
|
77
|
+
console.log(` ${r.detail}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\n' + '-'.repeat(60));
|
|
82
|
+
console.log(` Summary: ${checksPassed} passed, ${checksWarned} warnings, ${checksFailed} failed`);
|
|
83
|
+
console.log('-'.repeat(60) + '\n');
|
|
84
|
+
|
|
85
|
+
if (checksFailed > 0) {
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Main ───
|
|
91
|
+
function main() {
|
|
92
|
+
const { workspaceDir } = parseArgs();
|
|
93
|
+
const stateDir = join(workspaceDir, '.state');
|
|
94
|
+
|
|
95
|
+
console.log(`\n🔍 Diagnosing Nocturnal pipeline for workspace: ${workspaceDir}`);
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────
|
|
98
|
+
// CHECKPOINT 1: State directory structure
|
|
99
|
+
// ─────────────────────────────────────────────────────────
|
|
100
|
+
check('1. State directory structure', () => {
|
|
101
|
+
// All state dirs are inside .state/
|
|
102
|
+
const required = ['sessions', 'logs', 'nocturnal', 'nocturnal/samples'];
|
|
103
|
+
const missing = [];
|
|
104
|
+
for (const rel of required) {
|
|
105
|
+
if (!existsSync(join(stateDir, rel))) missing.push(rel);
|
|
106
|
+
}
|
|
107
|
+
if (missing.length > 0) throw new Error(`Missing directories: ${missing.join(', ')}`);
|
|
108
|
+
return 'All required directories present';
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────
|
|
112
|
+
// CHECKPOINT 2: Session tracker persistence
|
|
113
|
+
// ─────────────────────────────────────────────────────────
|
|
114
|
+
check('2. Session tracker persistence', () => {
|
|
115
|
+
const sessionsDir = join(stateDir, 'sessions');
|
|
116
|
+
if (!existsSync(sessionsDir)) throw new Error('sessions/ directory missing');
|
|
117
|
+
const files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
|
|
118
|
+
if (files.length === 0) {
|
|
119
|
+
return { status: 'warn', detail: 'No session files found — idle check will report idle immediately' };
|
|
120
|
+
}
|
|
121
|
+
// Verify at least one session file is valid JSON
|
|
122
|
+
let validSessions = 0;
|
|
123
|
+
for (const f of files) {
|
|
124
|
+
try {
|
|
125
|
+
const data = JSON.parse(readFileSync(join(sessionsDir, f), 'utf-8'));
|
|
126
|
+
if (data.sessionId && data.lastActivityAt) validSessions++;
|
|
127
|
+
} catch { /* corrupted, skip */ }
|
|
128
|
+
}
|
|
129
|
+
if (validSessions === 0) {
|
|
130
|
+
return { status: 'warn', detail: `${files.length} session file(s) found but none are valid JSON — idle check will likely report idle immediately` };
|
|
131
|
+
}
|
|
132
|
+
return `${files.length} session files, ${validSessions} valid with sessionId+lastActivityAt`;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─────────────────────────────────────────────────────────
|
|
136
|
+
// CHECKPOINT 3: Idle detection logic
|
|
137
|
+
// ─────────────────────────────────────────────────────────
|
|
138
|
+
check('3. Idle detection (checkWorkspaceIdle)', () => {
|
|
139
|
+
// Functions are minified — check for unique string markers instead.
|
|
140
|
+
const bundlePath = join(PLUGIN_DIR, 'dist', 'bundle.js');
|
|
141
|
+
const content = readFileSync(bundlePath, 'utf-8');
|
|
142
|
+
|
|
143
|
+
// Stable markers: log messages, object fields, event strings that survive minification.
|
|
144
|
+
const markers = [
|
|
145
|
+
{ name: 'Workspace not idle', reason: 'preflight idle check log message' },
|
|
146
|
+
{ name: 'trigger', reason: 'system session detection (checks trigger field)' },
|
|
147
|
+
{ name: 'abandonedSessionIds', reason: 'IdleCheckResult field (preserved in object literal)' },
|
|
148
|
+
{ name: 'trajectoryGuardrailConfirmsIdle', reason: 'IdleCheckResult field' },
|
|
149
|
+
];
|
|
150
|
+
const missing = markers.filter(m => !content.includes(m.name));
|
|
151
|
+
if (missing.length > 0) {
|
|
152
|
+
throw new Error(`Idle detection markers missing: ${missing.map(m => m.name).join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check PR #256 fix: legacy session temporal guard
|
|
156
|
+
// The fix adds `lastActivityAt` comparison before treating sessions as system sessions.
|
|
157
|
+
// In minified code this appears as a comparison involving `lastActivityAt`.
|
|
158
|
+
if (!content.includes('lastActivityAt')) {
|
|
159
|
+
return { status: 'warn', detail: 'lastActivityAt reference not found — temporal guard for legacy sessions may be missing' };
|
|
160
|
+
}
|
|
161
|
+
return 'Idle detection functions present (verified via stable string markers)';
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────
|
|
165
|
+
// CHECKPOINT 4: Evolution queue
|
|
166
|
+
// ─────────────────────────────────────────────────────────
|
|
167
|
+
check('4. Evolution queue', () => {
|
|
168
|
+
const queuePath = join(stateDir, 'evolution_queue.json');
|
|
169
|
+
if (!existsSync(queuePath)) {
|
|
170
|
+
return { status: 'warn', detail: 'No evolution queue — idle check has not yet enqueued a task' };
|
|
171
|
+
}
|
|
172
|
+
const queue = JSON.parse(readFileSync(queuePath, 'utf-8'));
|
|
173
|
+
const sleepTasks = queue.filter(t => t.taskKind === 'sleep_reflection');
|
|
174
|
+
const pending = sleepTasks.filter(t => t.status === 'pending' || t.status === 'in_progress');
|
|
175
|
+
const completed = sleepTasks.filter(t => t.status === 'completed');
|
|
176
|
+
const failed = sleepTasks.filter(t => t.status === 'failed');
|
|
177
|
+
|
|
178
|
+
if (pending.length > 0) return `${pending.length} pending sleep_reflection task(s) awaiting processing`;
|
|
179
|
+
if (completed.length > 0) return `${completed.length} completed, ${failed.length} failed (total ${sleepTasks.length} tasks)`;
|
|
180
|
+
return { status: 'warn', detail: `Queue exists with ${queue.length} items but no sleep_reflection tasks` };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────
|
|
184
|
+
// CHECKPOINT 5: Nocturnal samples (artifacts)
|
|
185
|
+
// ─────────────────────────────────────────────────────────
|
|
186
|
+
check('5. Nocturnal artifact persistence', () => {
|
|
187
|
+
const samplesDir = join(stateDir, 'nocturnal', 'samples');
|
|
188
|
+
if (!existsSync(samplesDir)) {
|
|
189
|
+
return { status: 'warn', detail: 'No samples directory — no reflections have been persisted yet' };
|
|
190
|
+
}
|
|
191
|
+
const files = readdirSync(samplesDir).filter(f => f.endsWith('.json'));
|
|
192
|
+
if (files.length === 0) return { status: 'warn', detail: 'samples/ directory exists but is empty' };
|
|
193
|
+
|
|
194
|
+
// Validate most recent artifact
|
|
195
|
+
const sorted = files
|
|
196
|
+
.map(f => ({ name: f, mtime: statSync(join(samplesDir, f)).mtimeMs }))
|
|
197
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
198
|
+
const latest = sorted[0].name;
|
|
199
|
+
const artifact = JSON.parse(readFileSync(join(samplesDir, latest), 'utf-8'));
|
|
200
|
+
const hasRequired = artifact.artifactId && artifact.badDecision && artifact.betterDecision && artifact.rationale;
|
|
201
|
+
if (!hasRequired) {
|
|
202
|
+
return { status: 'warn', detail: `Latest artifact ${latest} is missing required fields` };
|
|
203
|
+
}
|
|
204
|
+
return `${files.length} artifact(s), latest: ${latest} (${artifact.principleId || 'unknown principle'})`;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ─────────────────────────────────────────────────────────
|
|
208
|
+
// CHECKPOINT 6: Workflow store
|
|
209
|
+
// ─────────────────────────────────────────────────────────
|
|
210
|
+
check('6. Nocturnal workflow store', () => {
|
|
211
|
+
const workflowsPath = join(stateDir, 'nocturnal', 'workflows.json');
|
|
212
|
+
if (!existsSync(workflowsPath)) {
|
|
213
|
+
return { status: 'warn', detail: 'No workflows.json — no nocturnal workflows have been started' };
|
|
214
|
+
}
|
|
215
|
+
const workflows = JSON.parse(readFileSync(workflowsPath, 'utf-8'));
|
|
216
|
+
if (!Array.isArray(workflows) || workflows.length === 0) {
|
|
217
|
+
return { status: 'warn', detail: 'workflows.json is empty — no workflows recorded' };
|
|
218
|
+
}
|
|
219
|
+
const active = workflows.filter(w => w.state === 'active');
|
|
220
|
+
const completed = workflows.filter(w => w.state === 'completed');
|
|
221
|
+
const errored = workflows.filter(w => w.state === 'terminal_error');
|
|
222
|
+
const expired = workflows.filter(w => w.state === 'expired');
|
|
223
|
+
|
|
224
|
+
if (active.length > 0) {
|
|
225
|
+
return { status: 'warn', detail: `${active.length} workflow(s) still active — may be in progress or stuck. IDs: ${active.map(w => w.workflow_id).join(', ')}` };
|
|
226
|
+
}
|
|
227
|
+
return `${workflows.length} total: ${completed.length} completed, ${errored.length} errored, ${expired.length} expired`;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─────────────────────────────────────────────────────────
|
|
231
|
+
// CHECKPOINT 7: Nocturnal runtime state (cooldown/quota)
|
|
232
|
+
// ─────────────────────────────────────────────────────────
|
|
233
|
+
check('7. Nocturnal runtime state (cooldown/quota)', () => {
|
|
234
|
+
const runtimePath = join(stateDir, 'nocturnal-runtime.json');
|
|
235
|
+
if (!existsSync(runtimePath)) {
|
|
236
|
+
return 'No runtime state — no cooldown or quota restrictions';
|
|
237
|
+
}
|
|
238
|
+
const state = JSON.parse(readFileSync(runtimePath, 'utf-8'));
|
|
239
|
+
const issues = [];
|
|
240
|
+
|
|
241
|
+
if (state.globalCooldownUntil) {
|
|
242
|
+
const cooldownEnd = new Date(state.globalCooldownUntil).getTime();
|
|
243
|
+
if (cooldownEnd > Date.now()) {
|
|
244
|
+
const remainingMin = Math.round((cooldownEnd - Date.now()) / 60000);
|
|
245
|
+
issues.push(`global cooldown active (${remainingMin}min remaining)`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (state.recentRunTimestamps) {
|
|
250
|
+
const windowStart = Date.now() - 24 * 60 * 60 * 1000;
|
|
251
|
+
const recentRuns = state.recentRunTimestamps
|
|
252
|
+
.map(ts => new Date(ts).getTime())
|
|
253
|
+
.filter(ts => ts > windowStart);
|
|
254
|
+
if (recentRuns.length >= 3) {
|
|
255
|
+
issues.push(`quota exhausted (${recentRuns.length}/3 runs used in 24h)`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (issues.length > 0) {
|
|
260
|
+
return { status: 'warn', detail: issues.join('; ') };
|
|
261
|
+
}
|
|
262
|
+
return 'No active cooldown or quota restrictions';
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─────────────────────────────────────────────────────────
|
|
266
|
+
// CHECKPOINT 8: Bundle health
|
|
267
|
+
// ─────────────────────────────────────────────────────────
|
|
268
|
+
check('8. Plugin bundle health', () => {
|
|
269
|
+
const bundlePath = join(PLUGIN_DIR, 'dist', 'bundle.js');
|
|
270
|
+
if (!existsSync(bundlePath)) throw new Error('dist/bundle.js missing — run build first');
|
|
271
|
+
|
|
272
|
+
const content = readFileSync(bundlePath, 'utf-8');
|
|
273
|
+
|
|
274
|
+
// Use a mix of exported symbols and stable string markers.
|
|
275
|
+
// Class names and exported symbols survive minification; internal function names don't.
|
|
276
|
+
const markers = [
|
|
277
|
+
'EvolutionWorkerService', // exported class
|
|
278
|
+
'checkPainFlag', // exported function
|
|
279
|
+
'processEvolutionQueue', // function reference
|
|
280
|
+
'NocturnalWorkflowManager', // exported class
|
|
281
|
+
'executeNocturnalReflectionAsync', // used in log messages
|
|
282
|
+
'nocturnal_started', // event type string
|
|
283
|
+
'nocturnal_completed', // event type string
|
|
284
|
+
'nocturnal_failed', // event type string
|
|
285
|
+
'nocturnal_expired', // event type string
|
|
286
|
+
];
|
|
287
|
+
const missing = markers.filter(m => !content.includes(m));
|
|
288
|
+
if (missing.length > 0) throw new Error(`Missing critical symbols in bundle: ${missing.join(', ')}`);
|
|
289
|
+
|
|
290
|
+
return `Bundle OK (${Math.round(content.length / 1024)}KB), all ${markers.length} critical markers present`;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ─────────────────────────────────────────────────────────
|
|
294
|
+
// CHECKPOINT 9: Git state — uncommitted changes that could break pipeline
|
|
295
|
+
// ─────────────────────────────────────────────────────────
|
|
296
|
+
check('9. Git state (uncommitted changes)', () => {
|
|
297
|
+
try {
|
|
298
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8', timeout: 5000, cwd: PLUGIN_DIR }).trim();
|
|
299
|
+
if (!status) return 'Working tree clean';
|
|
300
|
+
const changedFiles = status.split('\n').length;
|
|
301
|
+
return { status: 'warn', detail: `${changedFiles} uncommitted change(s) in plugin directory` };
|
|
302
|
+
} catch {
|
|
303
|
+
return { status: 'warn', detail: 'Could not check git status' };
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ─────────────────────────────────────────────────────────
|
|
308
|
+
// CHECKPOINT 10: Pain flag state
|
|
309
|
+
// ─────────────────────────────────────────────────────────
|
|
310
|
+
check('10. Pain flag state', () => {
|
|
311
|
+
const painFlagPath = join(stateDir, '.pain_flag');
|
|
312
|
+
if (!existsSync(painFlagPath)) {
|
|
313
|
+
return 'No active pain flag';
|
|
314
|
+
}
|
|
315
|
+
const content = readFileSync(painFlagPath, 'utf-8');
|
|
316
|
+
const lines = content.split('\n');
|
|
317
|
+
const fields = {};
|
|
318
|
+
for (const line of lines) {
|
|
319
|
+
const colonIdx = line.indexOf(':');
|
|
320
|
+
if (colonIdx > 0) {
|
|
321
|
+
fields[line.substring(0, colonIdx).trim()] = line.substring(colonIdx + 1).trim();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Accept either the legacy contract (fields.score + fields.reason)
|
|
325
|
+
// or the current contract (active.source.score)
|
|
326
|
+
const legacy = fields.score && fields.reason;
|
|
327
|
+
const current = fields.active && fields.source && fields.score;
|
|
328
|
+
if (!legacy && !current) {
|
|
329
|
+
return { status: 'warn', detail: 'Pain flag exists but is missing required fields (need score + reason, or active.source.score)' };
|
|
330
|
+
}
|
|
331
|
+
return `Pain flag active (score: ${fields.score}, source: ${fields.source || fields.active?.source || 'unknown'}, session: ${fields.session_id || 'none'})`;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ─────────────────────────────────────────────────────────
|
|
335
|
+
// CHECKPOINT 11: Trajectory data
|
|
336
|
+
// ─────────────────────────────────────────────────────────
|
|
337
|
+
check('11. Trajectory data availability', () => {
|
|
338
|
+
const trajectoryPath = join(stateDir, 'trajectory.json');
|
|
339
|
+
const trajectoryDir = join(stateDir, 'trajectory');
|
|
340
|
+
const trajectoryDb = join(stateDir, 'trajectory.db');
|
|
341
|
+
if (!existsSync(trajectoryPath) && !existsSync(trajectoryDir) && !existsSync(trajectoryDb)) {
|
|
342
|
+
return { status: 'warn', detail: 'No trajectory data — snapshot extraction will use pain context fallback or fail' };
|
|
343
|
+
}
|
|
344
|
+
if (existsSync(trajectoryDb)) {
|
|
345
|
+
const stat = statSync(trajectoryDb);
|
|
346
|
+
return `Trajectory SQLite database present (${Math.round(stat.size / 1024)}KB)`;
|
|
347
|
+
}
|
|
348
|
+
// Check trajectory content
|
|
349
|
+
if (existsSync(trajectoryPath)) {
|
|
350
|
+
try {
|
|
351
|
+
const data = JSON.parse(readFileSync(trajectoryPath, 'utf-8'));
|
|
352
|
+
const entryCount = Array.isArray(data) ? data.length : Object.keys(data).length;
|
|
353
|
+
return `${entryCount} trajectory entries available`;
|
|
354
|
+
} catch {
|
|
355
|
+
return { status: 'warn', detail: 'trajectory.json exists but is corrupted' };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (existsSync(trajectoryDir)) {
|
|
359
|
+
const files = readdirSync(trajectoryDir).filter(f => f.endsWith('.json'));
|
|
360
|
+
return `${files.length} trajectory file(s) available`;
|
|
361
|
+
}
|
|
362
|
+
return { status: 'warn', detail: 'Trajectory storage not found in expected locations' };
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ─────────────────────────────────────────────────────────
|
|
366
|
+
// CHECKPOINT 12: Principle training state
|
|
367
|
+
// ─────────────────────────────────────────────────────────
|
|
368
|
+
check('12. Principle training state', () => {
|
|
369
|
+
// Check multiple possible locations
|
|
370
|
+
const candidates = [
|
|
371
|
+
join(stateDir, 'nocturnal', 'training_store.json'),
|
|
372
|
+
join(stateDir, 'principle_training_state.json'),
|
|
373
|
+
];
|
|
374
|
+
let trainingPath = null;
|
|
375
|
+
for (const c of candidates) {
|
|
376
|
+
if (existsSync(c)) { trainingPath = c; break; }
|
|
377
|
+
}
|
|
378
|
+
if (!trainingPath) {
|
|
379
|
+
return { status: 'warn', detail: 'No training_store.json or principle_training_state.json — NocturnalTargetSelector may not find evaluable principles' };
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const store = JSON.parse(readFileSync(trainingPath, 'utf-8'));
|
|
383
|
+
const principles = Object.keys(store.principles || store);
|
|
384
|
+
if (principles.length === 0) {
|
|
385
|
+
return { status: 'warn', detail: 'Training store exists but has no principles' };
|
|
386
|
+
}
|
|
387
|
+
const evaluable = principles.filter(p => {
|
|
388
|
+
const pr = store.principles ? store.principles[p] : store[p];
|
|
389
|
+
return pr && pr.evaluability !== 'manual_only';
|
|
390
|
+
});
|
|
391
|
+
return `${principles.length} principle(s) in training store, ${evaluable.length} evaluable`;
|
|
392
|
+
} catch {
|
|
393
|
+
return { status: 'warn', detail: 'Training store exists but is corrupted' };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
printReport();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
main();
|
|
@@ -71,6 +71,17 @@ async function runWorkflowWatchdog(
|
|
|
71
71
|
for (const wf of staleActive) {
|
|
72
72
|
const ageMin = Math.round((now - wf.created_at) / 60000);
|
|
73
73
|
details.push(`stale_active: ${wf.workflow_id} (${wf.workflow_type}, ${ageMin}min old)`);
|
|
74
|
+
|
|
75
|
+
// #257: Check if the last recorded event reason indicates expected subagent unavailability.
|
|
76
|
+
// If so, skip marking as terminal_error — the workflow is stale because the subagent
|
|
77
|
+
// was expectedly unavailable (daemon mode, process isolation), not due to a hard failure.
|
|
78
|
+
const events = store.getEvents(wf.workflow_id);
|
|
79
|
+
const lastEventReason = events.length > 0 ? events[events.length - 1].reason : 'unknown';
|
|
80
|
+
if (isExpectedSubagentError(lastEventReason)) {
|
|
81
|
+
logger?.debug?.(`[PD:Watchdog] Skipping stale active workflow ${wf.workflow_id}: expected subagent error (${lastEventReason})`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
store.updateWorkflowState(wf.workflow_id, 'terminal_error');
|
|
75
86
|
store.recordEvent(wf.workflow_id, 'watchdog_timeout', 'active', 'terminal_error', `Stale active > ${staleThreshold / 60000}s`, { ageMs: now - wf.created_at });
|
|
76
87
|
|
|
@@ -992,6 +1003,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
992
1003
|
logger: api?.logger || logger,
|
|
993
1004
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: api is guaranteed non-null in this recovery path where runtimeAdapter is required
|
|
994
1005
|
runtimeAdapter: new OpenClawTrinityRuntimeAdapter(api!),
|
|
1006
|
+
subagent: api?.runtime?.subagent,
|
|
995
1007
|
});
|
|
996
1008
|
try {
|
|
997
1009
|
// Force-expire this specific workflow regardless of TTL
|
|
@@ -1585,6 +1597,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1585
1597
|
stateDir: wctx.stateDir,
|
|
1586
1598
|
logger: api.logger,
|
|
1587
1599
|
runtimeAdapter: new OpenClawTrinityRuntimeAdapter(api),
|
|
1600
|
+
subagent: api.runtime.subagent,
|
|
1588
1601
|
});
|
|
1589
1602
|
|
|
1590
1603
|
if (!isPollingTask) {
|
|
@@ -2085,6 +2098,7 @@ export const EvolutionWorkerService: ExtendedEvolutionWorkerService = {
|
|
|
2085
2098
|
stateDir: wctx.stateDir,
|
|
2086
2099
|
logger: api.logger,
|
|
2087
2100
|
runtimeAdapter: new OpenClawTrinityRuntimeAdapter(api),
|
|
2101
|
+
subagent: api.runtime.subagent,
|
|
2088
2102
|
});
|
|
2089
2103
|
swept += await nocturnalMgr.sweepExpiredWorkflows(WORKFLOW_TTL_MS, subagentRuntime, agentSession);
|
|
2090
2104
|
nocturnalMgr.dispose();
|
|
@@ -560,7 +560,8 @@ export function checkPreflight(
|
|
|
560
560
|
stateDir: string,
|
|
561
561
|
principleId?: string,
|
|
562
562
|
trajectoryLastActivityAt?: number,
|
|
563
|
-
idleCheckOverride?: IdleCheckResult
|
|
563
|
+
idleCheckOverride?: IdleCheckResult,
|
|
564
|
+
skipGatesForManualTrigger?: boolean
|
|
564
565
|
): PreflightCheckResult {
|
|
565
566
|
const idle = idleCheckOverride ?? checkWorkspaceIdle(workspaceDir, {}, trajectoryLastActivityAt);
|
|
566
567
|
const cooldown = checkCooldown(stateDir, principleId);
|
|
@@ -571,16 +572,20 @@ export function checkPreflight(
|
|
|
571
572
|
blockers.push(`Workspace not idle (active for ${idle.idleForMs}ms, threshold=${DEFAULT_IDLE_THRESHOLD_MS}ms)`);
|
|
572
573
|
}
|
|
573
574
|
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
575
|
+
if (!skipGatesForManualTrigger) {
|
|
576
|
+
if (cooldown.globalCooldownActive) {
|
|
577
|
+
blockers.push(`Global cooldown active until ${cooldown.globalCooldownUntil}`);
|
|
578
|
+
}
|
|
577
579
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
580
|
+
if (cooldown.principleCooldownActive) {
|
|
581
|
+
blockers.push(`Principle cooldown active until ${cooldown.principleCooldownUntil}`);
|
|
582
|
+
}
|
|
581
583
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
+
if (cooldown.quotaExhausted) {
|
|
585
|
+
blockers.push(`Quota exhausted (${DEFAULT_MAX_RUNS_PER_WINDOW} runs per ${DEFAULT_QUOTA_WINDOW_MS / 3600000}h window)`);
|
|
586
|
+
}
|
|
587
|
+
} else if (cooldown.globalCooldownActive || cooldown.principleCooldownActive || cooldown.quotaExhausted) {
|
|
588
|
+
// Log that gates are being bypassed for manual trigger
|
|
584
589
|
}
|
|
585
590
|
|
|
586
591
|
if (idle.abandonedSessionIds.length > 0 && idle.userActiveSessions === 0) {
|
|
@@ -686,7 +686,8 @@ export function executeNocturnalReflection(
|
|
|
686
686
|
stateDir,
|
|
687
687
|
undefined, // principleId
|
|
688
688
|
undefined, // trajectoryLastActivityAt
|
|
689
|
-
options.idleCheckOverride
|
|
689
|
+
options.idleCheckOverride,
|
|
690
|
+
!!options.idleCheckOverride // skip cooldown/quota gates for manual/test triggers
|
|
690
691
|
);
|
|
691
692
|
diagnostics.preflight = preflight;
|
|
692
693
|
|
|
@@ -920,11 +921,15 @@ export function executeNocturnalReflection(
|
|
|
920
921
|
// -------------------------------------------------------------------------
|
|
921
922
|
// Step 6: Arbiter validation
|
|
922
923
|
// -------------------------------------------------------------------------
|
|
924
|
+
// #256: Use 0 for thinkingModelDeltaMin — Trinity chain (Dreamer→Philosopher→Scribe)
|
|
925
|
+
// already ensures quality. A delta of 0 is valid when both bad and better decisions
|
|
926
|
+
// show equally well-reasoned thinking (the Scribe's job is to contrast decisions,
|
|
927
|
+
// not to make one sound more "cognitive" than the other).
|
|
923
928
|
const arbiterResult = parseAndValidateArtifact(rawJson, {
|
|
924
929
|
expectedPrincipleId: selectedPrincipleId,
|
|
925
930
|
expectedSessionId: selectedSessionId,
|
|
926
931
|
qualityThresholds: {
|
|
927
|
-
thinkingModelDeltaMin: 0
|
|
932
|
+
thinkingModelDeltaMin: 0,
|
|
928
933
|
planningRatioGainMin: -0.5,
|
|
929
934
|
},
|
|
930
935
|
});
|
|
@@ -1153,7 +1158,8 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1153
1158
|
stateDir,
|
|
1154
1159
|
undefined,
|
|
1155
1160
|
undefined,
|
|
1156
|
-
options.idleCheckOverride
|
|
1161
|
+
options.idleCheckOverride,
|
|
1162
|
+
!!options.idleCheckOverride // skip cooldown/quota gates for manual/test triggers
|
|
1157
1163
|
);
|
|
1158
1164
|
diagnostics.preflight = preflight;
|
|
1159
1165
|
|
|
@@ -1354,11 +1360,12 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1354
1360
|
}
|
|
1355
1361
|
|
|
1356
1362
|
// Step 5: Arbiter validation
|
|
1363
|
+
// #256: Use 0 for thinkingModelDeltaMin — Trinity chain already ensures quality
|
|
1357
1364
|
const arbiterResult = parseAndValidateArtifact(rawJson, {
|
|
1358
1365
|
expectedPrincipleId: selectedPrincipleId,
|
|
1359
1366
|
expectedSessionId: selectedSessionId,
|
|
1360
1367
|
qualityThresholds: {
|
|
1361
|
-
thinkingModelDeltaMin: 0
|
|
1368
|
+
thinkingModelDeltaMin: 0,
|
|
1362
1369
|
planningRatioGainMin: -0.5,
|
|
1363
1370
|
},
|
|
1364
1371
|
});
|
|
@@ -347,11 +347,13 @@ export class NocturnalTargetSelector {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
// Step 2: Cooldown and quota check
|
|
350
|
+
// #256: Skip cooldown/quota for manual/test triggers (idleCheckOverride present)
|
|
351
|
+
const skipGates = !!this.idleCheckOverride;
|
|
350
352
|
const cooldownResult = checkCooldown(this.stateDir);
|
|
351
353
|
diagnostics.cooldownCheckPassed = !cooldownResult.globalCooldownActive;
|
|
352
354
|
diagnostics.quotaCheckPassed = !cooldownResult.quotaExhausted;
|
|
353
355
|
|
|
354
|
-
if (cooldownResult.globalCooldownActive) {
|
|
356
|
+
if (!skipGates && cooldownResult.globalCooldownActive) {
|
|
355
357
|
return {
|
|
356
358
|
decision: 'skip',
|
|
357
359
|
skipReason: 'global_cooldown_active',
|
|
@@ -359,7 +361,7 @@ export class NocturnalTargetSelector {
|
|
|
359
361
|
};
|
|
360
362
|
}
|
|
361
363
|
|
|
362
|
-
if (cooldownResult.quotaExhausted) {
|
|
364
|
+
if (!skipGates && cooldownResult.quotaExhausted) {
|
|
363
365
|
return {
|
|
364
366
|
decision: 'skip',
|
|
365
367
|
skipReason: 'quota_exhausted',
|
|
@@ -41,6 +41,7 @@ import type { RecentPainContext } from '../evolution-worker.js';
|
|
|
41
41
|
import * as fs from 'fs';
|
|
42
42
|
import * as path from 'path';
|
|
43
43
|
import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
|
|
44
|
+
import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
|
|
44
45
|
|
|
45
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
47
|
// NocturnalResult Type Alias
|
|
@@ -65,6 +66,8 @@ export interface NocturnalWorkflowOptions {
|
|
|
65
66
|
logger: PluginLogger;
|
|
66
67
|
/** Trinity runtime adapter for subagent execution */
|
|
67
68
|
runtimeAdapter: TrinityRuntimeAdapter;
|
|
69
|
+
/** Subagent runtime for availability probing (#254) */
|
|
70
|
+
subagent?: { run?: unknown };
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -138,6 +141,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
138
141
|
private readonly stateDir: string;
|
|
139
142
|
private readonly logger: PluginLogger;
|
|
140
143
|
private readonly runtimeAdapter: TrinityRuntimeAdapter;
|
|
144
|
+
private readonly subagent: { run?: unknown } | undefined;
|
|
141
145
|
private readonly store: WorkflowStore;
|
|
142
146
|
|
|
143
147
|
/** Tracks completion timestamps for idempotency */
|
|
@@ -156,6 +160,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
156
160
|
this.stateDir = opts.stateDir;
|
|
157
161
|
this.logger = opts.logger;
|
|
158
162
|
this.runtimeAdapter = opts.runtimeAdapter;
|
|
163
|
+
this.subagent = opts.subagent;
|
|
159
164
|
this.store = new WorkflowStore({ workspaceDir: opts.workspaceDir });
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -172,8 +177,9 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
172
177
|
metadata?: Record<string, unknown>;
|
|
173
178
|
}
|
|
174
179
|
): Promise<WorkflowHandle> {
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
// #254: Use isSubagentRuntimeAvailable instead of runtimeAdapter.isRuntimeAvailable()
|
|
181
|
+
// (which always returns true in OpenClawTrinityRuntimeAdapter)
|
|
182
|
+
if (!isSubagentRuntimeAvailable(this.subagent)) {
|
|
177
183
|
this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable, skipping workflow`);
|
|
178
184
|
throw new Error(`NocturnalWorkflowManager: subagent runtime unavailable`);
|
|
179
185
|
}
|