principles-disciple 1.112.0 → 1.114.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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.112.0",
5
+ "version": "1.114.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.112.0",
3
+ "version": "1.114.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -5,12 +5,14 @@ import {
5
5
  DreamerRunner,
6
6
  DefaultDreamerValidator,
7
7
  PiAiRuntimeAdapter,
8
+ OpenClawCliRuntimeAdapter,
8
9
  storeEmitter,
9
10
  resolveRuntimeConfigFromPdConfig,
10
11
  isRuntimeConfigError,
11
12
  computeConsumerDecision,
12
13
  InternalizationQueueReadModel,
13
14
  MVP_CORE_TASK_KINDS,
15
+ type PDRuntimeAdapter,
14
16
  } from '@principles/core/runtime-v2';
15
17
  import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
16
18
  import { SystemLogger } from '../core/system-logger.js';
@@ -45,7 +47,29 @@ function formatRunOnceCommand(workspaceDir: string): string {
45
47
  return `pd runtime internalization run-once --workspace "${workspaceDir}" --runner dreamer --runtime config --json`;
46
48
  }
47
49
 
48
- async function runConsumerCycle(
50
+ function getNextActionForError(category?: string): string {
51
+ if (category === 'lease_conflict') {
52
+ return 'Retry later or check for concurrent worker processes.';
53
+ }
54
+ if (category === 'timeout') {
55
+ return 'Check model provider service status/latency, or increase timeout settings in workflows.yaml.';
56
+ }
57
+ if (category === 'cancelled') {
58
+ return 'Re-enqueue or restart the task if it was cancelled by mistake.';
59
+ }
60
+ if (category === 'output_invalid') {
61
+ return 'Verify if model outputs conform to expected schema and adjust prompt or validation templates if needed.';
62
+ }
63
+ if (category === 'input_invalid') {
64
+ return 'Check predecessor task outputs and database integrity for malformed input references.';
65
+ }
66
+ if (category === 'max_attempts_exceeded') {
67
+ return 'Investigate persistent failures, correct the root issue, and clear last_error or reset attempt count.';
68
+ }
69
+ return 'Run: pd runtime internalization run-once --runner dreamer --runtime config --json to isolate the failure.';
70
+ }
71
+
72
+ export async function runConsumerCycle(
49
73
  workspaceDir: string,
50
74
  logger: PluginLogger,
51
75
  ): Promise<void> {
@@ -105,6 +129,10 @@ async function runConsumerCycle(
105
129
  });
106
130
  const snapshot = await readModel.getSnapshot();
107
131
 
132
+ if (snapshot.readyTasks.length > 5) {
133
+ logger.warn(`[PD:AutoConsumer] Backlog detected: ${snapshot.readyTasks.length} tasks ready. Processing only one task.`);
134
+ }
135
+
108
136
  const decision = computeConsumerDecision({
109
137
  autoConsumerEnabled: true,
110
138
  readyTaskCount: snapshot.readyTasks.length,
@@ -118,14 +146,16 @@ async function runConsumerCycle(
118
146
  return;
119
147
  }
120
148
 
149
+ const runtimeKind = runtimeConfigResult.runtimeKind;
150
+
121
151
  const orchestrator = new InternalizationOrchestrator(
122
152
  { stateManager },
123
- { owner: 'auto-consumer', runtimeKind: 'config', dryRun: false },
153
+ { owner: 'auto-consumer', runtimeKind, dryRun: true },
124
154
  );
125
155
 
126
156
  const wakeResult = await orchestrator.wakeOnce('dreamer');
127
157
 
128
- if (wakeResult.decision !== 'leased') {
158
+ if (wakeResult.decision !== 'would_lease') {
129
159
  const skipPayload: Record<string, unknown> = {
130
160
  decision: wakeResult.decision,
131
161
  };
@@ -137,15 +167,25 @@ async function runConsumerCycle(
137
167
  return;
138
168
  }
139
169
 
140
- const adapter = new PiAiRuntimeAdapter({
141
- provider: runtimeConfigResult.provider ?? 'openai',
142
- model: runtimeConfigResult.model ?? 'gpt-4o',
143
- apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
144
- maxRetries: runtimeConfigResult.maxRetries,
145
- timeoutMs: runtimeConfigResult.timeoutMs,
146
- baseUrl: runtimeConfigResult.baseUrl,
147
- workspace: workspaceDir,
148
- });
170
+ let adapter: PDRuntimeAdapter;
171
+ if (runtimeKind === 'pi-ai') {
172
+ adapter = new PiAiRuntimeAdapter({
173
+ provider: runtimeConfigResult.provider ?? 'openai',
174
+ model: runtimeConfigResult.model ?? 'gpt-4o',
175
+ apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
176
+ maxRetries: runtimeConfigResult.maxRetries,
177
+ timeoutMs: runtimeConfigResult.timeoutMs,
178
+ baseUrl: runtimeConfigResult.baseUrl,
179
+ workspace: workspaceDir,
180
+ });
181
+ } else if (runtimeKind === 'openclaw-cli') {
182
+ adapter = new OpenClawCliRuntimeAdapter({
183
+ runtimeMode: runtimeConfigResult.openclawMode ?? 'default',
184
+ workspaceDir: workspaceDir,
185
+ });
186
+ } else {
187
+ throw new Error(`Unsupported runtime kind resolved for auto-consumer: ${runtimeKind}`);
188
+ }
149
189
 
150
190
  const validator = new DefaultDreamerValidator();
151
191
  const runner = new DreamerRunner(
@@ -158,7 +198,7 @@ async function runConsumerCycle(
158
198
  },
159
199
  {
160
200
  owner: 'auto-consumer',
161
- runtimeKind: 'config',
201
+ runtimeKind,
162
202
  },
163
203
  );
164
204
 
@@ -169,7 +209,26 @@ async function runConsumerCycle(
169
209
  taskKind: 'dreamer',
170
210
  }));
171
211
 
172
- const runResult = await runner.run(taskId);
212
+ let runResult;
213
+ try {
214
+ runResult = await runner.run(taskId);
215
+ } catch (runErr) {
216
+ logger.error(`[PD:AutoConsumer] Runner crashed for task ${taskId}: ${String(runErr)}`);
217
+ try {
218
+ const task = await stateManager.getTask(taskId);
219
+ const failureReason = `Unhandled runner exception: ${runErr instanceof Error ? runErr.message : String(runErr)}`;
220
+ if (task && stateManager.getRetryPolicy().shouldRetry(task)) {
221
+ await stateManager.markTaskRetryWait(taskId, 'execution_failed', failureReason);
222
+ logger.info(`[PD:AutoConsumer] Marked task ${taskId} as retry_wait.`);
223
+ } else {
224
+ await stateManager.markTaskFailed(taskId, 'execution_failed', failureReason);
225
+ logger.info(`[PD:AutoConsumer] Marked task ${taskId} as failed.`);
226
+ }
227
+ } catch (dbErr) {
228
+ logger.error(`[PD:AutoConsumer] Failed to update state for crashed task ${taskId}: ${String(dbErr)}`);
229
+ }
230
+ throw runErr;
231
+ }
173
232
 
174
233
  if (runResult.status === 'succeeded') {
175
234
  const commitResult = await orchestrator.commitNextTaskProposal(taskId);
@@ -182,11 +241,19 @@ async function runConsumerCycle(
182
241
  `[PD:AutoConsumer] Task ${taskId} succeeded. Successor: ${commitResult.decision}`,
183
242
  );
184
243
  } else {
244
+ const errorCategory = runResult.errorCategory;
245
+ const failureReason = runResult.failureReason;
246
+ const nextAction = getNextActionForError(errorCategory);
185
247
  SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_TASK_FAILED', JSON.stringify({
186
248
  taskId,
187
249
  status: runResult.status,
250
+ errorCategory,
251
+ failureReason,
252
+ nextAction,
188
253
  }));
189
- logger.warn(`[PD:AutoConsumer] Task ${taskId} status: ${runResult.status}`);
254
+ logger.warn(
255
+ `[PD:AutoConsumer] Task ${taskId} status: ${runResult.status}. Category: ${errorCategory}. Reason: ${failureReason}. Next Action: ${nextAction}`
256
+ );
190
257
  }
191
258
  } catch (err) {
192
259
  SystemLogger.log(workspaceDir, 'INTERNALIZATION_CONSUMER_ERROR', String(err));
@@ -0,0 +1,219 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as yaml from 'js-yaml';
6
+ import Database from 'better-sqlite3';
7
+ import { runConsumerCycle } from '../src/service/internalization-auto-consumer-service.js';
8
+ import { DreamerRunner, OpenClawCliRuntimeAdapter, PiAiRuntimeAdapter } from '@principles/core/runtime-v2';
9
+
10
+ // We mock the dictionary service to prevent unwanted DB lookups
11
+ vi.mock('../src/core/dictionary-service.js', () => ({
12
+ DictionaryService: { get: vi.fn(() => ({ flush: vi.fn() })) },
13
+ }));
14
+
15
+ describe('Auto-Consumer Unhandled Runner Crash Recovery', () => {
16
+ let workspaceDir: string;
17
+ let dbPath: string;
18
+
19
+ beforeEach(() => {
20
+ // Create a temporary workspace
21
+ workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-auto-consumer-recovery-'));
22
+ fs.mkdirSync(path.join(workspaceDir, '.pd'), { recursive: true });
23
+ dbPath = path.join(workspaceDir, '.pd', 'state.db');
24
+
25
+ // Create a minimal state.db with tasks and runs tables
26
+ const db = new Database(dbPath);
27
+ db.exec(`
28
+ CREATE TABLE tasks (
29
+ task_id TEXT PRIMARY KEY,
30
+ task_kind TEXT,
31
+ status TEXT,
32
+ result_ref TEXT,
33
+ lease_owner TEXT,
34
+ lease_expires_at TEXT,
35
+ attempt_count INTEGER DEFAULT 0,
36
+ max_attempts INTEGER DEFAULT 3,
37
+ last_error TEXT,
38
+ diagnostic_json TEXT,
39
+ created_at TEXT,
40
+ updated_at TEXT
41
+ );
42
+ CREATE TABLE runs (
43
+ run_id TEXT PRIMARY KEY,
44
+ task_id TEXT,
45
+ runtime_kind TEXT,
46
+ execution_status TEXT,
47
+ started_at TEXT,
48
+ ended_at TEXT,
49
+ attempt_number INTEGER,
50
+ output_ref TEXT,
51
+ reason TEXT,
52
+ error_category TEXT,
53
+ created_at TEXT,
54
+ updated_at TEXT
55
+ );
56
+ `);
57
+ db.close();
58
+
59
+ // Write a mock config.yaml
60
+ const configPath = path.join(workspaceDir, '.pd', 'config.yaml');
61
+ const config = {
62
+ version: 1,
63
+ features: {
64
+ internalization_auto_consumer: { category: 'quiet', enabled: true },
65
+ },
66
+ runtimeProfiles: {
67
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
68
+ },
69
+ internalAgents: {
70
+ defaultRuntime: 'openclaw.default',
71
+ agents: {
72
+ dreamer: { enabled: true },
73
+ },
74
+ },
75
+ };
76
+ fs.writeFileSync(configPath, yaml.dump(config, { schema: yaml.JSON_SCHEMA }), 'utf8');
77
+ });
78
+
79
+ afterEach(() => {
80
+ vi.restoreAllMocks();
81
+ try {
82
+ fs.rmSync(workspaceDir, { recursive: true, force: true });
83
+ } catch { /* best-effort */ }
84
+ });
85
+
86
+ it('recovers stuck task and run states when runner.run throws an unhandled crash', async () => {
87
+ // Insert a pending task in DB
88
+ const db = new Database(dbPath);
89
+ const now = new Date().toISOString();
90
+ const diagJson = JSON.stringify({
91
+ pi_metadata: {
92
+ dependencyTaskIds: [],
93
+ channel: 'prompt',
94
+ timeoutMs: 300000,
95
+ inputArtifactRefs: [],
96
+ outputArtifactRefs: []
97
+ }
98
+ });
99
+ db.prepare(`
100
+ INSERT INTO tasks (task_id, task_kind, status, attempt_count, max_attempts, diagnostic_json, created_at, updated_at)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
102
+ `).run('dreamer-task-1', 'dreamer', 'pending', 0, 3, diagJson, now, now);
103
+ db.close();
104
+
105
+ // Mock DreamerRunner.run to crash with an unhandled exception after leasing the task
106
+ vi.spyOn(DreamerRunner.prototype, 'run').mockImplementation(async (tId) => {
107
+ const d = new Database(dbPath);
108
+ const nowIso = new Date().toISOString();
109
+ const expiresAt = new Date(Date.now() + 300000).toISOString();
110
+ d.prepare(`
111
+ UPDATE tasks SET status = 'leased', lease_owner = 'auto-consumer', lease_expires_at = ?, attempt_count = 1 WHERE task_id = ?
112
+ `).run(expiresAt, tId);
113
+ d.prepare(`
114
+ INSERT INTO runs (run_id, task_id, runtime_kind, execution_status, started_at, attempt_number, created_at, updated_at)
115
+ VALUES (?, ?, 'pi-ai', 'running', ?, 1, ?, ?)
116
+ `).run(`run_${tId}_1`, tId, nowIso, nowIso, nowIso);
117
+ d.close();
118
+
119
+ throw new Error('Simulated runner unhandled crash');
120
+ });
121
+
122
+ // Run the cycle
123
+ const mockLogger = {
124
+ info: vi.fn(),
125
+ warn: vi.fn(),
126
+ error: vi.fn(),
127
+ debug: vi.fn(),
128
+ };
129
+
130
+ await runConsumerCycle(workspaceDir, mockLogger);
131
+
132
+ // Verify database was updated to release lease and mark failed/retry_wait
133
+ const dbCheck = new Database(dbPath);
134
+ interface TaskRow {
135
+ status: string;
136
+ attempt_count: number;
137
+ lease_owner: string | null;
138
+ lease_expires_at: string | null;
139
+ last_error: string | null;
140
+ }
141
+ interface RunRow {
142
+ execution_status: string;
143
+ reason: string | null;
144
+ error_category: string | null;
145
+ }
146
+ const task = dbCheck.prepare('SELECT status, attempt_count, lease_owner, lease_expires_at, last_error FROM tasks WHERE task_id = ?').get('dreamer-task-1') as TaskRow | undefined;
147
+ const run = dbCheck.prepare('SELECT execution_status, reason, error_category FROM runs WHERE task_id = ?').get('dreamer-task-1') as RunRow | undefined;
148
+ dbCheck.close();
149
+
150
+ expect(task).toBeDefined();
151
+ expect(run).toBeDefined();
152
+ if (task && run) {
153
+ // The task should no longer be leased and should be marked failed or retry_wait (since attempt_count 1 <= max_attempts 3, it should be retry_wait)
154
+ expect(task.status).toBe('retry_wait');
155
+ expect(task.lease_owner).toBeNull();
156
+ expect(task.last_error).toBe('execution_failed');
157
+
158
+ // The run should be marked failed
159
+ expect(run.execution_status).toBe('failed');
160
+ expect(run.error_category).toBe('execution_failed');
161
+ expect(run.reason).toContain('Unhandled runner exception: Simulated runner unhandled crash');
162
+ }
163
+ });
164
+
165
+ it('does not construct PiAiRuntimeAdapter fallback when config specifies openclaw.default runtime', async () => {
166
+ // Insert a pending task in DB
167
+ const db = new Database(dbPath);
168
+ const now = new Date().toISOString();
169
+ const diagJson = JSON.stringify({
170
+ pi_metadata: {
171
+ dependencyTaskIds: [],
172
+ channel: 'prompt',
173
+ timeoutMs: 300000,
174
+ inputArtifactRefs: [],
175
+ outputArtifactRefs: []
176
+ }
177
+ });
178
+ db.prepare(`
179
+ INSERT INTO tasks (task_id, task_kind, status, attempt_count, max_attempts, diagnostic_json, created_at, updated_at)
180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
181
+ `).run('dreamer-task-2', 'dreamer', 'pending', 0, 3, diagJson, now, now);
182
+ db.close();
183
+
184
+ function hasRuntimeAdapter(value: unknown): value is { runtimeAdapter: unknown } {
185
+ return typeof value === 'object' && value !== null && Object.hasOwn(value, 'runtimeAdapter');
186
+ }
187
+
188
+ // Track the runtimeAdapter passed to DreamerRunner
189
+ let capturedAdapter: unknown = null;
190
+ vi.spyOn(DreamerRunner.prototype, 'run').mockImplementation(async function (this: unknown, _tId) {
191
+ if (hasRuntimeAdapter(this)) {
192
+ capturedAdapter = this.runtimeAdapter;
193
+ }
194
+ return {
195
+ status: 'succeeded',
196
+ runId: 'mock-run',
197
+ artifactId: 'mock-art',
198
+ resultRef: 'mock-ref',
199
+ };
200
+ });
201
+
202
+ const mockLogger = {
203
+ info: vi.fn(),
204
+ warn: vi.fn(),
205
+ error: vi.fn(),
206
+ debug: vi.fn(),
207
+ };
208
+
209
+ await runConsumerCycle(workspaceDir, mockLogger);
210
+
211
+ // Verify a runner run was triggered and we captured the adapter
212
+ expect(capturedAdapter).toBeDefined();
213
+ expect(capturedAdapter).not.toBeNull();
214
+
215
+ // The captured adapter should be an instance of OpenClawCliRuntimeAdapter, not PiAiRuntimeAdapter
216
+ expect(capturedAdapter).toBeInstanceOf(OpenClawCliRuntimeAdapter);
217
+ expect(capturedAdapter).not.toBeInstanceOf(PiAiRuntimeAdapter);
218
+ });
219
+ });