principles-disciple 1.113.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.113.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.113.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';
@@ -67,7 +69,7 @@ function getNextActionForError(category?: string): string {
67
69
  return 'Run: pd runtime internalization run-once --runner dreamer --runtime config --json to isolate the failure.';
68
70
  }
69
71
 
70
- async function runConsumerCycle(
72
+ export async function runConsumerCycle(
71
73
  workspaceDir: string,
72
74
  logger: PluginLogger,
73
75
  ): Promise<void> {
@@ -144,9 +146,11 @@ async function runConsumerCycle(
144
146
  return;
145
147
  }
146
148
 
149
+ const runtimeKind = runtimeConfigResult.runtimeKind;
150
+
147
151
  const orchestrator = new InternalizationOrchestrator(
148
152
  { stateManager },
149
- { owner: 'auto-consumer', runtimeKind: 'config', dryRun: true },
153
+ { owner: 'auto-consumer', runtimeKind, dryRun: true },
150
154
  );
151
155
 
152
156
  const wakeResult = await orchestrator.wakeOnce('dreamer');
@@ -163,15 +167,25 @@ async function runConsumerCycle(
163
167
  return;
164
168
  }
165
169
 
166
- const adapter = new PiAiRuntimeAdapter({
167
- provider: runtimeConfigResult.provider ?? 'openai',
168
- model: runtimeConfigResult.model ?? 'gpt-4o',
169
- apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
170
- maxRetries: runtimeConfigResult.maxRetries,
171
- timeoutMs: runtimeConfigResult.timeoutMs,
172
- baseUrl: runtimeConfigResult.baseUrl,
173
- workspace: workspaceDir,
174
- });
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
+ }
175
189
 
176
190
  const validator = new DefaultDreamerValidator();
177
191
  const runner = new DreamerRunner(
@@ -184,7 +198,7 @@ async function runConsumerCycle(
184
198
  },
185
199
  {
186
200
  owner: 'auto-consumer',
187
- runtimeKind: 'config',
201
+ runtimeKind,
188
202
  },
189
203
  );
190
204
 
@@ -195,7 +209,26 @@ async function runConsumerCycle(
195
209
  taskKind: 'dreamer',
196
210
  }));
197
211
 
198
- 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
+ }
199
232
 
200
233
  if (runResult.status === 'succeeded') {
201
234
  const commitResult = await orchestrator.commitNextTaskProposal(taskId);
@@ -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
+ });