principles-disciple 1.34.1 → 1.35.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/.dependency-cruiser.json +19 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
- package/src/config/defaults/runtime.ts +100 -24
- package/src/core/event-log.ts +87 -20
- package/src/core/nocturnal-candidate-scoring.ts +6 -6
- package/src/core/nocturnal-trinity-types.ts +94 -0
- package/src/core/nocturnal-trinity.ts +35 -99
- package/src/core/session-tracker.ts +7 -6
- package/src/core/system-logger.ts +104 -12
- package/src/core/workspace-dir-service.ts +40 -6
- package/src/core/workspace-dir-validation.ts +5 -37
- package/src/hooks/trajectory-collector.ts +7 -7
- package/src/index.ts +8 -68
- package/src/service/central-sync-service.ts +3 -8
- package/src/service/correction-observer-workflow-manager.ts +2 -2
- package/src/service/evolution-worker.ts +30 -35
- package/src/service/nocturnal-service.ts +72 -47
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +4 -4
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +4 -4
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -2
- package/src/service/subagent-workflow/types.ts +69 -3
- package/src/utils/shadow-fingerprint.ts +42 -0
- package/src/utils/workspace-resolver.ts +54 -0
- package/tests/core/workspace-dir-validation.test.ts +1 -1
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +3 -3
- package/vitest.config.ts +53 -6
package/src/index.ts
CHANGED
|
@@ -16,9 +16,7 @@ import type {
|
|
|
16
16
|
PluginHookSubagentSpawningResult,
|
|
17
17
|
PluginHookSubagentContext,
|
|
18
18
|
} from './openclaw-sdk.js';
|
|
19
|
-
import * as crypto from 'crypto';
|
|
20
19
|
import * as path from 'path';
|
|
21
|
-
import type { WorkerProfile } from './core/model-deployment-registry.js';
|
|
22
20
|
import { classifyTask } from './core/local-worker-routing.js';
|
|
23
21
|
import { completeShadowObservation, recordShadowRouting } from './core/shadow-observation-registry.js';
|
|
24
22
|
import { getCommandDescription } from './i18n/commands.js';
|
|
@@ -59,83 +57,25 @@ import { migrateDirectoryStructure } from './core/migration.js';
|
|
|
59
57
|
import { SystemLogger } from './core/system-logger.js';
|
|
60
58
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
61
59
|
import { createWritePainFlagTool } from './tools/write-pain-flag.js';
|
|
62
|
-
import { PathResolver
|
|
63
|
-
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
64
|
-
import { resolveRequiredWorkspaceDir, resolveWorkspaceDir, type WorkspaceResolutionContext } from './core/workspace-dir-service.js';
|
|
60
|
+
import { PathResolver } from './core/path-resolver.js';
|
|
65
61
|
import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
|
|
66
62
|
import { extractAgentIdFromSessionKey } from './utils/session-key.js';
|
|
63
|
+
import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe } from './utils/workspace-resolver.js';
|
|
64
|
+
import { computeRuntimeShadowTaskFingerprint, PD_LOCAL_PROFILES } from './utils/shadow-fingerprint.js';
|
|
65
|
+
import type { WorkerProfile } from './core/model-deployment-registry.js';
|
|
66
|
+
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
67
|
+
import { resolveWorkspaceDirFromApi } from './core/path-resolver.js';
|
|
68
|
+
import { resolveRequiredWorkspaceDir } from './core/workspace-dir-service.js';
|
|
67
69
|
|
|
68
70
|
// Track initialization to avoid repeated calls
|
|
69
71
|
let workspaceInitialized = false;
|
|
70
72
|
// Track started evolution workers — one per workspace
|
|
71
73
|
const startedWorkspaces = new Set<string>();
|
|
72
74
|
|
|
73
|
-
/**
|
|
74
|
-
* Resolve workspaceDir for slash commands.
|
|
75
|
-
* Chain: ctx.workspaceDir → resolveWorkspaceDirFromApi (official OpenClaw API + env vars)
|
|
76
|
-
*
|
|
77
|
-
* CRITICAL: Throws if workspaceDir cannot be resolved. Silent failures are dangerous
|
|
78
|
-
* because commands might operate on the wrong directory.
|
|
79
|
-
*/
|
|
80
|
-
function resolveCommandWorkspaceDir(
|
|
81
|
-
api: OpenClawPluginApi,
|
|
82
|
-
ctx: { workspaceDir?: string },
|
|
83
|
-
): string {
|
|
84
|
-
// 1. Direct from command context (most reliable — set by OpenClaw for current session)
|
|
85
|
-
if (ctx.workspaceDir) {
|
|
86
|
-
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
87
|
-
if (!issue) return ctx.workspaceDir;
|
|
88
|
-
api.logger.error(`[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 2. Official OpenClaw API → env vars → config file
|
|
92
|
-
const resolved = resolveWorkspaceDirFromApi(api);
|
|
93
|
-
if (resolved) return resolved;
|
|
94
|
-
|
|
95
|
-
// CRITICAL FAILURE: Cannot determine workspace directory
|
|
96
|
-
const errorMsg = `[PD:Command] CRITICAL: Cannot resolve workspace directory. ` +
|
|
97
|
-
`ctx.workspaceDir="${ctx.workspaceDir}" is invalid, and all fallbacks failed. ` +
|
|
98
|
-
`Commands will NOT execute to prevent data corruption.`;
|
|
99
|
-
api.logger.error(errorMsg);
|
|
100
|
-
|
|
101
|
-
throw new Error(errorMsg);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
75
|
// Map from childSessionKey → shadowObservationId
|
|
105
76
|
// Used to complete shadow observations when subagent ends
|
|
106
77
|
const pendingShadowObservations = new Map<string, string>();
|
|
107
78
|
|
|
108
|
-
// PD local worker profiles that are managed by the shadow routing policy
|
|
109
|
-
const PD_LOCAL_PROFILES = new Set<WorkerProfile>(['local-reader', 'local-editor']);
|
|
110
|
-
|
|
111
|
-
function computeRuntimeShadowTaskFingerprint(event: PluginHookSubagentSpawningEvent): string {
|
|
112
|
-
const payload = {
|
|
113
|
-
childSessionKey: event.childSessionKey,
|
|
114
|
-
agentId: event.agentId,
|
|
115
|
-
label: event.label ?? '',
|
|
116
|
-
mode: event.mode,
|
|
117
|
-
threadRequested: event.threadRequested,
|
|
118
|
-
requesterChannel: event.requester?.channel ?? '',
|
|
119
|
-
requesterThreadId: event.requester?.threadId ?? '',
|
|
120
|
-
};
|
|
121
|
-
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function _resolveCommandWorkspaceDirStrict(
|
|
125
|
-
api: OpenClawPluginApi,
|
|
126
|
-
ctx: WorkspaceResolutionContext,
|
|
127
|
-
): string {
|
|
128
|
-
return resolveRequiredWorkspaceDir(api, ctx, { source: 'command' });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function resolveToolHookWorkspaceDirSafe(
|
|
132
|
-
ctx: WorkspaceResolutionContext,
|
|
133
|
-
api: OpenClawPluginApi,
|
|
134
|
-
source: string,
|
|
135
|
-
): string | undefined {
|
|
136
|
-
return resolveWorkspaceDir(api, ctx, { source });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
79
|
const plugin = {
|
|
140
80
|
name: "Principles Disciple",
|
|
141
81
|
description: "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
@@ -421,7 +361,7 @@ const plugin = {
|
|
|
421
361
|
|
|
422
362
|
// ── Slash Commands ──
|
|
423
363
|
// Register command with optional short alias
|
|
424
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
364
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
425
365
|
const registerCommandWithAlias = (name: string, alias: string | null, desc: string, handler: any, opts?: { acceptsArgs?: boolean }) => {
|
|
426
366
|
const base = {
|
|
427
367
|
name,
|
|
@@ -7,18 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from '../openclaw-sdk.js';
|
|
9
9
|
import { CentralDatabase } from './central-database.js';
|
|
10
|
+
import { WORKFLOW_TTL_MS } from '../config/defaults/runtime.js';
|
|
10
11
|
|
|
11
12
|
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
|
12
13
|
let logger: PluginLogger | undefined = undefined;
|
|
13
14
|
let centralDb: CentralDatabase | null = null;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
* Default sync interval: 5 minutes.
|
|
17
|
-
* Can be overridden via config: intervals.central_sync_ms
|
|
18
|
-
*/
|
|
19
|
-
const DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1000;
|
|
20
|
-
|
|
21
|
-
// eslint-disable-next-line complexity -- complexity 12, refactor candidate
|
|
16
|
+
|
|
22
17
|
async function runSyncCycle(): Promise<void> {
|
|
23
18
|
if (!centralDb) {
|
|
24
19
|
logger?.warn?.('[PD:CentralSync] CentralDatabase not initialized, skipping sync');
|
|
@@ -51,7 +46,7 @@ export const CentralSyncService: OpenClawPluginService = {
|
|
|
51
46
|
logger = ctxLogger;
|
|
52
47
|
|
|
53
48
|
const { intervals } = config as { intervals?: { central_sync_ms?: number } };
|
|
54
|
-
const intervalMs = intervals?.central_sync_ms ??
|
|
49
|
+
const intervalMs = intervals?.central_sync_ms ?? WORKFLOW_TTL_MS;
|
|
55
50
|
|
|
56
51
|
// Initialize CentralDatabase
|
|
57
52
|
centralDb = new CentralDatabase();
|
|
@@ -22,11 +22,11 @@ import type {
|
|
|
22
22
|
CorrectionObserverPayload,
|
|
23
23
|
CorrectionObserverResult,
|
|
24
24
|
} from './correction-observer-types.js';
|
|
25
|
+
import { WORKFLOW_TTL_MS } from '../config/defaults/runtime.js';
|
|
25
26
|
|
|
26
27
|
const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-correction-';
|
|
27
28
|
|
|
28
29
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
29
|
-
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
30
30
|
|
|
31
31
|
// ── Options ─────────────────────────────────────────────────────────────────
|
|
32
32
|
|
|
@@ -162,7 +162,7 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
|
|
|
162
162
|
workflowType: 'correction_observer',
|
|
163
163
|
sessionPrefix: WORKFLOW_SESSION_PREFIX,
|
|
164
164
|
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
165
|
-
defaultTtlMs:
|
|
165
|
+
defaultTtlMs: WORKFLOW_TTL_MS,
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -16,10 +16,11 @@ import { getEvolutionLogger } from '../core/evolution-logger.js';
|
|
|
16
16
|
import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
|
|
17
17
|
export type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
|
|
18
18
|
import { LockUnavailableError } from '../config/index.js';
|
|
19
|
+
import { PAIN_QUEUE_DEDUP_WINDOW_MS } from '../config/defaults/runtime.js';
|
|
19
20
|
import { checkWorkspaceIdle, checkCooldown } from './nocturnal-runtime.js';
|
|
20
21
|
import { loadNocturnalConfig } from './nocturnal-config.js';
|
|
21
22
|
import { WorkflowStore } from './subagent-workflow/workflow-store.js';
|
|
22
|
-
import type { WorkflowRow } from './subagent-workflow/types.js';
|
|
23
|
+
import type { WorkflowRow, RecentPainContext } from './subagent-workflow/types.js';
|
|
23
24
|
import { EmpathyObserverWorkflowManager } from './subagent-workflow/empathy-observer-workflow-manager.js';
|
|
24
25
|
import { DeepReflectWorkflowManager } from './subagent-workflow/deep-reflect-workflow-manager.js';
|
|
25
26
|
import { NocturnalWorkflowManager, nocturnalWorkflowSpec } from './subagent-workflow/nocturnal-workflow-manager.js';
|
|
@@ -36,10 +37,18 @@ import type { CorrectionObserverPayload } from './correction-observer-types.js';
|
|
|
36
37
|
import { KeywordOptimizationService } from './keyword-optimization-service.js';
|
|
37
38
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
38
39
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
39
|
-
|
|
40
|
-
const WORKFLOW_TTL_MS = 5 * 60 * 1000; // 5 minutes default TTL for helper workflows
|
|
40
|
+
import { WORKFLOW_TTL_MS } from '../config/defaults/runtime.js';
|
|
41
41
|
import { OpenClawTrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Atomic file write — write to temp then rename to prevent partial writes on crash.
|
|
45
|
+
*/
|
|
46
|
+
function atomicWriteFileSync(filePath: string, data: string): void {
|
|
47
|
+
const tmpPath = filePath + '.tmp';
|
|
48
|
+
fs.writeFileSync(tmpPath, data, 'utf8');
|
|
49
|
+
fs.renameSync(tmpPath, filePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
// ── Workflow Watchdog ────────────────────────────────────────────────────────
|
|
44
53
|
// Detects stale/orphaned workflows, invalid results, and cleanup failures.
|
|
45
54
|
// Runs every heartbeat cycle, catching bugs like:
|
|
@@ -200,27 +209,6 @@ let timeoutId: NodeJS.Timeout | null = null;
|
|
|
200
209
|
export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
|
|
201
210
|
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation';
|
|
202
211
|
|
|
203
|
-
/**
|
|
204
|
-
* Recent pain context attached to sleep_reflection tasks.
|
|
205
|
-
* Carries explicit recent pain signal metadata without being a separate task kind.
|
|
206
|
-
* Used by NocturnalTargetSelector for ranking bias and context enrichment.
|
|
207
|
-
*/
|
|
208
|
-
export interface RecentPainContext {
|
|
209
|
-
/** Most recent unresolved pain event */
|
|
210
|
-
mostRecent: {
|
|
211
|
-
score: number;
|
|
212
|
-
source: string;
|
|
213
|
-
reason: string;
|
|
214
|
-
timestamp: string;
|
|
215
|
-
/** Session ID where the pain occurred */
|
|
216
|
-
sessionId: string;
|
|
217
|
-
} | null;
|
|
218
|
-
/** Count of pain events in the recent window (for signal strength) */
|
|
219
|
-
recentPainCount: number;
|
|
220
|
-
/** Highest pain score in the recent window */
|
|
221
|
-
recentMaxPainScore: number;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
212
|
export interface EvolutionQueueItem {
|
|
225
213
|
// Core identity
|
|
226
214
|
id: string;
|
|
@@ -426,7 +414,6 @@ function buildFallbackNocturnalSnapshot(
|
|
|
426
414
|
};
|
|
427
415
|
}
|
|
428
416
|
|
|
429
|
-
const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
|
|
430
417
|
|
|
431
418
|
// P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
|
|
432
419
|
export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
@@ -724,7 +711,7 @@ function enqueueNewSleepReflectionTask(
|
|
|
724
711
|
recentPainContext,
|
|
725
712
|
});
|
|
726
713
|
|
|
727
|
-
|
|
714
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
728
715
|
logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
|
|
729
716
|
}
|
|
730
717
|
|
|
@@ -869,7 +856,7 @@ async function doEnqueuePainTask(
|
|
|
869
856
|
retryCount: 0, maxRetries: 3,
|
|
870
857
|
});
|
|
871
858
|
|
|
872
|
-
|
|
859
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
873
860
|
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
|
|
874
861
|
result.enqueued = true;
|
|
875
862
|
|
|
@@ -1658,7 +1645,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1658
1645
|
|
|
1659
1646
|
// Write claimed state (includes any pain changes from above) and release lock
|
|
1660
1647
|
if (queueChanged) {
|
|
1661
|
-
|
|
1648
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
1662
1649
|
}
|
|
1663
1650
|
releaseLock();
|
|
1664
1651
|
for (const sleepTask of sleepReflectionTasks) {
|
|
@@ -1912,7 +1899,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1912
1899
|
freshQueue[idx] = sleepTask;
|
|
1913
1900
|
}
|
|
1914
1901
|
}
|
|
1915
|
-
|
|
1902
|
+
atomicWriteFileSync(queuePath, JSON.stringify(freshQueue, null, 2));
|
|
1916
1903
|
|
|
1917
1904
|
// Log completions to EvolutionLogger
|
|
1918
1905
|
for (const sleepTask of sleepReflectionTasks) {
|
|
@@ -2005,10 +1992,14 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
2005
1992
|
};
|
|
2006
1993
|
|
|
2007
1994
|
// Dispatch LLM subagent via CorrectionObserverWorkflowManager
|
|
1995
|
+
const subagent = api?.runtime?.subagent;
|
|
1996
|
+
if (!subagent) {
|
|
1997
|
+
throw new Error('[PD:EvolutionWorker] subagent runtime not available for keyword_optimization');
|
|
1998
|
+
}
|
|
2008
1999
|
const manager = new CorrectionObserverWorkflowManager({
|
|
2009
2000
|
workspaceDir: wctx.workspaceDir,
|
|
2010
2001
|
logger,
|
|
2011
|
-
subagent
|
|
2002
|
+
subagent,
|
|
2012
2003
|
agentSession: api?.runtime?.agent?.session,
|
|
2013
2004
|
});
|
|
2014
2005
|
|
|
@@ -2022,7 +2013,11 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
2022
2013
|
workflowId = handle.workflowId;
|
|
2023
2014
|
koTask.resultRef = workflowId;
|
|
2024
2015
|
} else {
|
|
2025
|
-
|
|
2016
|
+
// isPolling implies resultRef exists (checked above)
|
|
2017
|
+
workflowId = koTask.resultRef;
|
|
2018
|
+
if (!workflowId) {
|
|
2019
|
+
throw new Error(`[PD:EvolutionWorker] keyword_optimization task ${koTask.id} has no resultRef in polling mode`);
|
|
2020
|
+
}
|
|
2026
2021
|
}
|
|
2027
2022
|
|
|
2028
2023
|
// Poll workflow state
|
|
@@ -2097,7 +2092,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
2097
2092
|
}
|
|
2098
2093
|
|
|
2099
2094
|
if (queueChanged) {
|
|
2100
|
-
|
|
2095
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
2101
2096
|
}
|
|
2102
2097
|
|
|
2103
2098
|
// Pipeline observability: log stage-level summary at end of cycle
|
|
@@ -2215,7 +2210,7 @@ export async function registerEvolutionTaskSession(
|
|
|
2215
2210
|
if (!task.started_at) {
|
|
2216
2211
|
task.started_at = new Date().toISOString();
|
|
2217
2212
|
}
|
|
2218
|
-
|
|
2213
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
2219
2214
|
return true;
|
|
2220
2215
|
} finally {
|
|
2221
2216
|
releaseLock();
|
|
@@ -2255,7 +2250,7 @@ interface WorkerStatusReport {
|
|
|
2255
2250
|
function writeWorkerStatus(stateDir: string, report: WorkerStatusReport): void {
|
|
2256
2251
|
try {
|
|
2257
2252
|
const statusPath = path.join(stateDir, 'worker-status.json');
|
|
2258
|
-
|
|
2253
|
+
atomicWriteFileSync(statusPath, JSON.stringify(report, null, 2));
|
|
2259
2254
|
} catch (statusErr) {
|
|
2260
2255
|
// Non-critical: worker-status.json is for monitoring, failure is acceptable
|
|
2261
2256
|
// (no logger available in this standalone helper)
|
|
@@ -2286,7 +2281,7 @@ async function processEvolutionQueueWithResult(
|
|
|
2286
2281
|
const purgeResult = purgeStaleFailedTasks(queue, logger);
|
|
2287
2282
|
if (purgeResult.purged > 0) {
|
|
2288
2283
|
// Write back the cleaned queue
|
|
2289
|
-
|
|
2284
|
+
atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
2290
2285
|
}
|
|
2291
2286
|
|
|
2292
2287
|
queueResult.total = queue.length;
|