@akagilnc/pi-workflow-roles 0.1.1878 → 0.1.1893

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.
@@ -1,113 +1,105 @@
1
- import { lstatSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { activationBookDirectory, ensureRealDirectoryTree, errnoCode, errorText, pathContainedIn, } from "./activation-ledger-topology.js";
4
- /**
5
- * Typed missing durable session principal. Callers discriminate with instanceof/code;
6
- * never by parsing message prose. Original filesystem causes are retained.
7
- */
8
- export class ActivationSessionFileMissingError extends Error {
9
- code = "AK_ACTIVATION_SESSION_FILE_MISSING";
10
- path;
11
- constructor(path, options) {
12
- super(`Workflow role activation durable session file does not exist: ${path}`, options?.cause === undefined ? undefined : { cause: options.cause });
13
- this.name = "ActivationSessionFileMissingError";
14
- this.path = path;
15
- }
1
+ import {
2
+ lstatSync,
3
+ realpathSync,
4
+ statSync,
5
+ writeFileSync
6
+ } from "node:fs";
7
+ import { isAbsolute, resolve } from "node:path";
8
+ import {
9
+ errnoCode,
10
+ errorText
11
+ } from "./activation-ledger-topology.js";
12
+ class ActivationSessionFileMissingError extends Error {
13
+ code = "AK_ACTIVATION_SESSION_FILE_MISSING";
14
+ path;
15
+ constructor(path, options) {
16
+ super(
17
+ `Workflow role activation durable session file does not exist: ${path}`,
18
+ options?.cause === void 0 ? void 0 : { cause: options.cause }
19
+ );
20
+ this.name = "ActivationSessionFileMissingError";
21
+ this.path = path;
22
+ }
16
23
  }
17
- function materializeDeferredSessionFile(sessionManager, resolvedFile, ledgerHome) {
18
- const header = sessionManager.getHeader?.();
19
- if (header === null || header === undefined || header.type !== "session") {
20
- throw new ActivationSessionFileMissingError(resolvedFile);
21
- }
22
- ensureRealDirectoryTree(ledgerHome, dirname(resolvedFile));
23
- try {
24
- writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
25
- }
26
- catch (error) {
27
- if (errnoCode(error) !== "EEXIST") {
28
- throw new Error(`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
29
- }
30
- // Lost a create race — validate the winner below.
31
- }
32
- // Rebind full SessionManager so subsequent Pi appends use O_APPEND (flushed=true)
33
- // instead of exclusive wx create against the file we just wrote.
34
- if (typeof sessionManager.setSessionFile === "function") {
35
- sessionManager.setSessionFile(resolvedFile);
36
- }
24
+ function materializeDeferredSessionFile(sessionManager, resolvedFile) {
25
+ const header = sessionManager.getHeader?.();
26
+ if (header === null || header === void 0 || header.type !== "session") {
27
+ throw new ActivationSessionFileMissingError(resolvedFile);
28
+ }
29
+ try {
30
+ writeFileSync(resolvedFile, `${JSON.stringify(header)}
31
+ `, { flag: "wx" });
32
+ } catch (error) {
33
+ if (errnoCode(error) !== "EEXIST") {
34
+ throw new Error(
35
+ `Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`,
36
+ { cause: error }
37
+ );
38
+ }
39
+ }
40
+ if (typeof sessionManager.setSessionFile === "function") {
41
+ sessionManager.setSessionFile(resolvedFile);
42
+ }
37
43
  }
38
- /**
39
- * Admit only a durable Pi session file principal under the resolved machine ledger
40
- * book (ADR 0048). Requires an existing regular file at admission: resolve real paths
41
- * and prove containment under the real book. Reject relative paths, outside-book paths,
42
- * directories, symlink escapes, and nonexistent paths that cannot be materialized from
43
- * the live SessionManager header. Original filesystem causes are retained.
44
- *
45
- * Upstream Pi defers exclusive create until the first assistant message. When the path
46
- * is the live SessionManager principal under the book and only the header is in memory,
47
- * admission materializes that header onto the same path before the fact is written so
48
- * the role fact never points at a session that may be created later.
49
- */
50
- export function durableSessionPointer(sessionManager, options) {
51
- const file = sessionManager.getSessionFile?.();
52
- if (typeof file !== "string" || file.length === 0) {
53
- throw new Error("Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected");
54
- }
55
- if (!isAbsolute(file)) {
56
- throw new Error(`Workflow role activation requires an absolute durable session file path under the machine ledger book; got relative path: ${file}`);
57
- }
58
- const resolvedFile = resolve(file);
59
- const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
60
- if (!pathContainedIn(bookRoot, resolvedFile)) {
61
- throw new Error(`Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`);
62
- }
63
- try {
64
- lstatSync(resolvedFile);
65
- }
66
- catch (error) {
67
- if (errnoCode(error) !== "ENOENT") {
68
- throw new Error(`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
69
- }
70
- try {
71
- materializeDeferredSessionFile(sessionManager, resolvedFile, options.ledgerHome);
72
- }
73
- catch (materializeError) {
74
- // Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
75
- // Other materialize failures keep their own typed/native identity.
76
- if (materializeError instanceof ActivationSessionFileMissingError) {
77
- throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
78
- }
79
- throw materializeError;
80
- }
44
+ function durableSessionPointer(sessionManager) {
45
+ const file = sessionManager.getSessionFile?.();
46
+ if (typeof file !== "string" || file.length === 0) {
47
+ throw new Error(
48
+ "Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected"
49
+ );
50
+ }
51
+ if (!isAbsolute(file)) {
52
+ throw new Error(
53
+ `Workflow role activation requires an absolute durable session file path; got relative path: ${file}`
54
+ );
55
+ }
56
+ const resolvedFile = resolve(file);
57
+ try {
58
+ lstatSync(resolvedFile);
59
+ } catch (error) {
60
+ if (errnoCode(error) !== "ENOENT") {
61
+ throw new Error(
62
+ `Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`,
63
+ { cause: error }
64
+ );
81
65
  }
82
- let realBook;
83
66
  try {
84
- realBook = realpathSync(bookRoot);
85
- }
86
- catch (error) {
87
- throw new Error(`Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`, { cause: error });
88
- }
89
- let realFile;
90
- try {
91
- realFile = realpathSync(resolvedFile);
92
- }
93
- catch (error) {
67
+ materializeDeferredSessionFile(sessionManager, resolvedFile);
68
+ } catch (materializeError) {
69
+ if (materializeError instanceof ActivationSessionFileMissingError) {
94
70
  throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
95
- }
96
- if (!pathContainedIn(realBook, realFile)) {
97
- throw new Error(`Workflow role activation requires the durable session file principal under the machine ledger book (${realBook}); got: ${realFile}`);
98
- }
99
- let info;
100
- try {
101
- info = statSync(realFile);
102
- }
103
- catch (error) {
104
- throw new Error(`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`, { cause: error });
105
- }
106
- if (info.isDirectory()) {
107
- throw new Error(`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`);
108
- }
109
- if (!info.isFile()) {
110
- throw new Error(`Workflow role activation durable session principal is not a regular file: ${realFile}`);
111
- }
112
- return { kind: "session-file", path: realFile };
71
+ }
72
+ throw materializeError;
73
+ }
74
+ }
75
+ let realFile;
76
+ try {
77
+ realFile = realpathSync(resolvedFile);
78
+ } catch (error) {
79
+ throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
80
+ }
81
+ let info;
82
+ try {
83
+ info = statSync(realFile);
84
+ } catch (error) {
85
+ throw new Error(
86
+ `Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`,
87
+ { cause: error }
88
+ );
89
+ }
90
+ if (info.isDirectory()) {
91
+ throw new Error(
92
+ `Workflow role activation durable session principal must be a file, not a directory: ${realFile}`
93
+ );
94
+ }
95
+ if (!info.isFile()) {
96
+ throw new Error(
97
+ `Workflow role activation durable session principal is not a regular file: ${realFile}`
98
+ );
99
+ }
100
+ return { kind: "session-file", path: realFile };
113
101
  }
102
+ export {
103
+ ActivationSessionFileMissingError,
104
+ durableSessionPointer
105
+ };
@@ -11,6 +11,7 @@ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_E
11
11
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
12
12
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
13
13
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
14
+ import { hasUpstreamErrorTestimony, isNonSuccessHttpStatus, projectConfirmedRemotePayload, } from "./upstream-error-testimony.js";
14
15
  // ── shared constants / types ──────────────────────────────────────────────
15
16
  export const AUDITOR_TURN_LIMIT = 32;
16
17
  export const DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES = 2;
@@ -116,15 +117,23 @@ export async function createInheritedRuntime(options) {
116
117
  void (async () => {
117
118
  for (let attempt = 0;; attempt += 1) {
118
119
  const idle = createStreamIdleGuard(options.signal === undefined ? {} : { parentSignal: options.signal });
120
+ // Typed HTTP status observed via onResponse — never inferred from prose.
121
+ let observedHttpStatus;
119
122
  try {
120
123
  const requestSignal = request?.signal;
121
124
  const streamSignal = requestSignal === undefined
122
125
  ? idle.signal
123
126
  : AbortSignal.any([idle.signal, requestSignal]);
127
+ const priorOnResponse = request?.onResponse;
124
128
  const inheritedRequest = {
125
129
  ...(request ?? {}),
126
130
  ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
127
131
  signal: streamSignal,
132
+ onResponse: async (response, model) => {
133
+ if (typeof response?.status === "number")
134
+ observedHttpStatus = response.status;
135
+ await priorOnResponse?.(response, model);
136
+ },
128
137
  };
129
138
  if (options.runCompletion !== undefined) {
130
139
  await new Promise((resolve) => setImmediate(resolve));
@@ -133,12 +142,12 @@ export async function createInheritedRuntime(options) {
133
142
  const completed = await waitForStream(options.runCompletion(model, options.injectedSystemPrompt === undefined
134
143
  ? context
135
144
  : { ...context, systemPrompt: options.injectedSystemPrompt }, inheritedRequest), streamSignal);
136
- const response = {
145
+ const response = attachObservedHttpStatus({
137
146
  ...completed,
138
147
  api: model.api,
139
148
  provider: model.provider,
140
149
  model: model.id,
141
- };
150
+ }, observedHttpStatus);
142
151
  if (response.stopReason === "error" || response.stopReason === "aborted") {
143
152
  wrapped.push({ type: "error", reason: response.stopReason, error: response });
144
153
  }
@@ -158,9 +167,9 @@ export async function createInheritedRuntime(options) {
158
167
  break;
159
168
  sawEvent = true;
160
169
  idle.poke();
161
- wrapped.push(next.value);
170
+ wrapped.push(enrichStreamEvent(next.value, observedHttpStatus));
162
171
  }
163
- const response = await waitForStream(source.result(), idle.signal);
172
+ const response = attachObservedHttpStatus(await waitForStream(source.result(), idle.signal), observedHttpStatus);
164
173
  if (!sawEvent)
165
174
  wrapped.end(response);
166
175
  return;
@@ -190,6 +199,9 @@ export async function createInheritedRuntime(options) {
190
199
  continue;
191
200
  }
192
201
  state.streamFailure = failure;
202
+ const projected = projectStructuredRemote(failure);
203
+ // Prefer structured fields on the thrown failure; fall back to onResponse observation.
204
+ const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
193
205
  const response = {
194
206
  role: "assistant",
195
207
  content: [],
@@ -198,8 +210,14 @@ export async function createInheritedRuntime(options) {
198
210
  model: model.id,
199
211
  usage: emptyUsage(),
200
212
  stopReason: "error",
213
+ // Preserve the held failure message bytes — do not rewrite.
201
214
  errorMessage: failure instanceof Error ? failure.message : String(failure),
202
215
  timestamp: Date.now(),
216
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
217
+ ...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
218
+ ...(projected.body === undefined ? {} : { body: projected.body }),
219
+ ...(projected.code === undefined ? {} : { code: projected.code }),
220
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
203
221
  };
204
222
  wrapped.push({ type: "error", reason: "error", error: response });
205
223
  wrapped.end(response);
@@ -270,6 +288,104 @@ export async function createInheritedRuntime(options) {
270
288
  runtime.registerNativeProvider(provider);
271
289
  return state;
272
290
  }
291
+ function numericHttpStatus(value) {
292
+ return isNonSuccessHttpStatus(value) ? value : undefined;
293
+ }
294
+ /**
295
+ * Cause-chain reader over the shared upstream-testimony authority.
296
+ * Shape walking stays here; testimony + confirmed-remote payload rules are shared.
297
+ */
298
+ function projectStructuredRemote(error) {
299
+ let httpStatus;
300
+ let diagnostics;
301
+ let body;
302
+ let code;
303
+ let errno;
304
+ let cursor = error;
305
+ const seen = new Set();
306
+ while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
307
+ seen.add(cursor);
308
+ const record = cursor;
309
+ const nodeStatus = numericHttpStatus(record.statusCode)
310
+ ?? numericHttpStatus(record.status)
311
+ ?? numericHttpStatus(record.httpStatus);
312
+ const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
313
+ ? record.diagnostics
314
+ : undefined;
315
+ const nodeHasTestimony = hasUpstreamErrorTestimony({
316
+ ...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
317
+ ...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
318
+ });
319
+ if (httpStatus === undefined && nodeStatus !== undefined)
320
+ httpStatus = nodeStatus;
321
+ if (diagnostics === undefined && nodeDiagnostics !== undefined)
322
+ diagnostics = nodeDiagnostics;
323
+ // Payload only from confirmed-remote nodes — never arbitrary local Error.code.
324
+ if (nodeHasTestimony) {
325
+ const payload = projectConfirmedRemotePayload(record);
326
+ if (body === undefined && payload.body !== undefined)
327
+ body = payload.body;
328
+ if (code === undefined && payload.code !== undefined)
329
+ code = payload.code;
330
+ if (errno === undefined && payload.errno !== undefined)
331
+ errno = payload.errno;
332
+ }
333
+ cursor = record.cause;
334
+ }
335
+ return {
336
+ hasTestimony: hasUpstreamErrorTestimony({
337
+ ...(httpStatus === undefined ? {} : { httpStatus }),
338
+ ...(diagnostics === undefined ? {} : { diagnostics }),
339
+ }),
340
+ ...(httpStatus === undefined ? {} : { httpStatus }),
341
+ ...(diagnostics === undefined ? {} : { diagnostics }),
342
+ ...(body === undefined ? {} : { body }),
343
+ ...(code === undefined ? {} : { code }),
344
+ ...(errno === undefined ? {} : { errno }),
345
+ };
346
+ }
347
+ /**
348
+ * Attach a directly observed HTTP status onto an error/aborted assistant message.
349
+ * Does not invent status from errorMessage prose; skips when already held.
350
+ */
351
+ function attachObservedHttpStatus(message, observedHttpStatus) {
352
+ if (observedHttpStatus === undefined)
353
+ return message;
354
+ if (message.stopReason !== "error" && message.stopReason !== "aborted")
355
+ return message;
356
+ if (numericHttpStatus(observedHttpStatus) === undefined)
357
+ return message;
358
+ if (projectStructuredRemote(message).httpStatus !== undefined)
359
+ return message;
360
+ return Object.assign(message, {
361
+ status: observedHttpStatus,
362
+ statusCode: observedHttpStatus,
363
+ });
364
+ }
365
+ function enrichStreamEvent(event, observedHttpStatus) {
366
+ if (observedHttpStatus === undefined || event === null || typeof event !== "object")
367
+ return event;
368
+ const record = event;
369
+ if (record.type === "error" && record.error !== null && typeof record.error === "object") {
370
+ return {
371
+ ...record,
372
+ error: attachObservedHttpStatus(record.error, observedHttpStatus),
373
+ };
374
+ }
375
+ if (record.type === "done" && record.message !== null && typeof record.message === "object") {
376
+ return {
377
+ ...record,
378
+ message: attachObservedHttpStatus(record.message, observedHttpStatus),
379
+ };
380
+ }
381
+ if (record.partial !== null && typeof record.partial === "object") {
382
+ return {
383
+ ...record,
384
+ partial: attachObservedHttpStatus(record.partial, observedHttpStatus),
385
+ };
386
+ }
387
+ return event;
388
+ }
273
389
  function classifiedError(error, evidenceChildFailure) {
274
390
  const diagnostic = typeof error === "object" && error !== null && typeof error.errorMessage === "string"
275
391
  ? error.errorMessage
@@ -279,7 +395,9 @@ function classifiedError(error, evidenceChildFailure) {
279
395
  : Object.assign(new Error(diagnostic, { cause: error }), { evidenceChildOriginal: error });
280
396
  const classification = "evidenceChildFailure" in wrapped
281
397
  ? wrapped.evidenceChildFailure
282
- : evidenceChildFailure;
398
+ : evidenceChildFailure === "provider" && !projectStructuredRemote(error).hasTestimony
399
+ ? "unknown"
400
+ : evidenceChildFailure;
283
401
  return Object.assign(wrapped, { evidenceChildFailure: classification });
284
402
  }
285
403
  function emptyUsage() {
@@ -368,10 +486,14 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
368
486
  const lastAssistant = [...session.messages]
369
487
  .reverse()
370
488
  .find((message) => message.role === "assistant");
371
- if (lastAssistant?.role === "assistant" && lastAssistant.stopReason === "error") {
372
- throw classifiedError(new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }), "provider");
489
+ // error|aborted assistant stops share the upstream-testimony rule: provider only
490
+ // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
491
+ // for real local child/report failures (no assistant / blank report / cleanup).
492
+ if (lastAssistant?.role === "assistant"
493
+ && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
494
+ throw classifiedError(new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }), projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown");
373
495
  }
374
- if (lastAssistant?.role !== "assistant" || lastAssistant.stopReason === "aborted") {
496
+ if (lastAssistant?.role !== "assistant") {
375
497
  throw classifiedError(new Error("Evidence child child terminated without a report", {
376
498
  cause: lastAssistant ?? session.messages,
377
499
  }), "child");
@@ -721,15 +843,30 @@ export async function executeAuditorChild(options) {
721
843
  catch (retentionFailure) {
722
844
  if (response.stopReason !== "error")
723
845
  throw retentionFailure;
724
- const failure = new Error(response.errorMessage?.trim() || "provider failure", { cause: retentionFailure });
725
- failure.name = response.model || response.provider || "Error";
726
- failure.knownCause = "provider";
727
- failure.failureCode = response.provider || response.model;
846
+ // Do not trim/rewrite the held errorMessage bytes.
847
+ const diagnostic = typeof response.errorMessage === "string" && response.errorMessage.trim() !== ""
848
+ ? response.errorMessage
849
+ : undefined;
850
+ const projected = projectStructuredRemote(response);
851
+ const failure = new Error(diagnostic ?? "", { cause: retentionFailure });
852
+ if (projected.hasTestimony && (response.model || response.provider)) {
853
+ failure.name = response.model || response.provider || "Error";
854
+ failure.failureCode = response.provider || response.model;
855
+ }
856
+ failure.knownCause = projected.hasTestimony ? "provider" : "unrecognized";
728
857
  const retentionError = retentionFailure instanceof Error ? retentionFailure : undefined;
729
858
  const retentionCause = retentionError?.cause;
730
859
  failure.details = {
731
- ...(response.provider ? { provider: response.provider } : {}),
732
- ...(response.model ? { model: response.model } : {}),
860
+ ...(diagnostic === undefined ? {} : { errorMessage: diagnostic }),
861
+ ...(projected.hasTestimony && response.provider ? { provider: response.provider } : {}),
862
+ ...(projected.hasTestimony && response.model ? { model: response.model } : {}),
863
+ ...(response.api ? { api: response.api } : {}),
864
+ ...(response.rawStopReason ? { rawStopReason: response.rawStopReason } : {}),
865
+ ...(projected.httpStatus === undefined ? {} : { httpStatus: projected.httpStatus }),
866
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
867
+ ...(projected.body === undefined ? {} : { body: projected.body }),
868
+ ...(projected.code === undefined ? {} : { code: projected.code }),
869
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
733
870
  retentionFailure: {
734
871
  name: retentionError?.name ?? typeof retentionFailure,
735
872
  message: retentionError?.message ?? String(retentionFailure),
@@ -756,8 +893,8 @@ export async function executeAuditorChild(options) {
756
893
  parent: binding.parent,
757
894
  failure: {
758
895
  cause: failure.knownCause,
759
- identity: { name: failure.name, code: failure.failureCode },
760
- diagnostic: failure.message,
896
+ ...(failure.failureCode === undefined ? {} : { identity: { name: failure.name, code: failure.failureCode } }),
897
+ ...(failure.message === "" ? {} : { diagnostic: failure.message }),
761
898
  details: failure.details,
762
899
  },
763
900
  });