principles-disciple 1.7.6 → 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 +2 -9
- 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/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/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/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 +95 -1
- package/dist/core/trajectory.js +220 -6
- package/dist/core/workspace-context.d.ts +0 -6
- package/dist/core/workspace-context.js +0 -12
- package/dist/hooks/bash-risk.d.ts +6 -6
- package/dist/hooks/bash-risk.js +8 -8
- package/dist/hooks/gate-block-helper.js +1 -1
- package/dist/hooks/gate.d.ts +1 -1
- package/dist/hooks/gate.js +2 -2
- package/dist/hooks/gfi-gate.d.ts +3 -3
- package/dist/hooks/gfi-gate.js +15 -14
- package/dist/hooks/pain.js +6 -9
- package/dist/hooks/progressive-trust-gate.d.ts +21 -49
- package/dist/hooks/progressive-trust-gate.js +51 -204
- package/dist/hooks/prompt.d.ts +11 -11
- package/dist/hooks/prompt.js +158 -72
- package/dist/hooks/subagent.js +43 -6
- package/dist/i18n/commands.js +8 -8
- package/dist/index.js +129 -28
- package/dist/service/evolution-worker.d.ts +42 -4
- package/dist/service/evolution-worker.js +321 -13
- 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 +2 -23
- package/dist/service/phase3-input-filter.js +3 -27
- package/dist/service/runtime-summary-service.d.ts +0 -10
- package/dist/service/runtime-summary-service.js +1 -54
- package/dist/tools/deep-reflect.js +2 -1
- package/dist/types/event-types.d.ts +2 -10
- package/dist/types/runtime-summary.d.ts +1 -8
- 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
|
@@ -0,0 +1,352 @@
|
|
|
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
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { listSessions } from '../core/session-tracker.js';
|
|
23
|
+
import { withLockAsync } from '../utils/file-lock.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** File name for nocturnal runtime bookkeeping */
|
|
28
|
+
export const NOCTURNAL_RUNTIME_FILE = 'nocturnal-runtime.json';
|
|
29
|
+
/** Default idle threshold: workspace is considered idle if no activity for this duration (ms) */
|
|
30
|
+
export const DEFAULT_IDLE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
31
|
+
/** Default cooldown between nocturnal runs (ms) */
|
|
32
|
+
export const DEFAULT_GLOBAL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
|
|
33
|
+
/** Default per-principle cooldown (ms) */
|
|
34
|
+
export const DEFAULT_PRINCIPLE_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
35
|
+
/** Default maximum nocturnal runs per quota window */
|
|
36
|
+
export const DEFAULT_MAX_RUNS_PER_WINDOW = 3;
|
|
37
|
+
/** Default quota window size (ms) */
|
|
38
|
+
export const DEFAULT_QUOTA_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
39
|
+
/** Abandoned session threshold: sessions inactive for longer than this are ignored (ms) */
|
|
40
|
+
export const DEFAULT_ABANDONED_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Default State
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
function createDefaultState() {
|
|
45
|
+
return {
|
|
46
|
+
principleCooldowns: {},
|
|
47
|
+
recentRunTimestamps: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// File Operations (with locking)
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
async function readState(stateDir) {
|
|
54
|
+
const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
|
|
55
|
+
if (!fs.existsSync(filePath)) {
|
|
56
|
+
return createDefaultState();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
// Ensure required fields exist (migration-safe)
|
|
62
|
+
return {
|
|
63
|
+
principleCooldowns: parsed.principleCooldowns ?? {},
|
|
64
|
+
recentRunTimestamps: parsed.recentRunTimestamps ?? [],
|
|
65
|
+
lastRunAt: parsed.lastRunAt,
|
|
66
|
+
lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
|
|
67
|
+
globalCooldownUntil: parsed.globalCooldownUntil,
|
|
68
|
+
lastRunMeta: parsed.lastRunMeta,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Corrupted file — start fresh
|
|
73
|
+
return createDefaultState();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readStateSync(stateDir) {
|
|
77
|
+
const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
|
|
78
|
+
if (!fs.existsSync(filePath)) {
|
|
79
|
+
return createDefaultState();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
return {
|
|
85
|
+
principleCooldowns: parsed.principleCooldowns ?? {},
|
|
86
|
+
recentRunTimestamps: parsed.recentRunTimestamps ?? [],
|
|
87
|
+
lastRunAt: parsed.lastRunAt,
|
|
88
|
+
lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
|
|
89
|
+
globalCooldownUntil: parsed.globalCooldownUntil,
|
|
90
|
+
lastRunMeta: parsed.lastRunMeta,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.warn(`[nocturnal-runtime] State file corrupted, resetting: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
return createDefaultState();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function writeState(stateDir, state) {
|
|
99
|
+
const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
|
|
100
|
+
const stateDirPath = path.dirname(filePath);
|
|
101
|
+
if (!fs.existsSync(stateDirPath)) {
|
|
102
|
+
fs.mkdirSync(stateDirPath, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
await withLockAsync(filePath, async () => {
|
|
105
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Idle Detection
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Check if the workspace is currently idle based on session activity.
|
|
113
|
+
*
|
|
114
|
+
* IDLE DETERMINATION LOGIC:
|
|
115
|
+
* - Collect all sessions for the workspace via listSessions()
|
|
116
|
+
* - Filter out abandoned sessions (inactive > abandonedThresholdMs)
|
|
117
|
+
* - Workspace is idle if: no active sessions OR all active sessions have lastActivityAt older than idleThresholdMs
|
|
118
|
+
* - Abandoned sessions do NOT contribute to idle determination
|
|
119
|
+
*
|
|
120
|
+
* @param workspaceDir - Workspace directory to check
|
|
121
|
+
* @param options.idleThresholdMs - Consider idle if no activity for this duration (default: 30 min)
|
|
122
|
+
* @param options.abandonedThresholdMs - Consider session abandoned if inactive for this duration (default: 2 hr)
|
|
123
|
+
* @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
|
|
124
|
+
* @returns IdleCheckResult with full diagnostic information
|
|
125
|
+
*/
|
|
126
|
+
export function checkWorkspaceIdle(workspaceDir, options = {}, trajectoryLastActivityAt) {
|
|
127
|
+
const { idleThresholdMs = DEFAULT_IDLE_THRESHOLD_MS, abandonedThresholdMs = DEFAULT_ABANDONED_THRESHOLD_MS, } = options;
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const sessions = listSessions(workspaceDir);
|
|
130
|
+
// Separate active vs abandoned sessions
|
|
131
|
+
const abandonedSessions = [];
|
|
132
|
+
let mostRecentActivityAt = 0;
|
|
133
|
+
let activeSessionCount = 0;
|
|
134
|
+
for (const session of sessions) {
|
|
135
|
+
const inactiveFor = now - session.lastActivityAt;
|
|
136
|
+
if (inactiveFor > abandonedThresholdMs) {
|
|
137
|
+
abandonedSessions.push(session.sessionId);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
activeSessionCount++;
|
|
141
|
+
if (session.lastActivityAt > mostRecentActivityAt) {
|
|
142
|
+
mostRecentActivityAt = session.lastActivityAt;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const idleForMs = mostRecentActivityAt > 0 ? now - mostRecentActivityAt : now;
|
|
147
|
+
const isIdle = mostRecentActivityAt === 0 || idleForMs > idleThresholdMs;
|
|
148
|
+
// Trajectory guardrail: only used as a secondary check
|
|
149
|
+
// If trajectory says there's recent activity but session state says idle,
|
|
150
|
+
// that's a discrepancy we should note but still trust session state as primary
|
|
151
|
+
let trajectoryGuardrailConfirmsIdle = true;
|
|
152
|
+
if (trajectoryLastActivityAt !== undefined) {
|
|
153
|
+
const trajectoryIdleFor = now - trajectoryLastActivityAt;
|
|
154
|
+
// Guardrail confirms if trajectory also shows idle or near-idle (>80% of threshold)
|
|
155
|
+
trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
|
|
156
|
+
}
|
|
157
|
+
let reason;
|
|
158
|
+
if (mostRecentActivityAt === 0) {
|
|
159
|
+
reason = 'No active sessions found — workspace is idle';
|
|
160
|
+
}
|
|
161
|
+
else if (isIdle) {
|
|
162
|
+
reason = `Most recent activity ${idleForMs}ms ago (>${idleThresholdMs}ms threshold)`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
reason = `Recent activity ${idleForMs}ms ago (<${idleThresholdMs}ms threshold)`;
|
|
166
|
+
}
|
|
167
|
+
if (abandonedSessions.length > 0) {
|
|
168
|
+
reason += `; ${abandonedSessions.length} abandoned session(s) ignored`;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
isIdle,
|
|
172
|
+
mostRecentActivityAt,
|
|
173
|
+
idleForMs,
|
|
174
|
+
activeSessionCount,
|
|
175
|
+
abandonedSessionIds: abandonedSessions,
|
|
176
|
+
trajectoryGuardrailConfirmsIdle,
|
|
177
|
+
reason,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Cooldown Management
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Check if the workspace is currently in a cooldown period.
|
|
185
|
+
*
|
|
186
|
+
* @param stateDir - State directory
|
|
187
|
+
* @param principleId - Optional principle ID to check per-principle cooldown
|
|
188
|
+
* @param options - Cooldown configuration options
|
|
189
|
+
* @returns CooldownCheckResult
|
|
190
|
+
*/
|
|
191
|
+
export function checkCooldown(stateDir, principleId, options = {}) {
|
|
192
|
+
const { globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS, principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS, maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW, quotaWindowMs = DEFAULT_QUOTA_WINDOW_MS, } = options;
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const state = readStateSync(stateDir);
|
|
195
|
+
// Global cooldown check
|
|
196
|
+
let globalCooldownActive = false;
|
|
197
|
+
let globalCooldownRemainingMs = 0;
|
|
198
|
+
let globalCooldownUntil = null;
|
|
199
|
+
if (state.globalCooldownUntil) {
|
|
200
|
+
const cooldownEnd = new Date(state.globalCooldownUntil).getTime();
|
|
201
|
+
if (cooldownEnd > now) {
|
|
202
|
+
globalCooldownActive = true;
|
|
203
|
+
globalCooldownRemainingMs = cooldownEnd - now;
|
|
204
|
+
globalCooldownUntil = state.globalCooldownUntil;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Principle-specific cooldown check
|
|
208
|
+
let principleCooldownActive = false;
|
|
209
|
+
let principleCooldownRemainingMs = 0;
|
|
210
|
+
let principleCooldownUntil = null;
|
|
211
|
+
if (principleId && state.principleCooldowns[principleId]) {
|
|
212
|
+
const cooldownEnd = new Date(state.principleCooldowns[principleId]).getTime();
|
|
213
|
+
if (cooldownEnd > now) {
|
|
214
|
+
principleCooldownActive = true;
|
|
215
|
+
principleCooldownRemainingMs = cooldownEnd - now;
|
|
216
|
+
principleCooldownUntil = state.principleCooldowns[principleId];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Quota check: count runs in sliding window
|
|
220
|
+
const windowStart = now - quotaWindowMs;
|
|
221
|
+
const recentRuns = state.recentRunTimestamps
|
|
222
|
+
.map(ts => new Date(ts).getTime())
|
|
223
|
+
.filter(ts => ts > windowStart);
|
|
224
|
+
const quotaExhausted = recentRuns.length >= maxRunsPerWindow;
|
|
225
|
+
const runsRemaining = Math.max(0, maxRunsPerWindow - recentRuns.length);
|
|
226
|
+
return {
|
|
227
|
+
globalCooldownActive,
|
|
228
|
+
globalCooldownUntil,
|
|
229
|
+
globalCooldownRemainingMs,
|
|
230
|
+
principleCooldownActive,
|
|
231
|
+
principleCooldownUntil,
|
|
232
|
+
principleCooldownRemainingMs,
|
|
233
|
+
quotaExhausted,
|
|
234
|
+
runsRemaining,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Record that a nocturnal run has started.
|
|
239
|
+
* Updates global cooldown and quota tracking.
|
|
240
|
+
*
|
|
241
|
+
* @param stateDir - State directory
|
|
242
|
+
* @param principleId - Target principle ID for this run
|
|
243
|
+
*/
|
|
244
|
+
export async function recordRunStart(stateDir, principleId) {
|
|
245
|
+
const state = await readState(stateDir);
|
|
246
|
+
const now = new Date().toISOString();
|
|
247
|
+
state.lastRunAt = now;
|
|
248
|
+
state.lastRunMeta = {
|
|
249
|
+
targetPrincipleId: principleId,
|
|
250
|
+
status: 'skipped', // Will be updated on completion
|
|
251
|
+
};
|
|
252
|
+
// Set global cooldown
|
|
253
|
+
const cooldownUntil = new Date(Date.now() + DEFAULT_GLOBAL_COOLDOWN_MS).toISOString();
|
|
254
|
+
state.globalCooldownUntil = cooldownUntil;
|
|
255
|
+
// Add to recent runs for quota tracking
|
|
256
|
+
state.recentRunTimestamps.push(now);
|
|
257
|
+
// Prune old timestamps outside the quota window
|
|
258
|
+
const windowStart = Date.now() - DEFAULT_QUOTA_WINDOW_MS;
|
|
259
|
+
state.recentRunTimestamps = state.recentRunTimestamps
|
|
260
|
+
.map(ts => new Date(ts).getTime())
|
|
261
|
+
.filter(ts => ts > windowStart)
|
|
262
|
+
.map(ts => new Date(ts).toISOString());
|
|
263
|
+
await writeState(stateDir, state);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Record the outcome of a nocturnal run.
|
|
267
|
+
*
|
|
268
|
+
* @param stateDir - State directory
|
|
269
|
+
* @param outcome - 'success', 'failed', or 'skipped'
|
|
270
|
+
* @param details - Optional details about the run
|
|
271
|
+
*/
|
|
272
|
+
export async function recordRunEnd(stateDir, outcome, details) {
|
|
273
|
+
const state = await readState(stateDir);
|
|
274
|
+
const now = new Date().toISOString();
|
|
275
|
+
if (outcome === 'success') {
|
|
276
|
+
state.lastSuccessfulRunAt = now;
|
|
277
|
+
// Also set per-principle cooldown if we know which principle was targeted
|
|
278
|
+
if (state.lastRunMeta?.targetPrincipleId) {
|
|
279
|
+
const pid = state.lastRunMeta.targetPrincipleId;
|
|
280
|
+
state.principleCooldowns[pid] = new Date(Date.now() + DEFAULT_PRINCIPLE_COOLDOWN_MS).toISOString();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Update run metadata
|
|
284
|
+
state.lastRunMeta = {
|
|
285
|
+
...state.lastRunMeta,
|
|
286
|
+
status: outcome,
|
|
287
|
+
sampleCount: details?.sampleCount ?? state.lastRunMeta?.sampleCount,
|
|
288
|
+
reason: details?.reason ?? state.lastRunMeta?.reason,
|
|
289
|
+
};
|
|
290
|
+
// Note: global cooldown remains active (set at run start) - we don't clear it on failure
|
|
291
|
+
// This prevents rapid retry loops
|
|
292
|
+
await writeState(stateDir, state);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Clear all cooldowns (for testing or admin reset).
|
|
296
|
+
*
|
|
297
|
+
* @param stateDir - State directory
|
|
298
|
+
*/
|
|
299
|
+
export async function clearAllCooldowns(stateDir) {
|
|
300
|
+
const state = await readState(stateDir);
|
|
301
|
+
state.globalCooldownUntil = undefined;
|
|
302
|
+
state.principleCooldowns = {};
|
|
303
|
+
state.recentRunTimestamps = [];
|
|
304
|
+
state.lastRunMeta = undefined;
|
|
305
|
+
await writeState(stateDir, state);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get the current runtime state (for debugging/inspection).
|
|
309
|
+
*
|
|
310
|
+
* @param stateDir - State directory
|
|
311
|
+
* @returns The current NocturnalRuntimeState
|
|
312
|
+
*/
|
|
313
|
+
export async function getRuntimeState(stateDir) {
|
|
314
|
+
return readState(stateDir);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Combined pre-flight check for whether a nocturnal run should proceed.
|
|
318
|
+
* Integrates idle + cooldown + quota checks.
|
|
319
|
+
*
|
|
320
|
+
* @param workspaceDir - Workspace directory
|
|
321
|
+
* @param stateDir - State directory
|
|
322
|
+
* @param principleId - Target principle ID
|
|
323
|
+
* @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
|
|
324
|
+
* @param idleCheckOverride - Optional override for idle check result (for testing)
|
|
325
|
+
*/
|
|
326
|
+
export function checkPreflight(workspaceDir, stateDir, principleId, trajectoryLastActivityAt, idleCheckOverride) {
|
|
327
|
+
const idle = idleCheckOverride ?? checkWorkspaceIdle(workspaceDir, {}, trajectoryLastActivityAt);
|
|
328
|
+
const cooldown = checkCooldown(stateDir, principleId);
|
|
329
|
+
const blockers = [];
|
|
330
|
+
if (!idle.isIdle) {
|
|
331
|
+
blockers.push(`Workspace not idle (active for ${idle.idleForMs}ms, threshold=${DEFAULT_IDLE_THRESHOLD_MS}ms)`);
|
|
332
|
+
}
|
|
333
|
+
if (cooldown.globalCooldownActive) {
|
|
334
|
+
blockers.push(`Global cooldown active until ${cooldown.globalCooldownUntil}`);
|
|
335
|
+
}
|
|
336
|
+
if (cooldown.principleCooldownActive) {
|
|
337
|
+
blockers.push(`Principle cooldown active until ${cooldown.principleCooldownUntil}`);
|
|
338
|
+
}
|
|
339
|
+
if (cooldown.quotaExhausted) {
|
|
340
|
+
blockers.push(`Quota exhausted (${DEFAULT_MAX_RUNS_PER_WINDOW} runs per ${DEFAULT_QUOTA_WINDOW_MS / 3600000}h window)`);
|
|
341
|
+
}
|
|
342
|
+
if (idle.abandonedSessionIds.length > 0 && idle.activeSessionCount === 0) {
|
|
343
|
+
// Only block if ALL sessions are abandoned (meaning workspace truly has no activity)
|
|
344
|
+
// If some sessions are active, we trust the session-based idle check
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
canRun: blockers.length === 0,
|
|
348
|
+
idle,
|
|
349
|
+
cooldown,
|
|
350
|
+
blockers,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nocturnal Service — Trinity Reflection Pipeline Orchestrator
|
|
3
|
+
* ============================================================
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: Orchestrate the complete nocturnal reflection pipeline:
|
|
6
|
+
* 1. Workspace idle check
|
|
7
|
+
* 2. Target selection (principle + session)
|
|
8
|
+
* 3. Trajectory snapshot extraction
|
|
9
|
+
* 4. Trinity artifact generation (Dreamer -> Philosopher -> Scribe)
|
|
10
|
+
* OR single-reflector fallback (if Trinity disabled or fails)
|
|
11
|
+
* 5. Arbiter validation
|
|
12
|
+
* 6. Executability check
|
|
13
|
+
* 7. Artifact persistence
|
|
14
|
+
* 8. Cooldown recording
|
|
15
|
+
*
|
|
16
|
+
* DESIGN CONSTRAINTS (Phase 6):
|
|
17
|
+
* - Trinity is configurable (useTrinity flag)
|
|
18
|
+
* - Single-reflector fallback preserved if Trinity fails
|
|
19
|
+
* - All stage I/O is structured JSON contracts
|
|
20
|
+
* - Any malformed stage output fails the entire chain closed
|
|
21
|
+
* - Final artifact still passes arbiter + executability validation
|
|
22
|
+
* - Telemetry records chain mode, stage outcomes, candidate counts
|
|
23
|
+
* - No real training export (Phase 3+ only)
|
|
24
|
+
* - No auto-deployment
|
|
25
|
+
* - Approved artifacts go to .state/nocturnal/samples/{artifactId}.json
|
|
26
|
+
* - Cooldown recorded via nocturnal-runtime.ts
|
|
27
|
+
*
|
|
28
|
+
* THIS IS THE MAIN ORCHESTRATOR — all other nocturnal modules are called from here.
|
|
29
|
+
*/
|
|
30
|
+
import { type NocturnalSessionSnapshot } from '../core/nocturnal-trajectory-extractor.js';
|
|
31
|
+
import { type NocturnalSelectionResult, type SkipReason } from './nocturnal-target-selector.js';
|
|
32
|
+
import { type NocturnalArtifact, type ArbiterResult } from '../core/nocturnal-arbiter.js';
|
|
33
|
+
import { type TrinityConfig, type TrinityResult, type TrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
|
|
34
|
+
import { type BoundedAction } from '../core/nocturnal-executability.js';
|
|
35
|
+
import { type IdleCheckResult, type PreflightCheckResult } from './nocturnal-runtime.js';
|
|
36
|
+
/**
|
|
37
|
+
* Result of a complete nocturnal reflection run.
|
|
38
|
+
*/
|
|
39
|
+
export interface NocturnalRunResult {
|
|
40
|
+
/** Whether the run produced an approved artifact */
|
|
41
|
+
success: boolean;
|
|
42
|
+
/** The approved artifact (if success === true) */
|
|
43
|
+
artifact?: NocturnalArtifact & {
|
|
44
|
+
boundedAction?: BoundedAction;
|
|
45
|
+
};
|
|
46
|
+
/** Skip reason (if success === false because nothing to do) */
|
|
47
|
+
skipReason?: SkipReason;
|
|
48
|
+
/** Whether the selector found no target */
|
|
49
|
+
noTargetSelected: boolean;
|
|
50
|
+
/** Whether the reflector rejected or artifact failed validation */
|
|
51
|
+
validationFailed: boolean;
|
|
52
|
+
/** Validation failure reasons */
|
|
53
|
+
validationFailures: string[];
|
|
54
|
+
/** Snapshot used for reflection */
|
|
55
|
+
snapshot?: NocturnalSessionSnapshot;
|
|
56
|
+
/** Diagnostics from each pipeline stage */
|
|
57
|
+
diagnostics: NocturnalRunDiagnostics;
|
|
58
|
+
/** Trinity telemetry (if Trinity was used) */
|
|
59
|
+
trinityTelemetry?: TrinityResult['telemetry'];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Diagnostics from each pipeline stage.
|
|
63
|
+
*/
|
|
64
|
+
export interface NocturnalRunDiagnostics {
|
|
65
|
+
/** Pre-flight check result */
|
|
66
|
+
preflight: PreflightCheckResult | null;
|
|
67
|
+
/** Selection result */
|
|
68
|
+
selection: NocturnalSelectionResult | null;
|
|
69
|
+
/** Idle check result */
|
|
70
|
+
idle: IdleCheckResult | null;
|
|
71
|
+
/** Whether Trinity chain was attempted */
|
|
72
|
+
trinityAttempted: boolean;
|
|
73
|
+
/** Trinity result (if trinityAttempted === true) */
|
|
74
|
+
trinityResult: TrinityResult | null;
|
|
75
|
+
/** Which chain mode was used */
|
|
76
|
+
chainModeUsed: 'trinity' | 'single-reflector' | null;
|
|
77
|
+
/** Arbiter validation result */
|
|
78
|
+
arbiterResult: ArbiterResult | null;
|
|
79
|
+
/** Executability validation result (if arbiter passed) */
|
|
80
|
+
executabilityResult: {
|
|
81
|
+
executable: boolean;
|
|
82
|
+
failures: string[];
|
|
83
|
+
} | null;
|
|
84
|
+
/** Whether artifact was persisted */
|
|
85
|
+
persisted: boolean;
|
|
86
|
+
/** Persistence path (if persisted) */
|
|
87
|
+
persistedPath?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Configuration for the nocturnal service.
|
|
91
|
+
*/
|
|
92
|
+
export interface NocturnalServiceOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Whether to skip the reflector (for testing arbiter/executability in isolation).
|
|
95
|
+
* Default: false (reflector runs normally).
|
|
96
|
+
*/
|
|
97
|
+
skipReflector?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Override the reflector output (for testing).
|
|
100
|
+
* If provided, this JSON string is used instead of calling the stub reflector.
|
|
101
|
+
*/
|
|
102
|
+
reflectorOutputOverride?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Override idle check (for testing).
|
|
105
|
+
* If provided, this result is used instead of calling checkWorkspaceIdle.
|
|
106
|
+
*/
|
|
107
|
+
idleCheckOverride?: IdleCheckResult;
|
|
108
|
+
/**
|
|
109
|
+
* Trinity chain configuration.
|
|
110
|
+
* Default: { useTrinity: true, maxCandidates: 3, useStubs: false }
|
|
111
|
+
*/
|
|
112
|
+
trinityConfig?: Partial<TrinityConfig>;
|
|
113
|
+
/**
|
|
114
|
+
* Runtime adapter for real subagent execution.
|
|
115
|
+
* When provided, Trinity stages are invoked via the adapter's async methods.
|
|
116
|
+
* Ignored when trinityConfig.useStubs is true.
|
|
117
|
+
*/
|
|
118
|
+
runtimeAdapter?: TrinityRuntimeAdapter;
|
|
119
|
+
/**
|
|
120
|
+
* Override the Trinity result (for testing).
|
|
121
|
+
* If provided, this result is used instead of running the Trinity chain.
|
|
122
|
+
*/
|
|
123
|
+
trinityResultOverride?: TrinityResult;
|
|
124
|
+
/**
|
|
125
|
+
* Recent pain context from the evolution queue.
|
|
126
|
+
* When provided, the target selector uses it for ranking bias and diagnostics enrichment.
|
|
127
|
+
* This threads recent pain signals into sleep_reflection targeting without merging task kinds.
|
|
128
|
+
*/
|
|
129
|
+
painContext?: import('../service/evolution-worker.js').RecentPainContext;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Execute a complete nocturnal reflection run.
|
|
133
|
+
*
|
|
134
|
+
* Pipeline:
|
|
135
|
+
* 1. Pre-flight check (idle + cooldown + quota)
|
|
136
|
+
* 2. Target selection (principle + violating session)
|
|
137
|
+
* 3. Trajectory snapshot extraction
|
|
138
|
+
* 4. Reflector (stub) → JSON artifact
|
|
139
|
+
* 5. Arbiter validation
|
|
140
|
+
* 6. Executability check
|
|
141
|
+
* 7. Artifact persistence
|
|
142
|
+
* 8. Cooldown recording
|
|
143
|
+
*
|
|
144
|
+
* @param workspaceDir - Workspace directory
|
|
145
|
+
* @param stateDir - State directory
|
|
146
|
+
* @param options - Service configuration options
|
|
147
|
+
* @returns NocturnalRunResult
|
|
148
|
+
*/
|
|
149
|
+
export declare function executeNocturnalReflection(workspaceDir: string, stateDir: string, options?: NocturnalServiceOptions): NocturnalRunResult;
|
|
150
|
+
/**
|
|
151
|
+
* Async wrapper for executeNocturnalReflection.
|
|
152
|
+
* When runtimeAdapter is provided in options, uses runTrinityAsync for real subagent execution.
|
|
153
|
+
* Otherwise falls back to synchronous executeNocturnalReflection.
|
|
154
|
+
*/
|
|
155
|
+
export declare function executeNocturnalReflectionAsync(workspaceDir: string, stateDir: string, options?: NocturnalServiceOptions): Promise<NocturnalRunResult>;
|
|
156
|
+
/**
|
|
157
|
+
* List all approved nocturnal artifacts for a workspace.
|
|
158
|
+
* Returns artifacts sorted by createdAt (newest first).
|
|
159
|
+
*/
|
|
160
|
+
export declare function listApprovedNocturnalArtifacts(workspaceDir: string): Array<NocturnalArtifact & {
|
|
161
|
+
persistedAt: string;
|
|
162
|
+
boundedAction?: BoundedAction;
|
|
163
|
+
}>;
|