principles-disciple 1.8.0 → 1.8.1
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/core/config.d.ts +2 -0
- package/dist/core/session-tracker.d.ts +3 -1
- package/dist/core/session-tracker.js +12 -3
- package/dist/hooks/llm.d.ts +1 -0
- package/dist/hooks/llm.js +17 -70
- package/dist/hooks/progressive-trust-gate.d.ts +1 -0
- package/dist/hooks/progressive-trust-gate.js +45 -0
- package/dist/hooks/prompt.d.ts +2 -0
- package/dist/hooks/prompt.js +27 -6
- package/dist/hooks/subagent.js +2 -2
- package/dist/http/principles-console-route.js +114 -0
- package/dist/service/empathy-observer-manager.d.ts +46 -10
- package/dist/service/empathy-observer-manager.js +249 -64
- package/dist/service/evolution-worker.js +1 -0
- package/dist/service/health-query-service.d.ts +170 -0
- package/dist/service/health-query-service.js +662 -0
- package/dist/service/nocturnal-runtime.d.ts +2 -2
- package/dist/service/nocturnal-runtime.js +75 -4
- package/dist/service/nocturnal-service.js +2 -2
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.d.ts +48 -0
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.js +480 -0
- package/dist/service/subagent-workflow/index.d.ts +4 -0
- package/dist/service/subagent-workflow/index.js +3 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.d.ts +77 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.js +75 -0
- package/dist/service/subagent-workflow/types.d.ts +259 -0
- package/dist/service/subagent-workflow/types.js +11 -0
- package/dist/service/subagent-workflow/workflow-store.d.ts +26 -0
- package/dist/service/subagent-workflow/workflow-store.js +165 -0
- package/dist/tools/deep-reflect.js +2 -2
- package/openclaw.plugin.json +6 -1
- package/package.json +3 -3
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { readPainFlagData } from '../core/pain.js';
|
|
4
|
+
import { resolvePdPath } from '../core/paths.js';
|
|
5
|
+
import { listSessions } from '../core/session-tracker.js';
|
|
6
|
+
import { listDeployments } from '../core/model-deployment-registry.js';
|
|
7
|
+
import { ControlUiDatabase } from '../core/control-ui-db.js';
|
|
8
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
|
+
export class HealthQueryService {
|
|
10
|
+
workspaceDir;
|
|
11
|
+
stateDir;
|
|
12
|
+
trajectory;
|
|
13
|
+
config;
|
|
14
|
+
eventLog;
|
|
15
|
+
evolutionReducer;
|
|
16
|
+
uiDb;
|
|
17
|
+
tableColumnCache = new Map();
|
|
18
|
+
constructor(workspaceDir) {
|
|
19
|
+
this.workspaceDir = workspaceDir;
|
|
20
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
21
|
+
this.stateDir = wctx.stateDir;
|
|
22
|
+
this.trajectory = wctx.trajectory;
|
|
23
|
+
this.config = wctx.config;
|
|
24
|
+
this.eventLog = wctx.eventLog;
|
|
25
|
+
this.evolutionReducer = wctx.evolutionReducer;
|
|
26
|
+
this.uiDb = new ControlUiDatabase({ workspaceDir });
|
|
27
|
+
}
|
|
28
|
+
dispose() {
|
|
29
|
+
this.uiDb.dispose();
|
|
30
|
+
}
|
|
31
|
+
getOverviewHealth() {
|
|
32
|
+
const session = this.getCurrentSession();
|
|
33
|
+
const threshold = this.getGfiThreshold();
|
|
34
|
+
const trust = this.readTrust();
|
|
35
|
+
const evolution = this.readEvolutionScore();
|
|
36
|
+
const reducerStats = this.evolutionReducer.getStats();
|
|
37
|
+
const queue = this.readQueueStats();
|
|
38
|
+
const painFlag = this.readPainFlag();
|
|
39
|
+
const currentGfi = this.asNumber(session?.currentGfi, 0);
|
|
40
|
+
const peakToday = this.asNumber(session?.dailyGfiPeak, currentGfi);
|
|
41
|
+
return {
|
|
42
|
+
gfi: {
|
|
43
|
+
current: currentGfi,
|
|
44
|
+
peakToday,
|
|
45
|
+
threshold,
|
|
46
|
+
},
|
|
47
|
+
trust,
|
|
48
|
+
evolution,
|
|
49
|
+
painFlag,
|
|
50
|
+
principles: {
|
|
51
|
+
candidate: reducerStats.candidateCount,
|
|
52
|
+
probation: reducerStats.probationCount,
|
|
53
|
+
active: reducerStats.activeCount,
|
|
54
|
+
deprecated: reducerStats.deprecatedCount,
|
|
55
|
+
},
|
|
56
|
+
queue,
|
|
57
|
+
activeStage: this.computeHealthStage(currentGfi, threshold, painFlag.active),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
getEvolutionPrinciples() {
|
|
61
|
+
const stats = this.evolutionReducer.getStats();
|
|
62
|
+
const recent = this.readRecentPrincipleChanges(30);
|
|
63
|
+
const nocturnal = this.readNocturnalTraining();
|
|
64
|
+
const painSourceDistribution = this.readPainSourceDistribution();
|
|
65
|
+
const activeStage = this.readEvolutionActiveStage(nocturnal.queue);
|
|
66
|
+
return {
|
|
67
|
+
principles: {
|
|
68
|
+
summary: {
|
|
69
|
+
candidate: stats.candidateCount,
|
|
70
|
+
probation: stats.probationCount,
|
|
71
|
+
active: stats.activeCount,
|
|
72
|
+
deprecated: stats.deprecatedCount,
|
|
73
|
+
},
|
|
74
|
+
recent,
|
|
75
|
+
},
|
|
76
|
+
nocturnalTraining: nocturnal,
|
|
77
|
+
painSourceDistribution,
|
|
78
|
+
activeStage,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
getFeedbackGfi() {
|
|
82
|
+
const session = this.getCurrentSession();
|
|
83
|
+
const threshold = this.getGfiThreshold();
|
|
84
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
85
|
+
const trendRows = this.uiDb.all(`
|
|
86
|
+
SELECT substr(created_at, 1, 13) || ':00:00Z' AS hour, ROUND(SUM(score), 2) AS value
|
|
87
|
+
FROM pain_events
|
|
88
|
+
WHERE substr(created_at, 1, 10) = ?
|
|
89
|
+
GROUP BY substr(created_at, 1, 13)
|
|
90
|
+
ORDER BY hour ASC
|
|
91
|
+
`, today);
|
|
92
|
+
const sourceRows = this.uiDb.all(`
|
|
93
|
+
SELECT source, COUNT(*) AS total
|
|
94
|
+
FROM pain_events
|
|
95
|
+
WHERE substr(created_at, 1, 10) = ?
|
|
96
|
+
GROUP BY source
|
|
97
|
+
ORDER BY total DESC
|
|
98
|
+
`, today);
|
|
99
|
+
return {
|
|
100
|
+
current: this.asNumber(session?.currentGfi, 0),
|
|
101
|
+
peakToday: this.asNumber(session?.dailyGfiPeak, this.asNumber(session?.currentGfi, 0)),
|
|
102
|
+
threshold,
|
|
103
|
+
trend: trendRows.map((row) => ({ hour: row.hour, value: this.asNumber(row.value, 0) })),
|
|
104
|
+
sources: Object.fromEntries(sourceRows.map((row) => [row.source, this.asNumber(row.total, 0)])),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
getFeedbackEmpathyEvents(limit = 50) {
|
|
108
|
+
const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
109
|
+
const events = this.readMergedEvents()
|
|
110
|
+
.filter((entry) => entry.type === 'pain_signal' && String(entry.data?.source ?? '') === 'user_empathy')
|
|
111
|
+
.sort((a, b) => (b.ts || '').localeCompare(a.ts || ''))
|
|
112
|
+
.slice(0, safeLimit);
|
|
113
|
+
return events.map((entry) => {
|
|
114
|
+
const data = entry.data ?? {};
|
|
115
|
+
return {
|
|
116
|
+
timestamp: String(entry.ts ?? ''),
|
|
117
|
+
severity: typeof data.severity === 'string' ? data.severity : 'mild',
|
|
118
|
+
score: this.asNumber(data.score, 0),
|
|
119
|
+
reason: typeof data.reason === 'string' ? data.reason : '',
|
|
120
|
+
origin: typeof data.origin === 'string' ? data.origin : 'unknown',
|
|
121
|
+
gfiAfter: this.asNumber(data.gfiAfter ?? data.gfi_after ?? data.gfi, 0),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
getFeedbackGateBlocks(limit = 50) {
|
|
126
|
+
const trust = this.readTrust();
|
|
127
|
+
const rows = this.readGateBlocksRaw(limit);
|
|
128
|
+
return rows.map((row) => ({
|
|
129
|
+
timestamp: row.created_at,
|
|
130
|
+
toolName: row.tool_name,
|
|
131
|
+
reason: row.reason,
|
|
132
|
+
gfi: this.resolveGateBlockGfi(row),
|
|
133
|
+
trustStage: this.resolveGateBlockTrustStage(row, trust.stage),
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
getGateStats() {
|
|
137
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
138
|
+
const rows = this.uiDb.all(`
|
|
139
|
+
SELECT reason
|
|
140
|
+
FROM gate_blocks
|
|
141
|
+
WHERE substr(created_at, 1, 10) = ?
|
|
142
|
+
`, today);
|
|
143
|
+
let gfiBlocks = 0;
|
|
144
|
+
let stageBlocks = 0;
|
|
145
|
+
let p03Blocks = 0;
|
|
146
|
+
let bypassAttempts = 0;
|
|
147
|
+
let p16Exemptions = 0;
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
const reason = String(row.reason || '').toLowerCase();
|
|
150
|
+
if (reason.includes('gfi'))
|
|
151
|
+
gfiBlocks++;
|
|
152
|
+
if (reason.includes('tier') || reason.includes('stage') || reason.includes('trust'))
|
|
153
|
+
stageBlocks++;
|
|
154
|
+
if (reason.includes('p-03') || reason.includes('edit verification') || reason.includes('oldtext'))
|
|
155
|
+
p03Blocks++;
|
|
156
|
+
if (reason.includes('bypass'))
|
|
157
|
+
bypassAttempts++;
|
|
158
|
+
if (reason.includes('p-16') || reason.includes('exemption'))
|
|
159
|
+
p16Exemptions++;
|
|
160
|
+
}
|
|
161
|
+
const trust = this.readTrust();
|
|
162
|
+
const evolution = this.readEvolutionScore();
|
|
163
|
+
return {
|
|
164
|
+
today: {
|
|
165
|
+
gfiBlocks,
|
|
166
|
+
stageBlocks,
|
|
167
|
+
p03Blocks,
|
|
168
|
+
bypassAttempts,
|
|
169
|
+
p16Exemptions,
|
|
170
|
+
},
|
|
171
|
+
trust: {
|
|
172
|
+
stage: trust.stage,
|
|
173
|
+
score: trust.score,
|
|
174
|
+
status: this.scoreToStatus(trust.score),
|
|
175
|
+
},
|
|
176
|
+
evolution: {
|
|
177
|
+
tier: evolution.tier,
|
|
178
|
+
points: evolution.points,
|
|
179
|
+
status: this.evolutionToStatus(evolution.tier, evolution.points),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
getGateBlocks(limit = 50) {
|
|
184
|
+
const trust = this.readTrust();
|
|
185
|
+
const rows = this.readGateBlocksRaw(limit);
|
|
186
|
+
return rows.map((row) => ({
|
|
187
|
+
timestamp: row.created_at,
|
|
188
|
+
toolName: row.tool_name,
|
|
189
|
+
filePath: row.file_path ?? null,
|
|
190
|
+
reason: row.reason,
|
|
191
|
+
gateType: this.resolveGateType(row),
|
|
192
|
+
gfi: this.resolveGateBlockGfi(row),
|
|
193
|
+
trustStage: this.resolveGateBlockTrustStage(row, trust.stage),
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
readTrust() {
|
|
197
|
+
const scorecardPath = resolvePdPath(this.workspaceDir, 'AGENT_SCORECARD');
|
|
198
|
+
const scorecard = this.readJsonFile(scorecardPath, {});
|
|
199
|
+
const score = this.asNumber(scorecard.trustScore ?? scorecard.trust_score ?? scorecard.score, 0);
|
|
200
|
+
const rawStage = this.asNumber(scorecard.trustStage ?? scorecard.trust_stage ?? scorecard.stage, this.inferTrustStageFromScore(score));
|
|
201
|
+
const stage = Math.max(1, Math.min(4, Math.round(rawStage)));
|
|
202
|
+
return {
|
|
203
|
+
stage,
|
|
204
|
+
stageLabel: this.getTrustStageLabel(stage),
|
|
205
|
+
score,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
readEvolutionScore() {
|
|
209
|
+
const scorecardPath = path.join(this.stateDir, 'evolution-scorecard.json');
|
|
210
|
+
const scorecard = this.readJsonFile(scorecardPath, {});
|
|
211
|
+
const points = this.asNumber(scorecard.totalPoints ?? scorecard.total_points, 0);
|
|
212
|
+
const tierRaw = scorecard.currentTier ?? scorecard.current_tier ?? 1;
|
|
213
|
+
return {
|
|
214
|
+
tier: this.normalizeTierName(tierRaw),
|
|
215
|
+
points,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
readQueueStats() {
|
|
219
|
+
const queuePath = resolvePdPath(this.workspaceDir, 'EVOLUTION_QUEUE');
|
|
220
|
+
const queue = this.readJsonFile(queuePath, []);
|
|
221
|
+
const stats = { pending: 0, inProgress: 0, completed: 0 };
|
|
222
|
+
for (const item of queue) {
|
|
223
|
+
const status = String(item?.status ?? 'pending');
|
|
224
|
+
if (status === 'completed')
|
|
225
|
+
stats.completed++;
|
|
226
|
+
else if (status === 'in_progress')
|
|
227
|
+
stats.inProgress++;
|
|
228
|
+
else
|
|
229
|
+
stats.pending++;
|
|
230
|
+
}
|
|
231
|
+
return stats;
|
|
232
|
+
}
|
|
233
|
+
readPainFlag() {
|
|
234
|
+
const painFlagPath = resolvePdPath(this.workspaceDir, 'PAIN_FLAG');
|
|
235
|
+
if (!fs.existsSync(painFlagPath)) {
|
|
236
|
+
return { active: false, source: null, score: null };
|
|
237
|
+
}
|
|
238
|
+
const data = readPainFlagData(this.workspaceDir);
|
|
239
|
+
return {
|
|
240
|
+
active: true,
|
|
241
|
+
source: typeof data.source === 'string' ? data.source : null,
|
|
242
|
+
score: this.asNullableNumber(data.score),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
getCurrentSession() {
|
|
246
|
+
const sessions = listSessions(this.workspaceDir);
|
|
247
|
+
if (sessions.length === 0)
|
|
248
|
+
return null;
|
|
249
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
250
|
+
const aTs = Number(a.lastControlActivityAt ?? a.lastActivityAt ?? 0);
|
|
251
|
+
const bTs = Number(b.lastControlActivityAt ?? b.lastActivityAt ?? 0);
|
|
252
|
+
return bTs - aTs;
|
|
253
|
+
});
|
|
254
|
+
return sorted[0] ?? null;
|
|
255
|
+
}
|
|
256
|
+
getGfiThreshold() {
|
|
257
|
+
const fromConfig = this.asNullableNumber(this.config.get('gfi_gate.thresholds.low_risk_block'));
|
|
258
|
+
if (fromConfig !== null)
|
|
259
|
+
return fromConfig;
|
|
260
|
+
const fallbackPath = resolvePdPath(this.workspaceDir, 'PAIN_SETTINGS');
|
|
261
|
+
const raw = this.readJsonFile(fallbackPath, {});
|
|
262
|
+
const fallback = this.asNullableNumber((raw.gfi_gate?.thresholds?.low_risk_block));
|
|
263
|
+
return fallback ?? 70;
|
|
264
|
+
}
|
|
265
|
+
computeHealthStage(currentGfi, threshold, painFlagActive) {
|
|
266
|
+
if (painFlagActive || currentGfi >= threshold) {
|
|
267
|
+
return 'critical';
|
|
268
|
+
}
|
|
269
|
+
if (currentGfi >= threshold * 0.7) {
|
|
270
|
+
return 'warning';
|
|
271
|
+
}
|
|
272
|
+
return 'healthy';
|
|
273
|
+
}
|
|
274
|
+
getTrustStageLabel(stage) {
|
|
275
|
+
switch (stage) {
|
|
276
|
+
case 1:
|
|
277
|
+
return 'Observer';
|
|
278
|
+
case 2:
|
|
279
|
+
return 'Editor';
|
|
280
|
+
case 3:
|
|
281
|
+
return 'Developer';
|
|
282
|
+
case 4:
|
|
283
|
+
return 'Architect';
|
|
284
|
+
default:
|
|
285
|
+
return 'Observer';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
inferTrustStageFromScore(score) {
|
|
289
|
+
if (score >= 80)
|
|
290
|
+
return 4;
|
|
291
|
+
if (score >= 60)
|
|
292
|
+
return 3;
|
|
293
|
+
if (score >= 30)
|
|
294
|
+
return 2;
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
normalizeTierName(rawTier) {
|
|
298
|
+
if (typeof rawTier === 'string') {
|
|
299
|
+
const t = rawTier.trim();
|
|
300
|
+
if (t.length > 0)
|
|
301
|
+
return t;
|
|
302
|
+
}
|
|
303
|
+
const n = this.asNumber(rawTier, 1);
|
|
304
|
+
if (n === 1)
|
|
305
|
+
return 'Seed';
|
|
306
|
+
if (n === 2)
|
|
307
|
+
return 'Sprout';
|
|
308
|
+
if (n === 3)
|
|
309
|
+
return 'Sapling';
|
|
310
|
+
if (n === 4)
|
|
311
|
+
return 'Tree';
|
|
312
|
+
if (n === 5)
|
|
313
|
+
return 'Forest';
|
|
314
|
+
return `Tier-${n}`;
|
|
315
|
+
}
|
|
316
|
+
readRecentPrincipleChanges(limit) {
|
|
317
|
+
const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
|
|
318
|
+
if (!fs.existsSync(streamPath))
|
|
319
|
+
return [];
|
|
320
|
+
let lines = [];
|
|
321
|
+
try {
|
|
322
|
+
const raw = fs.readFileSync(streamPath, 'utf8').trim();
|
|
323
|
+
if (!raw)
|
|
324
|
+
return [];
|
|
325
|
+
lines = raw.split('\n');
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const records = [];
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
let event = null;
|
|
333
|
+
try {
|
|
334
|
+
event = JSON.parse(line);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
event = null;
|
|
338
|
+
}
|
|
339
|
+
if (!event?.type || !event.data)
|
|
340
|
+
continue;
|
|
341
|
+
const mapped = this.mapPrincipleEvent(event);
|
|
342
|
+
if (mapped)
|
|
343
|
+
records.push(mapped);
|
|
344
|
+
}
|
|
345
|
+
return records
|
|
346
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
347
|
+
.slice(0, Math.max(1, Math.min(200, limit)));
|
|
348
|
+
}
|
|
349
|
+
mapPrincipleEvent(event) {
|
|
350
|
+
const data = event.data ?? {};
|
|
351
|
+
const type = String(event.type);
|
|
352
|
+
const principleId = typeof data.principleId === 'string' ? data.principleId : '';
|
|
353
|
+
if (!principleId)
|
|
354
|
+
return null;
|
|
355
|
+
const principle = this.evolutionReducer.getPrincipleById(principleId);
|
|
356
|
+
const triggerPattern = typeof data.trigger === 'string'
|
|
357
|
+
? data.trigger
|
|
358
|
+
: typeof principle?.trigger === 'string'
|
|
359
|
+
? principle.trigger
|
|
360
|
+
: '';
|
|
361
|
+
const action = typeof data.action === 'string'
|
|
362
|
+
? data.action
|
|
363
|
+
: typeof principle?.action === 'string'
|
|
364
|
+
? principle.action
|
|
365
|
+
: '';
|
|
366
|
+
if (type === 'candidate_created') {
|
|
367
|
+
const toStatus = typeof data.status === 'string' ? data.status : 'candidate';
|
|
368
|
+
return {
|
|
369
|
+
principleId,
|
|
370
|
+
status: toStatus,
|
|
371
|
+
triggerPattern,
|
|
372
|
+
action,
|
|
373
|
+
fromStatus: 'none',
|
|
374
|
+
toStatus,
|
|
375
|
+
timestamp: String(event.ts ?? ''),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (type === 'principle_promoted') {
|
|
379
|
+
const fromStatus = typeof data.from === 'string' ? data.from : 'candidate';
|
|
380
|
+
const toStatus = typeof data.to === 'string' ? data.to : 'probation';
|
|
381
|
+
return {
|
|
382
|
+
principleId,
|
|
383
|
+
status: toStatus,
|
|
384
|
+
triggerPattern,
|
|
385
|
+
action,
|
|
386
|
+
fromStatus,
|
|
387
|
+
toStatus,
|
|
388
|
+
timestamp: String(event.ts ?? ''),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (type === 'principle_deprecated') {
|
|
392
|
+
return {
|
|
393
|
+
principleId,
|
|
394
|
+
status: 'deprecated',
|
|
395
|
+
triggerPattern,
|
|
396
|
+
action,
|
|
397
|
+
fromStatus: 'active',
|
|
398
|
+
toStatus: 'deprecated',
|
|
399
|
+
timestamp: String(event.ts ?? ''),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (type === 'principle_rolled_back') {
|
|
403
|
+
return {
|
|
404
|
+
principleId,
|
|
405
|
+
status: 'deprecated',
|
|
406
|
+
triggerPattern,
|
|
407
|
+
action,
|
|
408
|
+
fromStatus: 'probation',
|
|
409
|
+
toStatus: 'deprecated',
|
|
410
|
+
timestamp: String(event.ts ?? ''),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
readNocturnalTraining() {
|
|
416
|
+
const sampleDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_SAMPLES_DIR');
|
|
417
|
+
const exportDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_EXPORTS_DIR');
|
|
418
|
+
const reviewQueuePath = path.join(this.stateDir, 'nocturnal', 'review-queue.json');
|
|
419
|
+
const sampleFiles = this.safeListFiles(sampleDir, (name) => name.endsWith('.json') && name !== 'lineage-index.json');
|
|
420
|
+
const samples = sampleFiles.map((filePath) => this.readJsonFile(filePath, {}));
|
|
421
|
+
const queue = { pending: 0, inProgress: 0, completed: 0 };
|
|
422
|
+
for (const sample of samples) {
|
|
423
|
+
const status = String(sample.status ?? '').toLowerCase();
|
|
424
|
+
if (status === 'in_progress' || status === 'running')
|
|
425
|
+
queue.inProgress++;
|
|
426
|
+
else if (status === 'pending' || status === 'pending_review')
|
|
427
|
+
queue.pending++;
|
|
428
|
+
else
|
|
429
|
+
queue.completed++;
|
|
430
|
+
}
|
|
431
|
+
const reviewQueue = this.readJsonFile(reviewQueuePath, []);
|
|
432
|
+
queue.pending += reviewQueue.length;
|
|
433
|
+
const trinityRecords = samples
|
|
434
|
+
.map((sample) => ({
|
|
435
|
+
artifactId: String(sample.artifactId ?? ''),
|
|
436
|
+
status: String(sample.status ?? 'unknown'),
|
|
437
|
+
createdAt: String(sample.createdAt ?? ''),
|
|
438
|
+
}))
|
|
439
|
+
.filter((row) => row.artifactId.length > 0)
|
|
440
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
441
|
+
.slice(0, 20);
|
|
442
|
+
let passed = 0;
|
|
443
|
+
for (const sample of samples) {
|
|
444
|
+
const status = String(sample.status ?? '').toLowerCase();
|
|
445
|
+
const arbiterPassed = sample.arbiter?.passed === true;
|
|
446
|
+
if (arbiterPassed || status === 'approved' || status === 'approved_for_training') {
|
|
447
|
+
passed++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const total = samples.length;
|
|
451
|
+
const arbiterPassRate = total > 0 ? Number((passed / total).toFixed(4)) : 0;
|
|
452
|
+
const exportFiles = this.safeListFiles(exportDir, (name) => name.endsWith('.jsonl'));
|
|
453
|
+
const orpoSampleCount = exportFiles.length;
|
|
454
|
+
const deployments = listDeployments(this.stateDir).map((deployment) => ({
|
|
455
|
+
modelId: deployment.workerProfile,
|
|
456
|
+
status: deployment.routingEnabled ? 'active' : 'inactive',
|
|
457
|
+
checkpointPath: deployment.activeCheckpointId,
|
|
458
|
+
}));
|
|
459
|
+
return {
|
|
460
|
+
queue,
|
|
461
|
+
trinityRecords,
|
|
462
|
+
arbiterPassRate,
|
|
463
|
+
orpoSampleCount,
|
|
464
|
+
deployments,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
readPainSourceDistribution() {
|
|
468
|
+
const rows = this.uiDb.all(`
|
|
469
|
+
SELECT source, COUNT(*) AS total
|
|
470
|
+
FROM pain_events
|
|
471
|
+
GROUP BY source
|
|
472
|
+
ORDER BY total DESC
|
|
473
|
+
`);
|
|
474
|
+
return Object.fromEntries(rows.map((row) => [row.source, this.asNumber(row.total, 0)]));
|
|
475
|
+
}
|
|
476
|
+
readEvolutionActiveStage(queue) {
|
|
477
|
+
const events = this.trajectory.listEvolutionEvents(undefined, { limit: 1, offset: 0 });
|
|
478
|
+
if (events.length > 0) {
|
|
479
|
+
return events[0].stage;
|
|
480
|
+
}
|
|
481
|
+
if (queue.inProgress > 0)
|
|
482
|
+
return 'in_progress';
|
|
483
|
+
if (queue.pending > 0)
|
|
484
|
+
return 'pending';
|
|
485
|
+
if (queue.completed > 0)
|
|
486
|
+
return 'completed';
|
|
487
|
+
return 'idle';
|
|
488
|
+
}
|
|
489
|
+
readMergedEvents() {
|
|
490
|
+
const persistedEvents = this.readPersistedEvents();
|
|
491
|
+
const bufferedEvents = this.getBufferedEvents();
|
|
492
|
+
const merged = new Map();
|
|
493
|
+
for (const entry of [...persistedEvents, ...bufferedEvents]) {
|
|
494
|
+
merged.set(this.getEventDedupKey(entry), entry);
|
|
495
|
+
}
|
|
496
|
+
return [...merged.values()].sort((a, b) => (a.ts || '').localeCompare(b.ts || ''));
|
|
497
|
+
}
|
|
498
|
+
readPersistedEvents() {
|
|
499
|
+
const eventsPath = path.join(this.stateDir, 'logs', 'events.jsonl');
|
|
500
|
+
if (!fs.existsSync(eventsPath))
|
|
501
|
+
return [];
|
|
502
|
+
try {
|
|
503
|
+
const raw = fs.readFileSync(eventsPath, 'utf8').trim();
|
|
504
|
+
if (!raw)
|
|
505
|
+
return [];
|
|
506
|
+
return raw
|
|
507
|
+
.split('\n')
|
|
508
|
+
.map((line) => {
|
|
509
|
+
try {
|
|
510
|
+
return JSON.parse(line);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
.filter((entry) => entry !== null);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
getBufferedEvents() {
|
|
523
|
+
const candidate = this.eventLog;
|
|
524
|
+
if (typeof candidate.getBufferedEvents === 'function') {
|
|
525
|
+
return candidate.getBufferedEvents();
|
|
526
|
+
}
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
getEventDedupKey(entry) {
|
|
530
|
+
const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
|
|
531
|
+
if (eventId) {
|
|
532
|
+
return `${entry.type}:${entry.sessionId ?? 'none'}:${eventId}`;
|
|
533
|
+
}
|
|
534
|
+
return [
|
|
535
|
+
entry.ts ?? 'no-ts',
|
|
536
|
+
entry.type ?? 'no-type',
|
|
537
|
+
entry.category ?? 'no-category',
|
|
538
|
+
entry.sessionId ?? 'no-session',
|
|
539
|
+
typeof entry.data?.source === 'string' ? entry.data.source : 'no-source',
|
|
540
|
+
typeof entry.data?.toolName === 'string' ? entry.data.toolName : 'no-tool',
|
|
541
|
+
typeof entry.data?.reason === 'string' ? entry.data.reason : 'no-reason',
|
|
542
|
+
].join('::');
|
|
543
|
+
}
|
|
544
|
+
readGateBlocksRaw(limit) {
|
|
545
|
+
const safeLimit = Math.max(1, Math.min(1000, Math.floor(limit)));
|
|
546
|
+
const hasGfi = this.hasTableColumn('gate_blocks', 'gfi');
|
|
547
|
+
const hasGfiAfter = this.hasTableColumn('gate_blocks', 'gfi_after');
|
|
548
|
+
const hasTrustStage = this.hasTableColumn('gate_blocks', 'trust_stage');
|
|
549
|
+
const hasGateType = this.hasTableColumn('gate_blocks', 'gate_type');
|
|
550
|
+
const sql = `
|
|
551
|
+
SELECT
|
|
552
|
+
created_at,
|
|
553
|
+
tool_name,
|
|
554
|
+
file_path,
|
|
555
|
+
reason,
|
|
556
|
+
${hasGfi ? 'gfi' : 'NULL'} AS gfi,
|
|
557
|
+
${hasGfiAfter ? 'gfi_after' : 'NULL'} AS gfi_after,
|
|
558
|
+
${hasTrustStage ? 'trust_stage' : 'NULL'} AS trust_stage,
|
|
559
|
+
${hasGateType ? 'gate_type' : 'NULL'} AS gate_type
|
|
560
|
+
FROM gate_blocks
|
|
561
|
+
ORDER BY created_at DESC
|
|
562
|
+
LIMIT ?
|
|
563
|
+
`;
|
|
564
|
+
return this.uiDb.all(sql, safeLimit);
|
|
565
|
+
}
|
|
566
|
+
resolveGateBlockGfi(row) {
|
|
567
|
+
const direct = this.asNullableNumber(row.gfi ?? row.gfi_after);
|
|
568
|
+
if (direct !== null)
|
|
569
|
+
return direct;
|
|
570
|
+
const reason = String(row.reason ?? '');
|
|
571
|
+
const match = reason.match(/gfi\s*[:=]\s*(-?\d+(?:\.\d+)?)/i);
|
|
572
|
+
if (match) {
|
|
573
|
+
return this.asNumber(Number(match[1]), 0);
|
|
574
|
+
}
|
|
575
|
+
const session = this.getCurrentSession();
|
|
576
|
+
return this.asNumber(session?.currentGfi, 0);
|
|
577
|
+
}
|
|
578
|
+
resolveGateBlockTrustStage(row, fallbackStage) {
|
|
579
|
+
const direct = this.asNullableNumber(row.trust_stage);
|
|
580
|
+
if (direct !== null)
|
|
581
|
+
return Math.max(1, Math.min(4, Math.round(direct)));
|
|
582
|
+
const reason = String(row.reason ?? '').toLowerCase();
|
|
583
|
+
const match = reason.match(/stage\s*(\d+)/i);
|
|
584
|
+
if (match) {
|
|
585
|
+
return Math.max(1, Math.min(4, Math.round(this.asNumber(Number(match[1]), fallbackStage))));
|
|
586
|
+
}
|
|
587
|
+
return fallbackStage;
|
|
588
|
+
}
|
|
589
|
+
resolveGateType(row) {
|
|
590
|
+
if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
|
|
591
|
+
return row.gate_type;
|
|
592
|
+
}
|
|
593
|
+
const reason = String(row.reason ?? '').toLowerCase();
|
|
594
|
+
if (reason.includes('gfi'))
|
|
595
|
+
return 'gfi';
|
|
596
|
+
if (reason.includes('tier') || reason.includes('stage') || reason.includes('trust'))
|
|
597
|
+
return 'stage';
|
|
598
|
+
if (reason.includes('p-03') || reason.includes('edit verification') || reason.includes('oldtext'))
|
|
599
|
+
return 'p03';
|
|
600
|
+
if (reason.includes('p-16') || reason.includes('exemption'))
|
|
601
|
+
return 'p16';
|
|
602
|
+
return 'general';
|
|
603
|
+
}
|
|
604
|
+
hasTableColumn(tableName, columnName) {
|
|
605
|
+
let cached = this.tableColumnCache.get(tableName);
|
|
606
|
+
if (!cached) {
|
|
607
|
+
const rows = this.uiDb.all(`PRAGMA table_info(${tableName})`);
|
|
608
|
+
cached = new Set(rows.map((row) => row.name));
|
|
609
|
+
this.tableColumnCache.set(tableName, cached);
|
|
610
|
+
}
|
|
611
|
+
return cached.has(columnName);
|
|
612
|
+
}
|
|
613
|
+
scoreToStatus(score) {
|
|
614
|
+
if (score >= 70)
|
|
615
|
+
return 'healthy';
|
|
616
|
+
if (score >= 40)
|
|
617
|
+
return 'warning';
|
|
618
|
+
return 'critical';
|
|
619
|
+
}
|
|
620
|
+
evolutionToStatus(tier, points) {
|
|
621
|
+
const lower = tier.toLowerCase();
|
|
622
|
+
if (lower === 'forest' || lower === 'tree')
|
|
623
|
+
return 'healthy';
|
|
624
|
+
if (lower === 'sapling' || points >= 200)
|
|
625
|
+
return 'warning';
|
|
626
|
+
return 'critical';
|
|
627
|
+
}
|
|
628
|
+
safeListFiles(dirPath, predicate) {
|
|
629
|
+
if (!fs.existsSync(dirPath))
|
|
630
|
+
return [];
|
|
631
|
+
try {
|
|
632
|
+
return fs.readdirSync(dirPath)
|
|
633
|
+
.filter((name) => predicate(name))
|
|
634
|
+
.map((name) => path.join(dirPath, name));
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
readJsonFile(filePath, fallback) {
|
|
641
|
+
if (!fs.existsSync(filePath))
|
|
642
|
+
return fallback;
|
|
643
|
+
try {
|
|
644
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return fallback;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
asNumber(value, fallback) {
|
|
651
|
+
return Number.isFinite(value) ? Number(value) : fallback;
|
|
652
|
+
}
|
|
653
|
+
asNullableNumber(value) {
|
|
654
|
+
if (Number.isFinite(value))
|
|
655
|
+
return Number(value);
|
|
656
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
657
|
+
const n = Number(value);
|
|
658
|
+
return Number.isFinite(n) ? n : null;
|
|
659
|
+
}
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
@@ -68,8 +68,8 @@ export interface IdleCheckResult {
|
|
|
68
68
|
mostRecentActivityAt: number;
|
|
69
69
|
/** How long since the last activity (ms) */
|
|
70
70
|
idleForMs: number;
|
|
71
|
-
/** Number of active (non-abandoned) sessions found */
|
|
72
|
-
|
|
71
|
+
/** Number of active (non-abandoned) user sessions found */
|
|
72
|
+
userActiveSessions: number;
|
|
73
73
|
/** List of abandoned session IDs (inactive > abandoned threshold) */
|
|
74
74
|
abandonedSessionIds: string[];
|
|
75
75
|
/** Whether trajectory guardrail also confirms idle */
|