principles-disciple 1.19.0 → 1.20.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
CHANGED
package/package.json
CHANGED
|
@@ -14,8 +14,9 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
14
14
|
return typeof value === 'string' && value.trim().length > 0;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/** #246: Stats fields must now be finite numbers — null is no longer accepted. */
|
|
18
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
19
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function validateNocturnalSnapshotIngress(
|
|
@@ -56,20 +57,20 @@ export function validateNocturnalSnapshotIngress(
|
|
|
56
57
|
if (!isObjectRecord(stats)) {
|
|
57
58
|
reasons.push('snapshot.stats must be an object');
|
|
58
59
|
} else {
|
|
59
|
-
if (!
|
|
60
|
-
reasons.push('snapshot.stats.totalAssistantTurns must be a number
|
|
60
|
+
if (!isFiniteNumber(stats.totalAssistantTurns)) {
|
|
61
|
+
reasons.push('snapshot.stats.totalAssistantTurns must be a finite number');
|
|
61
62
|
}
|
|
62
|
-
if (!
|
|
63
|
-
reasons.push('snapshot.stats.totalToolCalls must be a number
|
|
63
|
+
if (!isFiniteNumber(stats.totalToolCalls)) {
|
|
64
|
+
reasons.push('snapshot.stats.totalToolCalls must be a finite number');
|
|
64
65
|
}
|
|
65
|
-
if (
|
|
66
|
+
if (!isFiniteNumber(stats.totalPainEvents)) {
|
|
66
67
|
reasons.push('snapshot.stats.totalPainEvents must be a finite number');
|
|
67
68
|
}
|
|
68
|
-
if (!
|
|
69
|
-
reasons.push('snapshot.stats.totalGateBlocks must be a number
|
|
69
|
+
if (!isFiniteNumber(stats.totalGateBlocks)) {
|
|
70
|
+
reasons.push('snapshot.stats.totalGateBlocks must be a finite number');
|
|
70
71
|
}
|
|
71
|
-
if (!
|
|
72
|
-
reasons.push('snapshot.stats.failureCount must be a number
|
|
72
|
+
if (!isFiniteNumber(stats.failureCount)) {
|
|
73
|
+
reasons.push('snapshot.stats.failureCount must be a finite number');
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -85,21 +86,6 @@ export function validateNocturnalSnapshotIngress(
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
if (!isFallback && isObjectRecord(stats)) {
|
|
89
|
-
if (stats.totalAssistantTurns === null) {
|
|
90
|
-
reasons.push('non-fallback snapshot.stats.totalAssistantTurns must be a number');
|
|
91
|
-
}
|
|
92
|
-
if (stats.totalToolCalls === null) {
|
|
93
|
-
reasons.push('non-fallback snapshot.stats.totalToolCalls must be a number');
|
|
94
|
-
}
|
|
95
|
-
if (stats.totalGateBlocks === null) {
|
|
96
|
-
reasons.push('non-fallback snapshot.stats.totalGateBlocks must be a number');
|
|
97
|
-
}
|
|
98
|
-
if (stats.failureCount === null) {
|
|
99
|
-
reasons.push('non-fallback snapshot.stats.failureCount must be a number');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
89
|
if (reasons.length > 0) {
|
|
104
90
|
return { status: 'invalid', reasons };
|
|
105
91
|
}
|
|
@@ -116,15 +116,18 @@ export interface NocturnalSessionSnapshot {
|
|
|
116
116
|
gateBlocks: NocturnalGateBlock[];
|
|
117
117
|
/**
|
|
118
118
|
* Summary statistics for quick triage.
|
|
119
|
-
*
|
|
120
|
-
* to
|
|
119
|
+
* #246: All fields are now number (never null).
|
|
120
|
+
* Previously null was used to mean "no trajectory data", but this caused
|
|
121
|
+
* downstream consumers to crash on arithmetic. The fallback path now
|
|
122
|
+
* queries the trajectory extractor for real data and falls back to 0.
|
|
123
|
+
* Use _dataSource === 'pain_context_fallback' to detect partial data.
|
|
121
124
|
*/
|
|
122
125
|
stats: {
|
|
123
|
-
totalAssistantTurns: number
|
|
124
|
-
totalToolCalls: number
|
|
126
|
+
totalAssistantTurns: number;
|
|
127
|
+
totalToolCalls: number;
|
|
125
128
|
totalPainEvents: number;
|
|
126
|
-
totalGateBlocks: number
|
|
127
|
-
failureCount: number
|
|
129
|
+
totalGateBlocks: number;
|
|
130
|
+
failureCount: number;
|
|
128
131
|
};
|
|
129
132
|
/**
|
|
130
133
|
* #219: Marker for data source to identify fallback/partial stats.
|
|
@@ -132,9 +132,17 @@ async function runWorkflowWatchdog(
|
|
|
132
132
|
if (dataSource === 'pain_context_fallback') {
|
|
133
133
|
details.push(`fallback_snapshot: nocturnal workflow ${wf.workflow_id} uses pain-context fallback (stats may be incomplete)`);
|
|
134
134
|
}
|
|
135
|
-
const stats = snapshot.stats as Record<string, number
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const stats = snapshot.stats as Record<string, number> | undefined;
|
|
136
|
+
// #246: Stats are now always number (never null). Detect "empty" fallback:
|
|
137
|
+
// fallback + all counts zero means no real data was available.
|
|
138
|
+
// NOTE: totalAssistantTurns may be 0 even for valid sessions because
|
|
139
|
+
// listRecentNocturnalCandidateSessions (used in fallback path) does not
|
|
140
|
+
// populate assistantTurnCount (only getNocturnalSessionSnapshot does).
|
|
141
|
+
// We use totalToolCalls=0 as the primary indicator instead.
|
|
142
|
+
if (stats && dataSource === 'pain_context_fallback' &&
|
|
143
|
+
stats.totalToolCalls === 0 && stats.totalGateBlocks === 0 &&
|
|
144
|
+
stats.failureCount === 0) {
|
|
145
|
+
details.push(`fallback_snapshot_stats: nocturnal workflow ${wf.workflow_id} has empty fallback stats (no trajectory data found)`);
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
} catch { /* ignore malformed metadata */ }
|
|
@@ -335,7 +343,10 @@ function isSessionAtOrBeforeTriggerTime(
|
|
|
335
343
|
return true;
|
|
336
344
|
}
|
|
337
345
|
|
|
338
|
-
function buildFallbackNocturnalSnapshot(
|
|
346
|
+
function buildFallbackNocturnalSnapshot(
|
|
347
|
+
sleepTask: EvolutionQueueItem,
|
|
348
|
+
extractor?: ReturnType<typeof createNocturnalTrajectoryExtractor> | null
|
|
349
|
+
): NocturnalSessionSnapshot | null {
|
|
339
350
|
const painContext = sleepTask.recentPainContext;
|
|
340
351
|
if (!painContext) {
|
|
341
352
|
return null;
|
|
@@ -349,6 +360,30 @@ function buildFallbackNocturnalSnapshot(sleepTask: EvolutionQueueItem): Nocturna
|
|
|
349
360
|
createdAt: painContext.mostRecent.timestamp,
|
|
350
361
|
}] : [];
|
|
351
362
|
|
|
363
|
+
// #246: Try to extract real session stats from trajectory DB for the pain session.
|
|
364
|
+
// The main path tries getNocturnalSessionSnapshot which returns null when no session
|
|
365
|
+
// exists. Here we attempt a lighter query via listRecentNocturnalCandidateSessions
|
|
366
|
+
// to at least get summary counts for the pain-triggering session.
|
|
367
|
+
let realStats: { totalAssistantTurns: number; totalToolCalls: number; failureCount: number; totalGateBlocks: number } | null = null;
|
|
368
|
+
if (extractor && painContext.mostRecent?.sessionId) {
|
|
369
|
+
try {
|
|
370
|
+
// #246-fix: Use minToolCalls=0 to avoid filtering out sessions with 0 tool calls.
|
|
371
|
+
// The pain-triggering session may have no tool calls but still be worth tracking.
|
|
372
|
+
const summaries = extractor.listRecentNocturnalCandidateSessions({ limit: 300, minToolCalls: 0 });
|
|
373
|
+
const match = summaries.find(s => s.sessionId === painContext.mostRecent!.sessionId);
|
|
374
|
+
if (match) {
|
|
375
|
+
realStats = {
|
|
376
|
+
totalAssistantTurns: match.assistantTurnCount,
|
|
377
|
+
totalToolCalls: match.toolCallCount,
|
|
378
|
+
failureCount: match.failureCount,
|
|
379
|
+
totalGateBlocks: match.gateBlockCount,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
// Best effort — non-fatal
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
352
387
|
return {
|
|
353
388
|
sessionId: painContext.mostRecent?.sessionId || sleepTask.id,
|
|
354
389
|
startedAt: sleepTask.timestamp,
|
|
@@ -359,11 +394,11 @@ function buildFallbackNocturnalSnapshot(sleepTask: EvolutionQueueItem): Nocturna
|
|
|
359
394
|
painEvents: fallbackPainEvents,
|
|
360
395
|
gateBlocks: [],
|
|
361
396
|
stats: {
|
|
362
|
-
totalAssistantTurns:
|
|
363
|
-
totalToolCalls:
|
|
364
|
-
failureCount:
|
|
397
|
+
totalAssistantTurns: realStats?.totalAssistantTurns ?? 0,
|
|
398
|
+
totalToolCalls: realStats?.totalToolCalls ?? 0,
|
|
399
|
+
failureCount: realStats?.failureCount ?? 0,
|
|
365
400
|
totalPainEvents: painContext.recentPainCount,
|
|
366
|
-
totalGateBlocks:
|
|
401
|
+
totalGateBlocks: realStats?.totalGateBlocks ?? 0,
|
|
367
402
|
},
|
|
368
403
|
_dataSource: 'pain_context_fallback',
|
|
369
404
|
};
|
|
@@ -1466,8 +1501,9 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1466
1501
|
} else {
|
|
1467
1502
|
// Phase 1: Build trajectory snapshot for Nocturnal pipeline
|
|
1468
1503
|
// Priority: Pain signal sessionId → Task ID → Recent session with violations
|
|
1504
|
+
let extractor: ReturnType<typeof createNocturnalTrajectoryExtractor> | null = null;
|
|
1469
1505
|
try {
|
|
1470
|
-
|
|
1506
|
+
extractor = createNocturnalTrajectoryExtractor(wctx.workspaceDir);
|
|
1471
1507
|
|
|
1472
1508
|
// 1. Try exact session ID from pain signal (most accurate)
|
|
1473
1509
|
const painSessionId = sleepTask.recentPainContext?.mostRecent?.sessionId;
|
|
@@ -1516,8 +1552,8 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1516
1552
|
|
|
1517
1553
|
// Phase 2: If no trajectory data, try pain-context fallback
|
|
1518
1554
|
if (!snapshotData && sleepTask.recentPainContext) {
|
|
1519
|
-
logger?.warn?.(`[PD:EvolutionWorker] Using pain-context fallback for ${sleepTask.id}: trajectory
|
|
1520
|
-
snapshotData = buildFallbackNocturnalSnapshot(sleepTask) ?? undefined;
|
|
1555
|
+
logger?.warn?.(`[PD:EvolutionWorker] Using pain-context fallback for ${sleepTask.id}: trajectory snapshot unavailable, will try session summary from extractor`);
|
|
1556
|
+
snapshotData = buildFallbackNocturnalSnapshot(sleepTask, extractor) ?? undefined;
|
|
1521
1557
|
}
|
|
1522
1558
|
|
|
1523
1559
|
const snapshotValidation = validateNocturnalSnapshotIngress(snapshotData);
|
|
@@ -54,10 +54,35 @@ describe('validateNocturnalSnapshotIngress', () => {
|
|
|
54
54
|
toolCalls: [],
|
|
55
55
|
painEvents: [],
|
|
56
56
|
gateBlocks: [],
|
|
57
|
+
stats: {
|
|
58
|
+
totalAssistantTurns: 0,
|
|
59
|
+
totalToolCalls: 0,
|
|
60
|
+
totalPainEvents: 0,
|
|
61
|
+
totalGateBlocks: 0,
|
|
62
|
+
failureCount: 0,
|
|
63
|
+
},
|
|
64
|
+
_dataSource: 'pain_context_fallback',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.status).toBe('invalid');
|
|
68
|
+
expect(result.reasons).toContain('fallback snapshot must contain at least one pain signal');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// #246: null stats fields should now be rejected (they used to be accepted for fallback)
|
|
72
|
+
it('rejects null values in stats fields', () => {
|
|
73
|
+
const result = validateNocturnalSnapshotIngress({
|
|
74
|
+
sessionId: 'session-1',
|
|
75
|
+
startedAt: '2026-04-10T00:00:00.000Z',
|
|
76
|
+
updatedAt: '2026-04-10T00:00:00.000Z',
|
|
77
|
+
assistantTurns: [],
|
|
78
|
+
userTurns: [],
|
|
79
|
+
toolCalls: [],
|
|
80
|
+
painEvents: [{ source: 'test', score: 5, severity: 'high', reason: 'test', createdAt: '2026-04-10T00:00:00.000Z' }],
|
|
81
|
+
gateBlocks: [],
|
|
57
82
|
stats: {
|
|
58
83
|
totalAssistantTurns: null,
|
|
59
84
|
totalToolCalls: null,
|
|
60
|
-
totalPainEvents:
|
|
85
|
+
totalPainEvents: 1,
|
|
61
86
|
totalGateBlocks: null,
|
|
62
87
|
failureCount: null,
|
|
63
88
|
},
|
|
@@ -65,6 +90,32 @@ describe('validateNocturnalSnapshotIngress', () => {
|
|
|
65
90
|
});
|
|
66
91
|
|
|
67
92
|
expect(result.status).toBe('invalid');
|
|
68
|
-
expect(result.reasons).toContain('
|
|
93
|
+
expect(result.reasons).toContain('snapshot.stats.totalAssistantTurns must be a finite number');
|
|
94
|
+
expect(result.reasons).toContain('snapshot.stats.totalToolCalls must be a finite number');
|
|
95
|
+
expect(result.reasons).toContain('snapshot.stats.totalGateBlocks must be a finite number');
|
|
96
|
+
expect(result.reasons).toContain('snapshot.stats.failureCount must be a finite number');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('accepts fallback snapshot with valid stats and pain signal', () => {
|
|
100
|
+
const result = validateNocturnalSnapshotIngress({
|
|
101
|
+
sessionId: 'session-1',
|
|
102
|
+
startedAt: '2026-04-10T00:00:00.000Z',
|
|
103
|
+
updatedAt: '2026-04-10T00:00:00.000Z',
|
|
104
|
+
assistantTurns: [],
|
|
105
|
+
userTurns: [],
|
|
106
|
+
toolCalls: [],
|
|
107
|
+
painEvents: [{ source: 'test', score: 5, severity: 'high', reason: 'test', createdAt: '2026-04-10T00:00:00.000Z' }],
|
|
108
|
+
gateBlocks: [],
|
|
109
|
+
stats: {
|
|
110
|
+
totalAssistantTurns: 0,
|
|
111
|
+
totalToolCalls: 0,
|
|
112
|
+
totalPainEvents: 1,
|
|
113
|
+
totalGateBlocks: 0,
|
|
114
|
+
failureCount: 0,
|
|
115
|
+
},
|
|
116
|
+
_dataSource: 'pain_context_fallback',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result.status).toBe('valid');
|
|
69
120
|
});
|
|
70
121
|
});
|