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/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, resolveWorkspaceDirFromApi } from './core/path-resolver.js';
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/max-params, @typescript-eslint/no-explicit-any
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 ?? DEFAULT_SYNC_INTERVAL_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: DEFAULT_TTL_MS,
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
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
- fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2), 'utf8');
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: api?.runtime?.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
- workflowId = koTask.resultRef!;
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
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
- fs.writeFileSync(statusPath, JSON.stringify(report, null, 2), 'utf8');
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
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
2284
+ atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
2290
2285
  }
2291
2286
 
2292
2287
  queueResult.total = queue.length;