@principles/core 1.84.0 → 1.86.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.
Files changed (36) hide show
  1. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js +52 -0
  2. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts +2 -0
  4. package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts.map +1 -0
  5. package/dist/runtime-v2/__tests__/telemetry-event.test.js +56 -0
  6. package/dist/runtime-v2/__tests__/telemetry-event.test.js.map +1 -0
  7. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts +2 -0
  8. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts.map +1 -0
  9. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js +398 -0
  10. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js.map +1 -0
  11. package/dist/runtime-v2/internalization/artificer-output.d.ts +3 -2
  12. package/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
  13. package/dist/runtime-v2/internalization/artificer-output.js +40 -31
  14. package/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
  15. package/dist/runtime-v2/internalization/artificer-runner.d.ts +59 -38
  16. package/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  17. package/dist/runtime-v2/internalization/artificer-runner.js +121 -395
  18. package/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  19. package/dist/runtime-v2/internalization/philosopher-output.d.ts +8 -2
  20. package/dist/runtime-v2/internalization/philosopher-output.d.ts.map +1 -1
  21. package/dist/runtime-v2/internalization/philosopher-output.js +16 -11
  22. package/dist/runtime-v2/internalization/philosopher-output.js.map +1 -1
  23. package/dist/runtime-v2/internalization/philosopher-runner.d.ts +36 -55
  24. package/dist/runtime-v2/internalization/philosopher-runner.d.ts.map +1 -1
  25. package/dist/runtime-v2/internalization/philosopher-runner.js +117 -401
  26. package/dist/runtime-v2/internalization/philosopher-runner.js.map +1 -1
  27. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +22 -0
  28. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -1
  29. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  30. package/dist/runtime-v2/runner/base-peer-runner.js +5 -1
  31. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  32. package/dist/telemetry-event.d.ts +2 -2
  33. package/dist/telemetry-event.d.ts.map +1 -1
  34. package/dist/telemetry-event.js +88 -0
  35. package/dist/telemetry-event.js.map +1 -1
  36. package/package.json +1 -1
@@ -1,15 +1,15 @@
1
- import { PDRuntimeError } from '../error-categories.js';
1
+ import { PDRuntimeError, isPDErrorCategory } from '../error-categories.js';
2
2
  import { hydratePITaskRecord } from './pitask-metadata.js';
3
- import { RunnerPhase } from '../runner/runner-phase.js';
4
3
  import { PhilosopherPromptBuilder } from './philosopher-prompt-builder.js';
5
4
  import { injectRunnerLineageIfAbsent } from './peer-runner-contracts.js';
6
- const DEFAULT_PHILOSOPHER_RUNNER_OPTIONS = {
5
+ import { BasePeerRunner } from '../runner/base-peer-runner.js';
6
+ export const DEFAULT_PHILOSOPHER_RUNNER_OPTIONS = {
7
7
  pollIntervalMs: 5_000,
8
8
  timeoutMs: 300_000,
9
9
  defaultMaxAttempts: 3,
10
10
  agentId: 'philosopher',
11
11
  };
12
- function resolvePhilosopherRunnerOptions(options) {
12
+ export function resolvePhilosopherRunnerOptions(options) {
13
13
  return {
14
14
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_PHILOSOPHER_RUNNER_OPTIONS.pollIntervalMs,
15
15
  timeoutMs: options.timeoutMs ?? DEFAULT_PHILOSOPHER_RUNNER_OPTIONS.timeoutMs,
@@ -19,120 +19,23 @@ function resolvePhilosopherRunnerOptions(options) {
19
19
  agentId: options.agentId ?? DEFAULT_PHILOSOPHER_RUNNER_OPTIONS.agentId,
20
20
  };
21
21
  }
22
- // ── PhilosopherRunner ──────────────────────────────────────────────────────────
23
- export class PhilosopherRunner {
24
- phase = RunnerPhase.Idle;
25
- resolvedOptions;
26
- stateManager;
27
- runtimeAdapter;
28
- eventEmitter;
22
+ // ── PhilosopherRunner ────────────────────────────────────────────────────────
23
+ export class PhilosopherRunner extends BasePeerRunner {
29
24
  validator;
30
- artifactStore;
31
25
  constructor(deps, options) {
32
- this.stateManager = deps.stateManager;
33
- this.runtimeAdapter = deps.runtimeAdapter;
34
- this.eventEmitter = deps.eventEmitter;
35
- this.validator = deps.validator;
36
- this.artifactStore = deps.artifactStore;
37
- this.resolvedOptions = resolvePhilosopherRunnerOptions(options);
38
- }
39
- get currentPhase() {
40
- return this.phase;
41
- }
42
- emitPhilosopherEvent(eventType, taskId, payload) {
43
- this.eventEmitter.emitTelemetry({
44
- eventType: eventType,
45
- traceId: taskId,
46
- timestamp: new Date().toISOString(),
47
- sessionId: this.resolvedOptions.owner,
48
- agentId: this.resolvedOptions.agentId,
49
- payload,
26
+ super(deps, options, {
27
+ runnerName: 'philosopher',
28
+ expectedTaskKind: 'philosopher',
29
+ defaultAgentId: 'philosopher',
30
+ resultRefPrefix: 'philosopher',
50
31
  });
32
+ this.validator = deps.validator;
51
33
  }
52
- async run(taskId) {
53
- this.phase = RunnerPhase.Idle;
54
- let leasedTask;
55
- try {
56
- leasedTask = await this.stateManager.acquireLease({
57
- taskId,
58
- owner: this.resolvedOptions.owner,
59
- runtimeKind: this.resolvedOptions.runtimeKind,
60
- });
61
- }
62
- catch (error) {
63
- return await this.handleLeaseOrPhaseError(taskId, error);
64
- }
65
- if (leasedTask.taskKind !== 'philosopher') {
66
- this.emitPhilosopherEvent('philosopher_wrong_task_kind', taskId, {
67
- expectedKind: 'philosopher',
68
- actualKind: leasedTask.taskKind,
69
- });
70
- await this.stateManager.markTaskFailed(taskId, 'input_invalid');
71
- return {
72
- status: 'failed',
73
- taskId,
74
- errorCategory: 'input_invalid',
75
- failureReason: `Task kind must be 'philosopher', got '${leasedTask.taskKind}'`,
76
- attemptCount: leasedTask.attemptCount,
77
- };
78
- }
79
- this.emitPhilosopherEvent('philosopher_task_leased', taskId, {
80
- taskKind: 'philosopher',
81
- attemptCount: leasedTask.attemptCount,
82
- });
83
- try {
84
- const storeRunId = await this.resolveStoreRunId(taskId);
85
- this.phase = RunnerPhase.BuildingContext;
86
- const { contextHash, dreamerArtifact, sourceDreamerArtifactId } = await this.buildContext(taskId);
87
- if (!dreamerArtifact) {
88
- return this.retryOrFail({
89
- taskId,
90
- task: leasedTask,
91
- errorCategory: 'input_invalid',
92
- failureReason: 'Dreamer dependency artifact not found',
93
- });
94
- }
95
- this.emitPhilosopherEvent('philosopher_context_built', taskId, { contextHash });
96
- this.phase = RunnerPhase.Invoking;
97
- const runHandle = await this.invokeRuntime({ taskId, contextHash, dreamerArtifact, sourceDreamerArtifactId });
98
- this.emitPhilosopherEvent('philosopher_run_started', taskId, {
99
- runtimeKind: this.resolvedOptions.runtimeKind,
100
- });
101
- this.phase = RunnerPhase.Polling;
102
- const finalStatus = await this.pollUntilTerminal(runHandle);
103
- if (finalStatus.status !== 'succeeded') {
104
- return await this.handleRuntimeFailure(taskId, leasedTask, finalStatus);
105
- }
106
- this.phase = RunnerPhase.FetchingOutput;
107
- const output = await this.fetchAndParseOutput(runHandle.runId);
108
- // Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
109
- injectRunnerLineageIfAbsent(output, 'taskId', taskId);
110
- this.phase = RunnerPhase.Validating;
111
- const validationResult = await this.validator.validate(output, taskId);
112
- if (!validationResult.valid) {
113
- return await this.handleValidationError({
114
- taskId,
115
- task: leasedTask,
116
- errors: validationResult.errors,
117
- errorCategory: validationResult.errorCategory,
118
- });
119
- }
120
- this.emitPhilosopherEvent('philosopher_output_validated', taskId, {
121
- principleTitle: output.principleCandidate.title,
122
- });
123
- return await this.succeedTask({
124
- taskId,
125
- runId: storeRunId,
126
- output,
127
- task: leasedTask,
128
- contextHash,
129
- });
130
- }
131
- catch (error) {
132
- return await this.handlePostLeaseError(taskId, leasedTask, error);
133
- }
34
+ // ── Abstract implementations ───────────────────────────────────────────────
35
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
36
+ get permanentErrorCategories() {
37
+ return new Set(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid']);
134
38
  }
135
- // ── Phase methods ─────────────────────────────────────────────────────────
136
39
  async buildContext(taskId) {
137
40
  const task = await this.stateManager.getTask(taskId);
138
41
  if (!task) {
@@ -141,15 +44,14 @@ export class PhilosopherRunner {
141
44
  const piTask = hydratePITaskRecord(task);
142
45
  const deps = piTask?.dependencyTaskIds ?? [];
143
46
  if (deps.length === 0) {
144
- this.emitPhilosopherEvent('philosopher_no_dependencies', taskId, {});
145
- return { contextHash: 'empty', dreamerArtifact: null, sourceDreamerArtifactId: null };
47
+ throw new PDRuntimeError('input_invalid', 'Dreamer dependency artifact not found');
146
48
  }
147
49
  for (const depId of deps) {
148
50
  const depTask = await this.stateManager.getTask(depId);
149
51
  if (!depTask)
150
52
  continue;
151
53
  if (depTask.status !== 'succeeded') {
152
- this.emitPhilosopherEvent('philosopher_dependency_not_succeeded', taskId, {
54
+ this.emitEvent('dependency_not_succeeded', taskId, {
153
55
  depTaskId: depId,
154
56
  depStatus: depTask.status,
155
57
  });
@@ -163,359 +65,173 @@ export class PhilosopherRunner {
163
65
  continue;
164
66
  const artifactRef = depPiTask?.outputArtifactRefs?.[0]?.ref ?? `pi-artifact://${depId}`;
165
67
  return {
166
- contextHash: PhilosopherRunner.hashContextRefs([artifactRef]),
68
+ contextHash: BasePeerRunner.hashContextRefs([artifactRef]),
167
69
  dreamerArtifact: firstArtifact.contentJson,
168
70
  sourceDreamerArtifactId: firstArtifact.artifactId,
169
71
  };
170
72
  }
171
73
  }
172
- this.emitPhilosopherEvent('philosopher_no_dreamer_artifact', taskId, {});
173
- return { contextHash: 'empty', dreamerArtifact: null, sourceDreamerArtifactId: null };
74
+ throw new PDRuntimeError('input_invalid', 'Dreamer dependency artifact not found');
174
75
  }
175
- static hashContextRefs(refs) {
176
- if (refs.length === 0)
177
- return 'empty';
178
- const str = refs.join('|');
179
- let hash = 0;
180
- for (let i = 0; i < str.length; i++) {
181
- hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
182
- }
183
- return `ctx-${Math.abs(hash).toString(16)}`;
184
- }
185
- async resolveStoreRunId(taskId) {
186
- const runs = await this.stateManager.getRunsByTask(taskId);
187
- const latestRun = runs[runs.length - 1];
188
- if (!latestRun) {
189
- throw new PDRuntimeError('execution_failed', `No run records found for task ${taskId} after lease acquisition`);
76
+ async invokeRuntime(taskId, context) {
77
+ let parsedDreamerArtifact;
78
+ try {
79
+ parsedDreamerArtifact = JSON.parse(context.dreamerArtifact);
190
80
  }
191
- return latestRun.runId;
192
- }
193
- async invokeRuntime(params) {
194
- let parsedDreamerArtifact = null;
195
- if (params.dreamerArtifact) {
196
- try {
197
- parsedDreamerArtifact = JSON.parse(params.dreamerArtifact);
198
- }
199
- catch {
200
- parsedDreamerArtifact = params.dreamerArtifact;
201
- }
81
+ catch {
82
+ parsedDreamerArtifact = context.dreamerArtifact;
202
83
  }
203
84
  const builder = new PhilosopherPromptBuilder();
204
85
  const { message } = builder.buildPrompt({
205
- taskId: params.taskId,
206
- contextHash: params.contextHash,
86
+ taskId,
87
+ contextHash: context.contextHash,
207
88
  dreamerArtifact: parsedDreamerArtifact,
208
- sourceDreamerArtifactId: params.sourceDreamerArtifactId ?? '',
89
+ sourceDreamerArtifactId: context.sourceDreamerArtifactId,
209
90
  });
210
- const startInput = {
91
+ return this.runtimeAdapter.startRun({
211
92
  agentSpec: { agentId: this.resolvedOptions.agentId, schemaVersion: 'v1' },
212
- taskRef: { taskId: params.taskId },
93
+ taskRef: { taskId },
213
94
  inputPayload: message,
214
95
  contextItems: [],
215
96
  outputSchemaRef: 'philosopher-output-v1',
216
97
  timeoutMs: this.resolvedOptions.timeoutMs,
217
- };
218
- return this.runtimeAdapter.startRun(startInput);
98
+ });
219
99
  }
220
- async pollUntilTerminal(runHandle) {
221
- const deadline = Date.now() + this.resolvedOptions.timeoutMs;
222
- const terminalStatuses = ['succeeded', 'failed', 'timed_out', 'cancelled'];
223
- while (Date.now() < deadline) {
224
- const status = await this.runtimeAdapter.pollRun(runHandle.runId);
225
- if (terminalStatuses.includes(status.status)) {
226
- return status;
227
- }
228
- await this.sleep(this.resolvedOptions.pollIntervalMs);
100
+ async validateOutput(output, taskId) {
101
+ const result = await this.validator.validate(output, taskId);
102
+ // Trust-boundary: validator is an injected dependency returning `string | undefined`
103
+ // for errorCategory. We must not `as`-cast; validate at runtime (ERR-001, ERR-005).
104
+ const rawCategory = result.errorCategory;
105
+ let errorCategory;
106
+ if (rawCategory == null) {
107
+ errorCategory = undefined;
229
108
  }
230
- let cancelFailed = false;
231
- try {
232
- await this.runtimeAdapter.cancelRun(runHandle.runId);
109
+ else if (isPDErrorCategory(rawCategory)) {
110
+ errorCategory = rawCategory;
233
111
  }
234
- catch (cancelErr) {
235
- cancelFailed = true;
236
- this.emitPhilosopherEvent('philosopher_cancel_run_failed', runHandle.runId, {
237
- runId: runHandle.runId,
238
- errorMessage: cancelErr instanceof Error ? cancelErr.message : String(cancelErr),
239
- });
112
+ else {
113
+ // Invalid errorCategory from validator — fail loud, do not pass through
114
+ return {
115
+ valid: false,
116
+ errors: [...result.errors, `invalid errorCategory: ${rawCategory}`],
117
+ errorCategory: 'output_invalid',
118
+ };
240
119
  }
241
- const cancelNote = cancelFailed ? ' (cancelRun also failed)' : '';
242
- throw new PDRuntimeError('timeout', `Run ${runHandle.runId} timed out after ${this.resolvedOptions.timeoutMs}ms${cancelNote}`);
120
+ return {
121
+ valid: result.valid,
122
+ errors: result.errors,
123
+ errorCategory,
124
+ };
243
125
  }
244
- async fetchAndParseOutput(runId) {
245
- const result = await this.runtimeAdapter.fetchOutput(runId);
246
- if (!result || !result.payload) {
247
- throw new PDRuntimeError('output_invalid', `No output available for run ${runId}`);
126
+ // eslint-disable-next-line @typescript-eslint/max-params
127
+ async succeedTask(taskId, runId, output, task, contextHash, context) {
128
+ // Lineage consistency: sourceDreamerArtifactId must match buildContext result (ERR-004).
129
+ if (output.sourceDreamerArtifactId !== context.sourceDreamerArtifactId) {
130
+ throw new PDRuntimeError('output_invalid', `sourceDreamerArtifactId mismatch: expected ${context.sourceDreamerArtifactId}, got ${output.sourceDreamerArtifactId}`);
248
131
  }
249
- return result.payload;
250
- }
251
- async succeedTask(ctx) {
132
+ // Store output before marking succeeded
252
133
  try {
253
- await this.stateManager.updateRunOutput(ctx.runId, JSON.stringify(ctx.output));
134
+ await this.stateManager.updateRunOutput(runId, JSON.stringify(output));
254
135
  }
255
136
  catch (updateErr) {
256
- this.emitPhilosopherEvent('philosopher_update_output_failed', ctx.taskId, {
257
- runId: ctx.runId,
137
+ this.emitEvent('update_output_failed', taskId, {
138
+ runId,
258
139
  errorMessage: updateErr instanceof Error ? updateErr.message : String(updateErr),
259
140
  });
260
141
  throw updateErr;
261
142
  }
262
- const artifactId = `pi-art-${ctx.taskId}-${ctx.runId}`;
263
- const now = new Date().toISOString();
143
+ // Resolve lineage artifact IDs
264
144
  let lineageArtifactIds = [];
145
+ let lineageHasRejected = false;
265
146
  try {
266
- const piTask = hydratePITaskRecord(ctx.task);
267
- const deps = piTask?.dependencyTaskIds ?? [];
268
- const results = await Promise.allSettled(deps.map((depId) => this.artifactStore.listBySourceTaskId(depId)));
269
- for (const result of results) {
270
- if (result.status === 'fulfilled') {
271
- for (const artifact of result.value) {
272
- lineageArtifactIds.push(artifact.artifactId);
273
- }
274
- }
275
- }
147
+ const lineageResult = await this.resolveLineageArtifactIds(taskId);
148
+ lineageArtifactIds = lineageResult.ids;
149
+ lineageHasRejected = lineageResult.hasRejected;
150
+ }
151
+ catch (lineageErr) {
152
+ this.emitEvent('lineage_resolve_failed', taskId, {
153
+ runId,
154
+ errorMessage: lineageErr instanceof Error ? lineageErr.message : String(lineageErr),
155
+ });
156
+ }
157
+ if (lineageHasRejected) {
158
+ this.emitEvent('lineage_partial', taskId, {
159
+ runId,
160
+ resolvedCount: lineageArtifactIds.length,
161
+ warning: 'Some dependency artifact queries were rejected; lineage may be incomplete',
162
+ });
276
163
  }
277
- catch { /* lineage resolution failure is non-fatal */ }
164
+ // Write PIArtifact via artifactStore (idempotent upsert)
165
+ const artifactId = `pi-art-${taskId}-${runId}`;
166
+ const now = new Date().toISOString();
278
167
  try {
279
168
  await this.artifactStore.upsertArtifact({
280
169
  artifactId,
281
170
  artifactKind: 'principle',
282
- sourceTaskId: ctx.taskId,
171
+ sourceTaskId: taskId,
283
172
  lineageArtifactIds,
284
173
  validationStatus: 'pending',
285
- contentJson: JSON.stringify(ctx.output),
174
+ contentJson: JSON.stringify(output),
286
175
  createdAt: now,
287
176
  updatedAt: now,
288
177
  });
289
178
  }
290
179
  catch (artifactErr) {
291
- this.emitPhilosopherEvent('philosopher_artifact_write_failed', ctx.taskId, {
292
- runId: ctx.runId,
180
+ this.emitEvent('artifact_write_failed', taskId, {
181
+ runId,
293
182
  errorMessage: artifactErr instanceof Error ? artifactErr.message : String(artifactErr),
294
183
  });
295
184
  return this.retryOrFail({
296
- taskId: ctx.taskId,
297
- task: ctx.task,
185
+ taskId,
186
+ task,
298
187
  errorCategory: 'artifact_commit_failed',
299
188
  failureReason: `PIArtifact write failed: ${artifactErr instanceof Error ? artifactErr.message : String(artifactErr)}`,
300
189
  });
301
190
  }
302
- const resultRef = `philosopher://${ctx.runId}`;
191
+ // Mark task succeeded
192
+ const resultRef = `${this.config.resultRefPrefix}://${runId}`;
303
193
  try {
304
- await this.stateManager.markTaskSucceeded(ctx.taskId, resultRef);
194
+ await this.stateManager.markTaskSucceeded(taskId, resultRef);
305
195
  }
306
196
  catch (stateErr) {
307
- this.emitPhilosopherEvent('philosopher_mark_succeeded_failed', ctx.taskId, {
308
- taskId: ctx.taskId,
309
- runId: ctx.runId,
197
+ this.emitEvent('mark_succeeded_failed', taskId, {
198
+ taskId,
199
+ runId,
310
200
  errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
311
201
  });
312
202
  throw stateErr;
313
203
  }
314
- this.emitPhilosopherEvent('philosopher_task_succeeded', ctx.taskId, {
315
- attemptCount: ctx.task.attemptCount,
204
+ this.emitEvent('task_succeeded', taskId, {
205
+ attemptCount: task.attemptCount,
316
206
  resultRef,
317
- principleTitle: ctx.output.principleCandidate.title,
207
+ principleTitle: output.principleCandidate.title,
318
208
  });
319
- this.phase = RunnerPhase.Completed;
320
209
  return {
321
210
  status: 'succeeded',
322
- taskId: ctx.taskId,
323
- runId: ctx.runId,
211
+ taskId,
212
+ runId,
324
213
  artifactId,
325
214
  resultRef,
326
- contextHash: ctx.contextHash,
327
- output: ctx.output,
328
- attemptCount: ctx.task.attemptCount,
215
+ contextHash,
216
+ output,
217
+ attemptCount: task.attemptCount,
329
218
  };
330
219
  }
331
- async handleRuntimeFailure(taskId, task, runStatus) {
332
- const errorCategory = this.mapRunStatusToErrorCategory(runStatus.status);
333
- this.emitPhilosopherEvent('philosopher_run_failed', taskId, {
334
- runStatus: runStatus.status,
335
- errorCategory,
336
- });
337
- return this.retryOrFail({
338
- taskId,
339
- task,
340
- errorCategory,
341
- failureReason: `Runtime execution ended with status: ${runStatus.status}`,
342
- });
343
- }
344
- async handleValidationError(ctx) {
345
- const category = (ctx.errorCategory ?? 'output_invalid');
346
- this.emitPhilosopherEvent('philosopher_output_invalid', ctx.taskId, {
347
- errorCount: ctx.errors.length,
348
- errorCategory: category,
349
- });
350
- return this.retryOrFail({
351
- taskId: ctx.taskId,
352
- task: ctx.task,
353
- errorCategory: category,
354
- failureReason: `Validation failed: ${ctx.errors.join('; ')}`,
355
- });
356
- }
357
- async handleLeaseOrPhaseError(taskId, error) {
358
- const classified = this.classifyError(error);
359
- if (classified.category === 'lease_conflict') {
360
- this.emitPhilosopherEvent('philosopher_run_failed', taskId, {
361
- errorCategory: 'lease_conflict',
362
- errorMessage: classified.message,
363
- });
364
- return {
365
- status: 'failed',
366
- taskId,
367
- errorCategory: 'lease_conflict',
368
- failureReason: classified.message,
369
- attemptCount: 1,
370
- };
371
- }
372
- this.emitPhilosopherEvent('philosopher_run_failed', taskId, {
373
- errorCategory: classified.category,
374
- errorMessage: classified.message,
375
- });
376
- const task = {
377
- taskId,
378
- taskKind: 'philosopher',
379
- status: 'leased',
380
- createdAt: new Date().toISOString(),
381
- updatedAt: new Date().toISOString(),
382
- attemptCount: 1,
383
- maxAttempts: this.resolvedOptions.defaultMaxAttempts,
384
- };
385
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
386
- }
387
- async handlePostLeaseError(taskId, task, error) {
388
- const classified = this.classifyError(error);
389
- this.emitPhilosopherEvent('philosopher_run_failed', taskId, {
390
- errorCategory: classified.category,
391
- errorMessage: classified.message,
392
- });
393
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
394
- }
395
- async retryOrFail(ctx) {
396
- if (this.isPermanentError(ctx.errorCategory)) {
397
- try {
398
- await this.stateManager.markTaskFailed(ctx.taskId, ctx.errorCategory);
399
- }
400
- catch (stateErr) {
401
- this.emitPhilosopherEvent('philosopher_mark_failed_error', ctx.taskId, {
402
- errorCategory: 'storage_unavailable',
403
- attemptCount: ctx.task.attemptCount,
404
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
405
- });
406
- return {
407
- status: 'failed',
408
- taskId: ctx.taskId,
409
- errorCategory: 'storage_unavailable',
410
- failureReason: `State manager error: ${ctx.failureReason}`,
411
- attemptCount: ctx.task.attemptCount,
412
- };
413
- }
414
- this.emitPhilosopherEvent('philosopher_task_failed', ctx.taskId, {
415
- errorCategory: ctx.errorCategory,
416
- attemptCount: ctx.task.attemptCount,
417
- failureReason: ctx.failureReason,
418
- });
419
- this.phase = RunnerPhase.Failed;
420
- return {
421
- status: 'failed',
422
- taskId: ctx.taskId,
423
- errorCategory: ctx.errorCategory,
424
- failureReason: ctx.failureReason,
425
- attemptCount: ctx.task.attemptCount,
426
- };
427
- }
428
- const shouldRetry = this.stateManager.getRetryPolicy().shouldRetry(ctx.task);
429
- if (shouldRetry) {
430
- try {
431
- await this.stateManager.markTaskRetryWait(ctx.taskId, ctx.errorCategory);
432
- }
433
- catch (stateErr) {
434
- this.emitPhilosopherEvent('philosopher_mark_retry_error', ctx.taskId, {
435
- errorCategory: 'storage_unavailable',
436
- attemptCount: ctx.task.attemptCount,
437
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
438
- });
439
- return {
440
- status: 'failed',
441
- taskId: ctx.taskId,
442
- errorCategory: 'storage_unavailable',
443
- failureReason: `State manager error: ${ctx.failureReason}`,
444
- attemptCount: ctx.task.attemptCount,
445
- };
446
- }
447
- this.emitPhilosopherEvent('philosopher_task_retried', ctx.taskId, {
448
- errorCategory: ctx.errorCategory,
449
- attemptCount: ctx.task.attemptCount,
450
- });
451
- this.phase = RunnerPhase.RetryWaiting;
452
- return {
453
- status: 'retried',
454
- taskId: ctx.taskId,
455
- errorCategory: ctx.errorCategory,
456
- failureReason: ctx.failureReason,
457
- attemptCount: ctx.task.attemptCount,
458
- };
459
- }
460
- try {
461
- await this.stateManager.markTaskFailed(ctx.taskId, 'max_attempts_exceeded');
462
- }
463
- catch (stateErr) {
464
- this.emitPhilosopherEvent('philosopher_mark_failed_error', ctx.taskId, {
465
- errorCategory: 'storage_unavailable',
466
- attemptCount: ctx.task.attemptCount,
467
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
468
- });
469
- return {
470
- status: 'failed',
471
- taskId: ctx.taskId,
472
- errorCategory: 'storage_unavailable',
473
- failureReason: `State manager error: ${ctx.failureReason}`,
474
- attemptCount: ctx.task.attemptCount,
475
- };
476
- }
477
- this.emitPhilosopherEvent('philosopher_task_failed', ctx.taskId, {
478
- errorCategory: 'max_attempts_exceeded',
479
- attemptCount: ctx.task.attemptCount,
480
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
481
- });
482
- this.phase = RunnerPhase.Failed;
483
- return {
484
- status: 'failed',
485
- taskId: ctx.taskId,
486
- errorCategory: 'max_attempts_exceeded',
487
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
488
- attemptCount: ctx.task.attemptCount,
489
- };
490
- }
491
- // ── Error classification ──────────────────────────────────────────────────
492
- PERMANENT_ERROR_CATEGORIES = new Set(Object.freeze(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid']));
493
- isPermanentError(category) {
494
- return this.PERMANENT_ERROR_CATEGORIES.has(category);
495
- }
220
+ // ── Optional hooks ─────────────────────────────────────────────────────────
221
+ /**
222
+ * Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
223
+ * Only fill when absent via Object.hasOwn — present-but-falsy values
224
+ * must reach validation and fail loud (Runtime Contract Rule 3).
225
+ */
496
226
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
497
- classifyError(error) {
498
- if (error instanceof PDRuntimeError) {
499
- return { category: error.category, message: error.message };
500
- }
501
- if (error instanceof Error) {
502
- return { category: 'execution_failed', message: error.message };
503
- }
504
- return { category: 'execution_failed', message: String(error) };
227
+ postFetchTransform(taskId, untrustedOutput) {
228
+ injectRunnerLineageIfAbsent(untrustedOutput, 'taskId', taskId);
505
229
  }
506
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
507
- mapRunStatusToErrorCategory(status) {
508
- switch (status) {
509
- case 'failed': return 'execution_failed';
510
- case 'timed_out': return 'timeout';
511
- case 'cancelled': return 'cancelled';
512
- default: return 'execution_failed';
513
- }
514
- }
515
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
516
- sleep(ms) {
517
- return new Promise((resolve) => setTimeout(resolve, ms));
230
+ emitSuccessTelemetry(taskId, output) {
231
+ this.emitEvent('principle_candidate_generated', taskId, {
232
+ principleTitle: output.principleCandidate.title,
233
+ confidence: output.principleCandidate.confidence,
234
+ });
518
235
  }
519
236
  }
520
- export { resolvePhilosopherRunnerOptions, DEFAULT_PHILOSOPHER_RUNNER_OPTIONS };
521
237
  //# sourceMappingURL=philosopher-runner.js.map