principles-disciple 1.16.0 → 1.17.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +3 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +27 -28
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +209 -104
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -156,7 +156,8 @@ export class HealthQueryService {
|
|
|
156
156
|
const queue = this.readQueueStats();
|
|
157
157
|
const painFlag = this.readPainFlag();
|
|
158
158
|
|
|
159
|
-
// GFI:
|
|
159
|
+
// GFI: Re-sync from session JSON on every request for real-time data
|
|
160
|
+
this.syncGfiFromSession();
|
|
160
161
|
const gfiData = this.readGfiFromDb();
|
|
161
162
|
const {currentGfi} = gfiData;
|
|
162
163
|
const peakToday = gfiData.dailyGfiPeak;
|
|
@@ -277,7 +278,8 @@ export class HealthQueryService {
|
|
|
277
278
|
ORDER BY total DESC
|
|
278
279
|
`, today);
|
|
279
280
|
|
|
280
|
-
// GFI:
|
|
281
|
+
// GFI: Re-sync from session JSON for real-time data
|
|
282
|
+
this.syncGfiFromSession();
|
|
281
283
|
const gfiData = this.readGfiFromDb();
|
|
282
284
|
|
|
283
285
|
return {
|
|
@@ -551,7 +553,7 @@ export class HealthQueryService {
|
|
|
551
553
|
const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
|
|
552
554
|
if (!fs.existsSync(streamPath)) return [];
|
|
553
555
|
|
|
554
|
-
|
|
556
|
+
|
|
555
557
|
let lines: string[] = [];
|
|
556
558
|
try {
|
|
557
559
|
const raw = fs.readFileSync(streamPath, 'utf8').trim();
|
|
@@ -563,7 +565,7 @@ export class HealthQueryService {
|
|
|
563
565
|
|
|
564
566
|
const records: RecentPrincipleChange[] = [];
|
|
565
567
|
for (const line of lines) {
|
|
566
|
-
|
|
568
|
+
|
|
567
569
|
let event: EvolutionStreamRecord | null = null;
|
|
568
570
|
try {
|
|
569
571
|
event = JSON.parse(line) as EvolutionStreamRecord;
|
|
@@ -781,7 +783,7 @@ export class HealthQueryService {
|
|
|
781
783
|
return [];
|
|
782
784
|
}
|
|
783
785
|
|
|
784
|
-
|
|
786
|
+
|
|
785
787
|
private getEventDedupKey(entry: EventLogEntry): string {
|
|
786
788
|
const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
|
|
787
789
|
if (eventId) {
|
|
@@ -851,7 +853,7 @@ export class HealthQueryService {
|
|
|
851
853
|
return fallbackStage;
|
|
852
854
|
}
|
|
853
855
|
|
|
854
|
-
|
|
856
|
+
|
|
855
857
|
private resolveGateType(row: GateBlockRow): string {
|
|
856
858
|
if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
|
|
857
859
|
return row.gate_type;
|
|
@@ -875,14 +877,14 @@ export class HealthQueryService {
|
|
|
875
877
|
return cached.has(columnName);
|
|
876
878
|
}
|
|
877
879
|
|
|
878
|
-
|
|
880
|
+
|
|
879
881
|
private scoreToStatus(score: number): string {
|
|
880
882
|
if (score >= 70) return 'healthy';
|
|
881
883
|
if (score >= 40) return 'warning';
|
|
882
884
|
return 'critical';
|
|
883
885
|
}
|
|
884
886
|
|
|
885
|
-
|
|
887
|
+
|
|
886
888
|
private evolutionToStatus(tier: string, points: number): string {
|
|
887
889
|
const lower = tier.toLowerCase();
|
|
888
890
|
if (lower === 'forest' || lower === 'tree') return 'healthy';
|
|
@@ -890,7 +892,7 @@ export class HealthQueryService {
|
|
|
890
892
|
return 'critical';
|
|
891
893
|
}
|
|
892
894
|
|
|
893
|
-
|
|
895
|
+
|
|
894
896
|
private safeListFiles(dirPath: string, predicate: (_name: string) => boolean): string[] {
|
|
895
897
|
if (!fs.existsSync(dirPath)) return [];
|
|
896
898
|
try {
|
|
@@ -902,7 +904,7 @@ export class HealthQueryService {
|
|
|
902
904
|
}
|
|
903
905
|
}
|
|
904
906
|
|
|
905
|
-
|
|
907
|
+
|
|
906
908
|
private readJsonFile<T>(filePath: string, fallback: T): T {
|
|
907
909
|
if (!fs.existsSync(filePath)) return fallback;
|
|
908
910
|
try {
|
|
@@ -912,12 +914,12 @@ export class HealthQueryService {
|
|
|
912
914
|
}
|
|
913
915
|
}
|
|
914
916
|
|
|
915
|
-
|
|
917
|
+
|
|
916
918
|
private asNumber(value: unknown, fallback: number): number {
|
|
917
919
|
return Number.isFinite(value) ? Number(value) : fallback;
|
|
918
920
|
}
|
|
919
921
|
|
|
920
|
-
|
|
922
|
+
|
|
921
923
|
private asNullableNumber(value: unknown): number | null {
|
|
922
924
|
if (Number.isFinite(value)) return Number(value);
|
|
923
925
|
if (typeof value === 'string' && value.trim().length > 0) {
|
|
@@ -953,7 +955,7 @@ export class HealthQueryService {
|
|
|
953
955
|
today,
|
|
954
956
|
);
|
|
955
957
|
} catch (err) {
|
|
956
|
-
|
|
958
|
+
// Non-critical: GFI sync failure should not block queries
|
|
957
959
|
}
|
|
958
960
|
}
|
|
959
961
|
|
|
@@ -981,11 +983,15 @@ export class HealthQueryService {
|
|
|
981
983
|
*/
|
|
982
984
|
private readLatestSessionFromFile(): SessionState | null {
|
|
983
985
|
const sessionsDir = path.join(this.stateDir, 'sessions');
|
|
984
|
-
if (!fs.existsSync(sessionsDir))
|
|
986
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
985
989
|
|
|
986
990
|
try {
|
|
987
991
|
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
|
|
988
|
-
if (files.length === 0)
|
|
992
|
+
if (files.length === 0) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
989
995
|
|
|
990
996
|
let latest: SessionState | null = null;
|
|
991
997
|
let latestTs = 0;
|
|
@@ -994,7 +1000,10 @@ export class HealthQueryService {
|
|
|
994
1000
|
try {
|
|
995
1001
|
const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
|
|
996
1002
|
const state = JSON.parse(content) as SessionState;
|
|
997
|
-
|
|
1003
|
+
// Skip sessions from different workspaces
|
|
1004
|
+
if (state.workspaceDir && state.workspaceDir !== this.workspaceDir) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
998
1007
|
const ts = Number(state.lastControlActivityAt ?? state.lastActivityAt ?? 0);
|
|
999
1008
|
if (ts > latestTs) {
|
|
1000
1009
|
latestTs = ts;
|
|
@@ -1006,7 +1015,8 @@ export class HealthQueryService {
|
|
|
1006
1015
|
}
|
|
1007
1016
|
|
|
1008
1017
|
return latest;
|
|
1009
|
-
} catch {
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
// Non-critical: failure to read session files should not crash the service
|
|
1010
1020
|
return null;
|
|
1011
1021
|
}
|
|
1012
1022
|
}
|
|
@@ -83,9 +83,9 @@ export class MonitoringQueryService {
|
|
|
83
83
|
const failedEvent = events.find(e => e.event_type === `trinity_${stage}_failed`);
|
|
84
84
|
|
|
85
85
|
// Determine status
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
let status: 'pending' | 'running' | 'completed' | 'failed';
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
let reason: string | undefined;
|
|
90
90
|
|
|
91
91
|
if (!startEvent) {
|
|
@@ -106,7 +106,7 @@ export class MonitoringQueryService {
|
|
|
106
106
|
const outputCount = stageOutputs.filter(so => so.stage === stage).length;
|
|
107
107
|
|
|
108
108
|
// Calculate duration if stage started and completed/failed
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
let duration: number | undefined;
|
|
111
111
|
if (startEvent && (completeEvent || failedEvent)) {
|
|
112
112
|
const endEvent = completeEvent || failedEvent;
|
|
@@ -304,7 +304,7 @@ export function checkWorkspaceIdle(
|
|
|
304
304
|
trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
let reason: string;
|
|
309
309
|
if (mostRecentActivityAt === 0) {
|
|
310
310
|
reason = 'No active sessions found — workspace is idle';
|
|
@@ -352,7 +352,7 @@ export function checkCooldown(
|
|
|
352
352
|
} = {}
|
|
353
353
|
): CooldownCheckResult {
|
|
354
354
|
const {
|
|
355
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
355
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Cooldown parameters reserved for future quota enforcement */
|
|
356
356
|
globalCooldownMs: _globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS,
|
|
357
357
|
principleCooldownMs: _principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS,
|
|
358
358
|
maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW,
|
|
@@ -372,7 +372,7 @@ export function checkCooldown(
|
|
|
372
372
|
if (cooldownEnd > now) {
|
|
373
373
|
globalCooldownActive = true;
|
|
374
374
|
globalCooldownRemainingMs = cooldownEnd - now;
|
|
375
|
-
globalCooldownUntil = state.globalCooldownUntil;
|
|
375
|
+
globalCooldownUntil = state.globalCooldownUntil;
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
@@ -540,7 +540,7 @@ export interface PreflightCheckResult {
|
|
|
540
540
|
* @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
|
|
541
541
|
* @param idleCheckOverride - Optional override for idle check result (for testing)
|
|
542
542
|
*/
|
|
543
|
-
|
|
543
|
+
|
|
544
544
|
export function checkPreflight(
|
|
545
545
|
workspaceDir: string,
|
|
546
546
|
stateDir: string,
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
import { NocturnalPathResolver } from '../core/nocturnal-paths.js';
|
|
100
100
|
import { registerSample } from '../core/nocturnal-dataset.js';
|
|
101
101
|
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
102
|
+
import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
|
|
102
103
|
|
|
103
104
|
// ---------------------------------------------------------------------------
|
|
104
105
|
// Types
|
|
@@ -266,16 +267,16 @@ function invokeStubReflector(
|
|
|
266
267
|
|
|
267
268
|
// Build a plausible bad/better decision pair based on available snapshot data.
|
|
268
269
|
// This is synthetic — real reflection would come from subagent analysis.
|
|
269
|
-
const hasFailures = snapshot.stats.failureCount > 0;
|
|
270
|
+
const hasFailures = (snapshot.stats.failureCount ?? 0) > 0;
|
|
270
271
|
const hasPain = snapshot.stats.totalPainEvents > 0;
|
|
271
|
-
const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
|
|
272
|
+
const hasGateBlocks = (snapshot.stats.totalGateBlocks ?? 0) > 0;
|
|
272
273
|
|
|
273
274
|
// Detect what kind of signal is available and craft appropriate artifact
|
|
274
|
-
|
|
275
|
+
|
|
275
276
|
let badDecision: string;
|
|
276
|
-
|
|
277
|
+
|
|
277
278
|
let betterDecision: string;
|
|
278
|
-
|
|
279
|
+
|
|
279
280
|
let rationale: string;
|
|
280
281
|
|
|
281
282
|
if (hasGateBlocks) {
|
|
@@ -362,7 +363,7 @@ function buildGateBlockRefs(snapshot: NocturnalSessionSnapshot): string[] {
|
|
|
362
363
|
);
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
|
|
366
|
+
|
|
366
367
|
function buildDefaultArtificerOutput(
|
|
367
368
|
ruleId: string,
|
|
368
369
|
artifact: NocturnalArtifact,
|
|
@@ -411,7 +412,7 @@ function buildDefaultArtificerOutput(
|
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
|
|
415
|
+
|
|
415
416
|
function persistCodeCandidate(
|
|
416
417
|
workspaceDir: string,
|
|
417
418
|
stateDir: string,
|
|
@@ -503,7 +504,7 @@ function persistCodeCandidate(
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
|
|
506
|
-
|
|
507
|
+
|
|
507
508
|
function maybePersistArtificerCandidate(
|
|
508
509
|
workspaceDir: string,
|
|
509
510
|
stateDir: string,
|
|
@@ -743,10 +744,10 @@ export function executeNocturnalReflection(
|
|
|
743
744
|
// -------------------------------------------------------------------------
|
|
744
745
|
// Step 5: Artifact generation (Trinity or single-reflector)
|
|
745
746
|
// -------------------------------------------------------------------------
|
|
746
|
-
|
|
747
|
+
|
|
747
748
|
let trinityArtifact: TrinityDraftArtifact | null = null;
|
|
748
749
|
let trinityResult: TrinityResult | null = null;
|
|
749
|
-
|
|
750
|
+
|
|
750
751
|
let rawJson: string;
|
|
751
752
|
|
|
752
753
|
if (options.skipReflector) {
|
|
@@ -966,7 +967,7 @@ export function executeNocturnalReflection(
|
|
|
966
967
|
boundedAction: execResult.boundedAction,
|
|
967
968
|
};
|
|
968
969
|
|
|
969
|
-
|
|
970
|
+
|
|
970
971
|
let persistedPath: string;
|
|
971
972
|
try {
|
|
972
973
|
persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
|
|
@@ -1090,7 +1091,7 @@ export async function executeNocturnalReflectionAsync(
|
|
|
1090
1091
|
|
|
1091
1092
|
// If runtime adapter is provided, use async Trinity path
|
|
1092
1093
|
if (options.runtimeAdapter) {
|
|
1093
|
-
|
|
1094
|
+
|
|
1094
1095
|
return executeNocturnalReflectionWithAdapter(workspaceDir, stateDir, options);
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
@@ -1139,20 +1140,36 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1139
1140
|
}
|
|
1140
1141
|
|
|
1141
1142
|
// Step 2: Target selection (or use override to skip)
|
|
1142
|
-
|
|
1143
|
+
|
|
1143
1144
|
let selectedPrincipleId: string | undefined;
|
|
1144
|
-
|
|
1145
|
+
|
|
1145
1146
|
let selectedSessionId: string | undefined;
|
|
1146
|
-
|
|
1147
|
+
|
|
1147
1148
|
let snapshot: NocturnalSessionSnapshot | null = null;
|
|
1148
1149
|
|
|
1149
1150
|
if (options.principleIdOverride && options.snapshotOverride) {
|
|
1151
|
+
const snapshotValidation = validateNocturnalSnapshotIngress(options.snapshotOverride);
|
|
1152
|
+
if (snapshotValidation.status !== 'valid' || !snapshotValidation.snapshot) {
|
|
1153
|
+
return {
|
|
1154
|
+
success: false,
|
|
1155
|
+
skipReason: 'insufficient_snapshot_data',
|
|
1156
|
+
noTargetSelected: true,
|
|
1157
|
+
validationFailed: true,
|
|
1158
|
+
validationFailures: snapshotValidation.reasons.length > 0
|
|
1159
|
+
? snapshotValidation.reasons
|
|
1160
|
+
: ['invalid snapshot override'],
|
|
1161
|
+
snapshot: undefined,
|
|
1162
|
+
diagnostics,
|
|
1163
|
+
trinityTelemetry: undefined,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1150
1167
|
// Skip Selector: use provided principleId and snapshot directly
|
|
1151
1168
|
selectedPrincipleId = options.principleIdOverride;
|
|
1152
|
-
selectedSessionId =
|
|
1153
|
-
snapshot =
|
|
1169
|
+
selectedSessionId = snapshotValidation.snapshot.sessionId;
|
|
1170
|
+
snapshot = snapshotValidation.snapshot;
|
|
1154
1171
|
// Calculate violation density from snapshot stats for meaningful diagnostics
|
|
1155
|
-
const snapStats =
|
|
1172
|
+
const snapStats = snapshotValidation.snapshot.stats;
|
|
1156
1173
|
const totalToolCalls = snapStats?.totalToolCalls ?? 0;
|
|
1157
1174
|
const failureCount = snapStats?.failureCount ?? 0;
|
|
1158
1175
|
const violationDensity = totalToolCalls > 0 ? failureCount / totalToolCalls : 0;
|
|
@@ -1205,9 +1222,9 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1205
1222
|
};
|
|
1206
1223
|
}
|
|
1207
1224
|
|
|
1208
|
-
|
|
1225
|
+
|
|
1209
1226
|
selectedPrincipleId = selection.selectedPrincipleId;
|
|
1210
|
-
|
|
1227
|
+
|
|
1211
1228
|
selectedSessionId = selection.selectedSessionId;
|
|
1212
1229
|
|
|
1213
1230
|
if (!selectedPrincipleId || !selectedSessionId) {
|
|
@@ -1240,10 +1257,10 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1240
1257
|
});
|
|
1241
1258
|
|
|
1242
1259
|
// Step 4: Trinity execution via adapter (async)
|
|
1243
|
-
|
|
1260
|
+
|
|
1244
1261
|
let trinityArtifact: TrinityDraftArtifact | null = null;
|
|
1245
1262
|
let trinityResult: TrinityResult | null = null;
|
|
1246
|
-
|
|
1263
|
+
|
|
1247
1264
|
let rawJson: string;
|
|
1248
1265
|
|
|
1249
1266
|
if (options.skipReflector) {
|
|
@@ -1348,7 +1365,7 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1348
1365
|
|
|
1349
1366
|
// Step 7: Persist artifact
|
|
1350
1367
|
const artifactWithBoundedAction = { ...arbiterResult.artifact, boundedAction: execResult.boundedAction };
|
|
1351
|
-
|
|
1368
|
+
|
|
1352
1369
|
let persistedPath: string;
|
|
1353
1370
|
try {
|
|
1354
1371
|
persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
|
|
@@ -286,7 +286,7 @@ export class NocturnalTargetSelector {
|
|
|
286
286
|
recentMaxPainScore: number;
|
|
287
287
|
};
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
constructor(
|
|
291
291
|
workspaceDir: string,
|
|
292
292
|
stateDir: string,
|
|
@@ -518,7 +518,7 @@ export class NocturnalTargetSelector {
|
|
|
518
518
|
*
|
|
519
519
|
* This is a convenience wrapper for the common case.
|
|
520
520
|
*/
|
|
521
|
-
|
|
521
|
+
|
|
522
522
|
export function selectNocturnalTarget(
|
|
523
523
|
workspaceDir: string,
|
|
524
524
|
stateDir: string,
|
|
@@ -418,7 +418,7 @@ export class RuntimeSummaryService {
|
|
|
418
418
|
* NOT a truth source for Phase 3 eligibility or decisions.
|
|
419
419
|
* Queue is the only authoritative execution truth source.
|
|
420
420
|
*/
|
|
421
|
-
|
|
421
|
+
|
|
422
422
|
private static buildDirectiveSummary(
|
|
423
423
|
queue: QueueItem[] | null,
|
|
424
424
|
directive: DirectiveFile | null,
|
|
@@ -69,7 +69,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
|
|
|
69
69
|
return super.startWorkflow(spec, options);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
protected override generateWorkflowId(): string {
|
|
74
74
|
return `wf_dr_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
75
75
|
}
|
|
@@ -70,7 +70,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
|
|
|
70
70
|
return super.startWorkflow(spec, options);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
protected override createWorkflowMetadata<TResult>(
|
|
75
75
|
spec: SubagentWorkflowSpec<TResult>,
|
|
76
76
|
options: {
|
|
@@ -103,7 +103,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
|
|
|
103
103
|
].join('\n');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
protected override generateWorkflowId(): string {
|
|
108
108
|
return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
109
109
|
}
|
|
@@ -185,7 +185,7 @@ export const empathyObserverWorkflowSpec: SubagentWorkflowSpec<EmpathyResult> =
|
|
|
185
185
|
ttlMs: 300_000,
|
|
186
186
|
shouldDeleteSessionAfterFinalize: true,
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
|
|
189
189
|
buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
|
|
190
190
|
const userMessage = String(taskInput).trim();
|
|
191
191
|
return [
|
|
@@ -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 { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
|
|
44
|
+
import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
|
|
44
45
|
|
|
45
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
47
|
// NocturnalResult Type Alias
|
|
@@ -82,7 +83,7 @@ export interface NocturnalWorkflowOptions {
|
|
|
82
83
|
* - timeoutMs: 15 minutes (900000ms)
|
|
83
84
|
* - ttlMs: 30 minutes (1800000ms)
|
|
84
85
|
*/
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
|
|
87
88
|
workflowType: 'nocturnal',
|
|
88
89
|
transport: 'runtime_direct',
|
|
@@ -115,7 +116,7 @@ export const nocturnalWorkflowSpec: SubagentWorkflowSpec<NocturnalResult> = {
|
|
|
115
116
|
return status === 'ok';
|
|
116
117
|
},
|
|
117
118
|
};
|
|
118
|
-
|
|
119
|
+
|
|
119
120
|
|
|
120
121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
122
|
// NocturnalWorkflowManager
|
|
@@ -212,15 +213,17 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
212
213
|
this.store.recordEvent(workflowId, 'nocturnal_started', null, 'active', 'TrinityRuntimeAdapter invoked', { workflowType: 'nocturnal' });
|
|
213
214
|
|
|
214
215
|
// Extract snapshot and principleId from taskInput.metadata (NOC-07: Trinity async path)
|
|
215
|
-
const
|
|
216
|
+
const snapshotValidation = validateNocturnalSnapshotIngress(options.metadata?.snapshot);
|
|
217
|
+
const snapshot = snapshotValidation.snapshot;
|
|
216
218
|
const principleId = options.metadata?.principleId as string | undefined;
|
|
217
219
|
// Extract painContext for Selector ranking bias
|
|
218
220
|
const painContext = options.metadata?.painContext as RecentPainContext | undefined;
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.logger.warn(`[PD:NocturnalWorkflow]
|
|
223
|
-
this.store.
|
|
222
|
+
if (snapshotValidation.status !== 'valid' || !snapshot) {
|
|
223
|
+
const reason = `Invalid snapshot ingress: ${snapshotValidation.reasons.join('; ') || 'missing snapshot'}`;
|
|
224
|
+
this.logger.warn(`[PD:NocturnalWorkflow] ${reason} workflow=${workflowId}`);
|
|
225
|
+
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
226
|
+
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, { workflowId });
|
|
224
227
|
return {
|
|
225
228
|
workflowId,
|
|
226
229
|
childSessionKey: `nocturnal:internal:${workflowId}`,
|
|
@@ -350,13 +353,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
350
353
|
this.markCompleted(workflowId);
|
|
351
354
|
}
|
|
352
355
|
|
|
353
|
-
|
|
356
|
+
|
|
354
357
|
async notifyLifecycleEvent(
|
|
355
|
-
|
|
358
|
+
|
|
356
359
|
_workflowId: string,
|
|
357
|
-
|
|
360
|
+
|
|
358
361
|
_event: 'subagent_spawned' | 'subagent_ended',
|
|
359
|
-
|
|
362
|
+
|
|
360
363
|
_data?: Record<string, unknown>
|
|
361
364
|
): Promise<void> {
|
|
362
365
|
// D-10: No-op. NocturnalWorkflowManager does not use the wait-on-run pattern.
|
|
@@ -411,13 +414,13 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
411
414
|
maxAgeMs = 30 * 60 * 1000,
|
|
412
415
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: subagentRuntime param is intentionally any for backward compatibility with callers
|
|
413
416
|
subagentRuntime?: any,
|
|
414
|
-
|
|
417
|
+
|
|
415
418
|
agentSession?: {
|
|
416
419
|
resolveStorePath: () => string;
|
|
417
420
|
loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
|
|
418
421
|
saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
|
|
419
422
|
},
|
|
420
|
-
|
|
423
|
+
|
|
421
424
|
): Promise<number> {
|
|
422
425
|
const expired = this.store.getExpiredWorkflows(maxAgeMs);
|
|
423
426
|
|
|
@@ -8,12 +8,12 @@ import type {
|
|
|
8
8
|
import { isExpectedSubagentError } from './subagent-error-utils.js';
|
|
9
9
|
|
|
10
10
|
export interface TransportDriver {
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
run(params: RunParams): Promise<RunResult>;
|
|
13
13
|
wait(params: WaitParams): Promise<WaitResult>;
|
|
14
14
|
getResult(params: GetResultParams): Promise<GetResultResult>;
|
|
15
15
|
cleanup(params: CleanupParams): Promise<void>;
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface RunParams {
|
|
@@ -55,7 +55,7 @@ export interface CleanupParams {
|
|
|
55
55
|
deleteTranscript?: boolean;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
type PluginRuntimeSubagent = {
|
|
60
60
|
run: (params: {
|
|
61
61
|
sessionKey: string;
|
|
@@ -79,21 +79,21 @@ type PluginRuntimeSubagent = {
|
|
|
79
79
|
deleteTranscript?: boolean;
|
|
80
80
|
}) => Promise<void>;
|
|
81
81
|
};
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* OpenClaw plugin SDK's agent.session namespace — always available (not gateway-scoped).
|
|
86
86
|
* These functions are imported directly from OpenClaw's session store module.
|
|
87
87
|
*/
|
|
88
88
|
export type AgentSessionAPI = {
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
resolveStorePath: () => string;
|
|
91
91
|
loadSessionStore: (storePath: string, opts?: { skipCache?: boolean }) => Record<string, unknown>;
|
|
92
92
|
saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
|
|
93
93
|
resolveSessionFilePath: (sessionKey: string) => string;
|
|
94
94
|
/** Optional: OpenClaw config object needed for session path resolution */
|
|
95
95
|
config?: unknown;
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
export class RuntimeDirectDriver implements TransportDriver {
|
|
@@ -206,6 +206,10 @@ export class RuntimeDirectDriver implements TransportDriver {
|
|
|
206
206
|
this.logger.info(`[PD:RuntimeDirectDriver] Gateway-scoped cleanup unavailable (${errMsg.split(':')[0]}), falling back to agent.session`);
|
|
207
207
|
await this.cleanupViaAgentSession(params.sessionKey);
|
|
208
208
|
this.logger.info(`[PD:RuntimeDirectDriver] Fallback cleanup succeeded`);
|
|
209
|
+
} else if (isNonGatewayContext && !this.agentSession) {
|
|
210
|
+
// #232: Both cleanup paths unavailable — this is an OpenClaw environment limitation,
|
|
211
|
+
// not a PD bug. Session will be cleaned up on next gateway request or TTL expiry.
|
|
212
|
+
this.logger.warn(`[PD:RuntimeDirectDriver] Session cleanup skipped (${errMsg.split(':')[0]}): no gateway context and agentSession unavailable — session will expire naturally`);
|
|
209
213
|
} else {
|
|
210
214
|
this.logger.error(`[PD:RuntimeDirectDriver] Cleanup failed: ${errMsg}`);
|
|
211
215
|
throw error;
|
|
@@ -145,7 +145,7 @@ export interface WorkflowHandle {
|
|
|
145
145
|
* ```
|
|
146
146
|
*/
|
|
147
147
|
export interface SubagentWorkflowSpec<TResult> {
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
/** Unique identifier for this workflow type */
|
|
150
150
|
workflowType: string;
|
|
151
151
|
/** Which transport mechanism to use */
|
|
@@ -172,7 +172,7 @@ export interface SubagentWorkflowSpec<TResult> {
|
|
|
172
172
|
* For runtime_direct: typically finalize only on 'ok', skip on 'timeout'/'error'.
|
|
173
173
|
*/
|
|
174
174
|
shouldFinalizeOnWaitStatus: (status: 'ok' | 'error' | 'timeout') => boolean;
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// ── Empathy Observer Specific Types ──────────────────────────────────────────
|
|
@@ -229,7 +229,7 @@ export interface EmpathyObserverWorkflowSpec extends SubagentWorkflowSpec<Empath
|
|
|
229
229
|
* This is what the helper exposes to business modules.
|
|
230
230
|
*/
|
|
231
231
|
export interface WorkflowManager {
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
/**
|
|
234
234
|
* Start a new workflow.
|
|
235
235
|
* Creates workflow state, spawns subagent, and returns handle.
|
|
@@ -289,7 +289,7 @@ export interface WorkflowManager {
|
|
|
289
289
|
* Release resources (DB connections, timers).
|
|
290
290
|
*/
|
|
291
291
|
dispose: () => void;
|
|
292
|
-
|
|
292
|
+
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// ── Workflow Store (for SQLite persistence) ──────────────────────────────────
|
|
@@ -155,7 +155,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
|
|
|
155
155
|
* Create workflow metadata for store.createWorkflow().
|
|
156
156
|
* Subclasses override to add type-specific fields.
|
|
157
157
|
*/
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
protected createWorkflowMetadata<TResult>(
|
|
160
160
|
spec: SubagentWorkflowSpec<TResult>,
|
|
161
161
|
options: {
|
|
@@ -181,7 +181,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
|
|
|
181
181
|
* Called after driver.run() succeeds.
|
|
182
182
|
* Subclasses override to call store.createWorkflow() with type-specific metadata.
|
|
183
183
|
*/
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
protected async createWorkflowRecord<TResult>(
|
|
186
186
|
workflowId: string,
|
|
187
187
|
childSessionKey: string,
|
|
@@ -213,7 +213,7 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
|
|
|
213
213
|
|
|
214
214
|
// ── Protected Helpers ────────────────────────────────────────────────────
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
protected buildRunParams<TResult>(
|
|
218
218
|
spec: SubagentWorkflowSpec<TResult>,
|
|
219
219
|
options: {
|
|
@@ -311,11 +311,11 @@ export abstract class WorkflowManagerBase implements WorkflowManager {
|
|
|
311
311
|
status: 'ok' | 'error' | 'timeout',
|
|
312
312
|
error?: string
|
|
313
313
|
): Promise<void> {
|
|
314
|
-
|
|
314
|
+
|
|
315
315
|
let workflow;
|
|
316
316
|
try {
|
|
317
317
|
workflow = this.store.getWorkflow(workflowId);
|
|
318
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
318
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Error is handled via early returns based on status, not error value */
|
|
319
319
|
} catch (_dbError) {
|
|
320
320
|
// Database connection closed (e.g., by lifecycle notification dispose).
|
|
321
321
|
// If subagent succeeded, this is a known race condition — the workflow
|