principles-disciple 1.113.0 → 1.114.1
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/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.114.1",
|
|
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.
|
|
3
|
+
"version": "1.114.1",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@typescript-eslint/parser": "^8.58.0",
|
|
55
55
|
"@vitest/coverage-v8": "^4.1.8",
|
|
56
56
|
"esbuild": "^0.28.0",
|
|
57
|
-
"eslint": "^10.
|
|
57
|
+
"eslint": "^10.5.0",
|
|
58
58
|
"jsdom": "^29.1.1",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
60
|
"vite": "^8.0.16",
|
|
@@ -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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
201
|
+
runtimeKind,
|
|
188
202
|
},
|
|
189
203
|
);
|
|
190
204
|
|
|
@@ -195,7 +209,26 @@ async function runConsumerCycle(
|
|
|
195
209
|
taskKind: 'dreamer',
|
|
196
210
|
}));
|
|
197
211
|
|
|
198
|
-
|
|
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
|
+
});
|