principles-disciple 1.34.2 → 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.
@@ -13,11 +13,11 @@ import type { RuntimeDirectDriver } from './runtime-direct-driver.js';
13
13
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
14
14
  import { buildCritiquePromptV2 } from '../../tools/critique-prompt.js';
15
15
  import { WorkflowManagerBase } from './workflow-manager-base.js';
16
+ import { DEEP_REFLECT_TTL_MS } from '../../config/defaults/runtime.js';
16
17
 
17
18
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
18
19
 
19
20
  const DEFAULT_TIMEOUT_MS = 60_000; // Deep-reflect needs more time than empathy
20
- const DEFAULT_TTL_MS = 10 * 60 * 1000;
21
21
 
22
22
  export interface DeepReflectWorkflowOptions {
23
23
  workspaceDir: string;
@@ -37,7 +37,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
37
37
  workflowType: 'deep-reflect',
38
38
  sessionPrefix: WORKFLOW_SESSION_PREFIX,
39
39
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
40
- defaultTtlMs: DEFAULT_TTL_MS,
40
+ defaultTtlMs: DEEP_REFLECT_TTL_MS,
41
41
  });
42
42
  }
43
43
 
@@ -70,7 +70,7 @@ export class DeepReflectWorkflowManager extends WorkflowManagerBase {
70
70
  }
71
71
 
72
72
 
73
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
73
+
74
74
  protected override generateWorkflowId(): string {
75
75
  return `wf_dr_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
76
76
  }
@@ -98,7 +98,7 @@ export const deepReflectWorkflowSpec: SubagentWorkflowSpec<DeepReflectResult> =
98
98
  workflowType: 'deep-reflect',
99
99
  transport: 'runtime_direct',
100
100
  timeoutMs: 60_000,
101
- ttlMs: 10 * 60 * 1000,
101
+ ttlMs: DEEP_REFLECT_TTL_MS,
102
102
  shouldDeleteSessionAfterFinalize: true,
103
103
 
104
104
  buildPrompt(taskInput: unknown, ctx: DeepReflectBuildPromptContext): string {
@@ -14,11 +14,11 @@ import { trackFriction } from '../../core/session-tracker.js';
14
14
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
15
15
  import { WorkflowManagerBase } from './workflow-manager-base.js';
16
16
  import { normalizeSeverity } from '../../core/empathy-types.js';
17
+ import { WORKFLOW_TTL_MS } from '../../config/defaults/runtime.js';
17
18
 
18
19
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
19
20
 
20
21
  const DEFAULT_TIMEOUT_MS = 30_000;
21
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
22
22
 
23
23
  export interface EmpathyObserverWorkflowOptions {
24
24
  workspaceDir: string;
@@ -38,7 +38,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
38
38
  workflowType: 'empathy-observer',
39
39
  sessionPrefix: WORKFLOW_SESSION_PREFIX,
40
40
  defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
41
- defaultTtlMs: DEFAULT_TTL_MS,
41
+ defaultTtlMs: WORKFLOW_TTL_MS,
42
42
  });
43
43
  }
44
44
 
@@ -71,7 +71,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
71
71
  }
72
72
 
73
73
 
74
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
74
+
75
75
  protected override createWorkflowMetadata<TResult>(
76
76
  spec: SubagentWorkflowSpec<TResult>,
77
77
  options: {
@@ -105,7 +105,7 @@ export class EmpathyObserverWorkflowManager extends WorkflowManagerBase {
105
105
  }
106
106
 
107
107
 
108
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
108
+
109
109
  protected override generateWorkflowId(): string {
110
110
  return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
111
111
  }
@@ -36,7 +36,7 @@ import {
36
36
  } from '../nocturnal-service.js';
37
37
  import { type TrinityStageFailure, type TrinityResult } from '../../core/nocturnal-trinity.js';
38
38
  import type { TrinityRuntimeAdapter } from '../../core/nocturnal-trinity.js';
39
- import type { RecentPainContext } from '../evolution-worker.js';
39
+ import type { RecentPainContext } from './types.js';
40
40
  import * as fs from 'fs';
41
41
  import * as path from 'path';
42
42
  import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
@@ -394,7 +394,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
394
394
  }
395
395
 
396
396
 
397
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
397
+
398
398
  async notifyLifecycleEvent(
399
399
 
400
400
  _workflowId: string,
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type {
13
13
  NocturnalArtifact,
14
+ ArbiterResult,
14
15
  } from '../../core/nocturnal-arbiter.js';
15
16
  import type {
16
17
  BoundedAction,
@@ -18,12 +19,16 @@ import type {
18
19
  import type {
19
20
  NocturnalSessionSnapshot,
20
21
  } from '../../core/nocturnal-trajectory-extractor.js';
21
- import type {
22
- NocturnalRunDiagnostics,
23
- } from '../nocturnal-service.js';
24
22
  import type {
25
23
  TrinityResult,
26
24
  } from '../../core/nocturnal-trinity.js';
25
+ import type {
26
+ IdleCheckResult,
27
+ PreflightCheckResult,
28
+ } from '../nocturnal-runtime.js';
29
+ import type {
30
+ NocturnalSelectionResult,
31
+ } from '../nocturnal-target-selector.js';
27
32
 
28
33
  // ── Workflow Transport ────────────────────────────────────────────────────────
29
34
 
@@ -366,6 +371,27 @@ export type { PluginLogger } from '../../openclaw-sdk.js';
366
371
 
367
372
  // ── Nocturnal Workflow Types ───────────────────────────────────────────────────
368
373
 
374
+ /**
375
+ * Recent pain context for sleep_reflection tasks.
376
+ * Used by target selector for ranking bias and context enrichment.
377
+ * Originally from evolution-worker.ts, moved here to break circular dependency.
378
+ */
379
+ export interface RecentPainContext {
380
+ /** Most recent unresolved pain event */
381
+ mostRecent: {
382
+ score: number;
383
+ source: string;
384
+ reason: string;
385
+ timestamp: string;
386
+ /** Session ID where the pain occurred */
387
+ sessionId: string;
388
+ } | null;
389
+ /** Count of pain events in the recent window (for signal strength) */
390
+ recentPainCount: number;
391
+ /** Highest pain score in the recent window */
392
+ recentMaxPainScore: number;
393
+ }
394
+
369
395
  /**
370
396
  * Nocturnal workflow result type.
371
397
  * Mirrors NocturnalRunResult from nocturnal-service.ts (per D-02).
@@ -381,3 +407,43 @@ export type NocturnalResult = {
381
407
  diagnostics: NocturnalRunDiagnostics;
382
408
  trinityTelemetry?: TrinityResult['telemetry'];
383
409
  };
410
+
411
+ /**
412
+ * Diagnostics from each pipeline stage.
413
+ * Duplicated from nocturnal-service.ts to break circular dependency.
414
+ */
415
+ export interface NocturnalRunDiagnostics {
416
+ /** Pre-flight check result */
417
+ preflight: PreflightCheckResult | null;
418
+ /** Selection result */
419
+ selection: NocturnalSelectionResult | null;
420
+ /** Idle check result */
421
+ idle: IdleCheckResult | null;
422
+ /** Whether Trinity chain was attempted */
423
+ trinityAttempted: boolean;
424
+ /** Trinity result (if trinityAttempted === true) */
425
+ trinityResult: TrinityResult | null;
426
+ /** Which chain mode was used */
427
+ chainModeUsed: 'trinity' | 'single-reflector' | null;
428
+ /** Arbiter validation result */
429
+ arbiterResult: ArbiterResult | null;
430
+ /** Executability validation result (if arbiter passed) */
431
+ executabilityResult: { executable: boolean; failures: string[] } | null;
432
+ /** Whether artifact was persisted */
433
+ persisted: boolean;
434
+ /** Persistence path (if persisted) */
435
+ persistedPath?: string;
436
+ /** Code-candidate sidecar diagnostics */
437
+ artificer: NocturnalArtificerDiagnostics;
438
+ }
439
+
440
+ export interface NocturnalArtificerDiagnostics {
441
+ status: 'skipped' | 'validation_failed' | 'persisted_candidate';
442
+ reason?:
443
+ | 'behavioral_artifact_unavailable'
444
+ | 'no_deterministic_rule'
445
+ | 'persistence_failed'
446
+ | 'cancelled';
447
+ validationErrors?: string[];
448
+ persistedPath?: string;
449
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shadow Observation Fingerprint Utilities
3
+ *
4
+ * Computes fingerprints for shadow task routing and tracking.
5
+ */
6
+
7
+ import * as crypto from 'crypto';
8
+ import type { PluginHookSubagentSpawningEvent } from '../openclaw-sdk.js';
9
+
10
+ /**
11
+ * PD local worker profiles that are managed by the shadow routing policy.
12
+ */
13
+ const _pdLocalProfiles = new Set(['local-reader', 'local-editor']);
14
+ export const PD_LOCAL_PROFILES: ReadonlySet<string> = _pdLocalProfiles;
15
+
16
+ /**
17
+ * Check if a profile is a local PD profile.
18
+ */
19
+ export function isLocalProfile(profile: string): boolean {
20
+ return _pdLocalProfiles.has(profile);
21
+ }
22
+
23
+ const RUNTIME_SHADOW_FINGERPRINT_HEX_LENGTH = 16;
24
+
25
+ /**
26
+ * Compute a fingerprint for runtime shadow task tracking.
27
+ * Used to correlate shadow routing decisions with subagent lifecycle events.
28
+ */
29
+ export function computeRuntimeShadowTaskFingerprint(
30
+ event: PluginHookSubagentSpawningEvent,
31
+ ): string {
32
+ const payload = {
33
+ childSessionKey: event.childSessionKey,
34
+ agentId: event.agentId,
35
+ label: event.label ?? '',
36
+ mode: event.mode,
37
+ threadRequested: event.threadRequested,
38
+ requesterChannel: event.requester?.channel ?? '',
39
+ requesterThreadId: event.requester?.threadId ?? '',
40
+ };
41
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, RUNTIME_SHADOW_FINGERPRINT_HEX_LENGTH);
42
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Workspace Directory Resolution Utilities
3
+ *
4
+ * Shared helpers for resolving workspace directories across commands and hooks.
5
+ */
6
+
7
+ import type { OpenClawPluginApi } from '../openclaw-sdk.js';
8
+ import { validateWorkspaceDir, type WorkspaceResolutionContext } from '../core/workspace-dir-validation.js';
9
+ import { resolveWorkspaceDir } from '../core/workspace-dir-service.js';
10
+ import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
11
+
12
+ /**
13
+ * Resolve workspace directory for command execution.
14
+ *
15
+ * Chain: ctx.workspaceDir → resolveWorkspaceDirFromApi (official OpenClaw API + env vars)
16
+ *
17
+ * CRITICAL: Throws if workspaceDir cannot be resolved. Silent failures are dangerous
18
+ * because commands might operate on the wrong directory.
19
+ */
20
+ export function resolveCommandWorkspaceDir(
21
+ api: OpenClawPluginApi,
22
+ ctx: { workspaceDir?: string },
23
+ ): string {
24
+ // 1. Direct from command context (most reliable — set by OpenClaw for current session)
25
+ if (ctx.workspaceDir) {
26
+ const issue = validateWorkspaceDir(ctx.workspaceDir);
27
+ if (!issue) return ctx.workspaceDir;
28
+ api.logger.error(`[PD:Command] ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
29
+ }
30
+
31
+ // 2. Official OpenClaw API → env vars → config file
32
+ const resolved = resolveWorkspaceDirFromApi(api);
33
+ if (resolved) return resolved;
34
+
35
+ // CRITICAL FAILURE: Cannot determine workspace directory
36
+ const errorMsg = `[PD:Command] CRITICAL: Cannot resolve workspace directory. ` +
37
+ `ctx.workspaceDir="${ctx.workspaceDir}" is invalid, and all fallbacks failed. ` +
38
+ `Commands will NOT execute to prevent data corruption.`;
39
+ api.logger.error(errorMsg);
40
+
41
+ throw new Error(errorMsg);
42
+ }
43
+
44
+ /**
45
+ * Resolve workspace directory for tool hook execution (safe version).
46
+ * Returns undefined instead of throwing if resolution fails.
47
+ */
48
+ export function resolveToolHookWorkspaceDirSafe(
49
+ ctx: WorkspaceResolutionContext,
50
+ api: OpenClawPluginApi,
51
+ source: string,
52
+ ): string | undefined {
53
+ return resolveWorkspaceDir(api, ctx, { source });
54
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import * as os from 'os';
3
- import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-validation.js';
3
+ import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-service.js';
4
4
 
5
5
  const homeDir = os.homedir();
6
6
 
@@ -65,7 +65,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
65
65
 
66
66
  describe('Scenario 2: ctx.workspaceDir is undefined (current OpenClaw behavior)', () => {
67
67
  it('should fallback to agentId resolution', async () => {
68
- const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
68
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
69
69
 
70
70
  const mockApi = createMockApi(testWorkspaceDir);
71
71
  const ctx = {
@@ -80,7 +80,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
80
80
  });
81
81
 
82
82
  it('should refuse to guess a workspace when agentId is also undefined', async () => {
83
- const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
83
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
84
84
 
85
85
  const mockApi = createMockApi(testWorkspaceDir);
86
86
  const ctx = {
@@ -131,7 +131,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
131
131
 
132
132
  describe('Scenario 4: Invalid workspace candidates are rejected', () => {
133
133
  it('should return undefined when all workspace resolution candidates are invalid', async () => {
134
- const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
134
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
135
135
 
136
136
  const mockApi = createMockApi(os.homedir());
137
137
  mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir());
package/vitest.config.ts CHANGED
@@ -1,12 +1,41 @@
1
1
  import { defineConfig } from 'vitest/config';
2
2
 
3
+ /**
4
+ * Vitest configuration with test layering
5
+ *
6
+ * LAYERS:
7
+ * - unit: Mock-based tests, no real DB (fast, parallel)
8
+ * - integration: Tests using real SQLite DB (requires threads pool)
9
+ *
10
+ * USAGE:
11
+ * - npm test → run all tests
12
+ * - npm run test:unit → run unit tests only (fast)
13
+ * - npm run test:integration → run integration tests only
14
+ *
15
+ * WHY threads pool?
16
+ * better-sqlite3 native handles don't clean up properly in fork subprocesses,
17
+ * causing teardown hangs. Threads pool handles this correctly.
18
+ */
19
+
20
+ // Integration tests: use real SQLite database
21
+ const integrationTests = [
22
+ 'tests/core/control-ui-db.test.ts',
23
+ 'tests/core/evolution-logger.test.ts',
24
+ 'tests/core/nocturnal-e2e.test.ts',
25
+ 'tests/core/nocturnal-trajectory-extractor.test.ts',
26
+ 'tests/core/replay-engine.test.ts',
27
+ 'tests/core/trajectory.test.ts',
28
+ 'tests/integration/**/*.test.ts',
29
+ 'tests/integration/**/*.test.tsx',
30
+ 'tests/service/nocturnal-service-code-candidate.test.ts',
31
+ 'tests/service/nocturnal-target-selector.test.ts',
32
+ ];
33
+
3
34
  export default defineConfig({
4
35
  test: {
5
36
  environment: 'node',
6
37
  include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
7
- // vitest 4: pool: 'forks' 默认启用隔离,每个测试文件在独立进程中运行
8
- pool: 'forks',
9
- // 确保测试完成后进程能正常退出
38
+ pool: 'threads',
10
39
  teardownTimeout: 15000,
11
40
  coverage: {
12
41
  provider: 'v8',
@@ -18,6 +47,24 @@ export default defineConfig({
18
47
  branches: 60,
19
48
  statements: 70,
20
49
  },
21
- }
22
- }
23
- });
50
+ },
51
+ // Workspace projects for layered testing
52
+ projects: [
53
+ {
54
+ test: {
55
+ name: 'unit',
56
+ include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
57
+ exclude: integrationTests,
58
+ pool: 'threads',
59
+ },
60
+ },
61
+ {
62
+ test: {
63
+ name: 'integration',
64
+ include: integrationTests,
65
+ pool: 'threads',
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ });