@akagilnc/pi-workflow-roles 0.1.1883 → 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.
@@ -6,7 +6,7 @@ import { executeEvidenceChild } from "./evidence-child-executor.js";
6
6
  export function projectSharedChildFailure(error) {
7
7
  if (typeof error === "object" && error !== null && "evidenceChildFailure" in error) {
8
8
  const classification = error.evidenceChildFailure;
9
- if (classification === "provider" || classification === "child") {
9
+ if (classification === "provider" || classification === "child" || classification === "unknown") {
10
10
  Object.assign(error, { reviewerFailure: classification });
11
11
  }
12
12
  }
@@ -0,0 +1,60 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ const TYPED_HTTP_FILE = "typed-provider-http.json";
4
+ function typedProviderHttpPath(runDirectory) {
5
+ return join(runDirectory, TYPED_HTTP_FILE);
6
+ }
7
+ async function clearTypedProviderHttpObservation(runDirectory) {
8
+ try {
9
+ await unlink(typedProviderHttpPath(runDirectory));
10
+ } catch (error) {
11
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
12
+ return;
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+ async function recordTypedProviderHttpStatus(runDirectory, observation) {
18
+ if (observation.httpStatus >= 200 && observation.httpStatus < 300) {
19
+ await clearTypedProviderHttpObservation(runDirectory);
20
+ return;
21
+ }
22
+ const body = {
23
+ httpStatus: observation.httpStatus,
24
+ provider: observation.provider
25
+ };
26
+ await writeFile(
27
+ typedProviderHttpPath(runDirectory),
28
+ `${JSON.stringify(body)}
29
+ `,
30
+ "utf8"
31
+ );
32
+ }
33
+ async function readLatestTypedProviderHttpObservation(runDirectory) {
34
+ let text;
35
+ try {
36
+ text = await readFile(typedProviderHttpPath(runDirectory), "utf8");
37
+ } catch (error) {
38
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
39
+ return void 0;
40
+ }
41
+ throw error;
42
+ }
43
+ const raw = JSON.parse(text);
44
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
45
+ throw new Error("typed provider HTTP observation must be a JSON object");
46
+ }
47
+ const record = raw;
48
+ if (typeof record.httpStatus !== "number") {
49
+ throw new Error("typed provider HTTP observation missing numeric httpStatus");
50
+ }
51
+ if (typeof record.provider !== "string" || record.provider.trim() === "") {
52
+ throw new Error("typed provider HTTP observation missing non-empty provider");
53
+ }
54
+ return { httpStatus: record.httpStatus, provider: record.provider };
55
+ }
56
+ export {
57
+ clearTypedProviderHttpObservation,
58
+ readLatestTypedProviderHttpObservation,
59
+ recordTypedProviderHttpStatus
60
+ };
@@ -0,0 +1,19 @@
1
+ function isNonSuccessHttpStatus(status) {
2
+ return typeof status === "number" && (status < 200 || status >= 300);
3
+ }
4
+ function hasUpstreamErrorTestimony(input) {
5
+ if (isNonSuccessHttpStatus(input.httpStatus)) return true;
6
+ return Array.isArray(input.diagnostics) && input.diagnostics.length > 0;
7
+ }
8
+ function projectConfirmedRemotePayload(input) {
9
+ return {
10
+ ...input.body === void 0 ? {} : { body: input.body },
11
+ ...input.code === void 0 ? {} : { code: input.code },
12
+ ...input.errno === void 0 ? {} : { errno: input.errno }
13
+ };
14
+ }
15
+ export {
16
+ hasUpstreamErrorTestimony,
17
+ isNonSuccessHttpStatus,
18
+ projectConfirmedRemotePayload
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1883",
3
+ "version": "0.1.1893",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,6 +14,8 @@ const entries = [
14
14
  "activation-reconciliation",
15
15
  "sitian-role-run-coordinates",
16
16
  "sitian-record-entry",
17
+ "typed-provider-http",
18
+ "upstream-error-testimony",
17
19
  ];
18
20
 
19
21
  /**
@@ -33,6 +33,11 @@ import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
33
33
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
34
34
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
35
35
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
36
+ import {
37
+ hasUpstreamErrorTestimony,
38
+ isNonSuccessHttpStatus,
39
+ projectConfirmedRemotePayload,
40
+ } from "./upstream-error-testimony.ts";
36
41
 
37
42
  // ── shared constants / types ──────────────────────────────────────────────
38
43
 
@@ -186,15 +191,25 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
186
191
  const idle = createStreamIdleGuard(
187
192
  options.signal === undefined ? {} : { parentSignal: options.signal },
188
193
  );
194
+ // Typed HTTP status observed via onResponse — never inferred from prose.
195
+ let observedHttpStatus: number | undefined;
189
196
  try {
190
197
  const requestSignal = request?.signal;
191
198
  const streamSignal = requestSignal === undefined
192
199
  ? idle.signal
193
200
  : AbortSignal.any([idle.signal, requestSignal]);
201
+ const priorOnResponse = request?.onResponse;
194
202
  const inheritedRequest = {
195
203
  ...(request ?? {}),
196
204
  ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
197
205
  signal: streamSignal,
206
+ onResponse: async (
207
+ response: { status: number; headers: Record<string, string> },
208
+ model: Model<Api>,
209
+ ) => {
210
+ if (typeof response?.status === "number") observedHttpStatus = response.status;
211
+ await priorOnResponse?.(response, model);
212
+ },
198
213
  } as ProviderStreamOptions;
199
214
  if (options.runCompletion !== undefined) {
200
215
  await new Promise<void>((resolve) => setImmediate(resolve));
@@ -209,12 +224,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
209
224
  ),
210
225
  streamSignal,
211
226
  );
212
- const response: AssistantMessage = {
227
+ const response: AssistantMessage = attachObservedHttpStatus({
213
228
  ...completed,
214
229
  api: model.api,
215
230
  provider: model.provider,
216
231
  model: model.id,
217
- };
232
+ }, observedHttpStatus);
218
233
  if (response.stopReason === "error" || response.stopReason === "aborted") {
219
234
  wrapped.push({ type: "error", reason: response.stopReason, error: response });
220
235
  } else {
@@ -232,9 +247,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
232
247
  if (next.done) break;
233
248
  sawEvent = true;
234
249
  idle.poke();
235
- wrapped.push(next.value as any);
250
+ wrapped.push(enrichStreamEvent(next.value, observedHttpStatus) as any);
236
251
  }
237
- const response = await waitForStream(source.result(), idle.signal);
252
+ const response = attachObservedHttpStatus(
253
+ await waitForStream(source.result(), idle.signal),
254
+ observedHttpStatus,
255
+ );
238
256
  if (!sawEvent) wrapped.end(response);
239
257
  return;
240
258
  } catch (error) {
@@ -264,17 +282,26 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
264
282
  continue;
265
283
  }
266
284
  state.streamFailure = failure;
267
- const response: AssistantMessage = {
268
- role: "assistant",
269
- content: [],
285
+ const projected = projectStructuredRemote(failure);
286
+ // Prefer structured fields on the thrown failure; fall back to onResponse observation.
287
+ const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
288
+ const response = {
289
+ role: "assistant" as const,
290
+ content: [] as [],
270
291
  api: model.api,
271
292
  provider: model.provider,
272
293
  model: model.id,
273
294
  usage: emptyUsage(),
274
- stopReason: "error",
295
+ stopReason: "error" as const,
296
+ // Preserve the held failure message bytes — do not rewrite.
275
297
  errorMessage: failure instanceof Error ? failure.message : String(failure),
276
298
  timestamp: Date.now(),
277
- };
299
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
300
+ ...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
301
+ ...(projected.body === undefined ? {} : { body: projected.body }),
302
+ ...(projected.code === undefined ? {} : { code: projected.code }),
303
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
304
+ } as unknown as AssistantMessage;
278
305
  wrapped.push({ type: "error", reason: "error", error: response });
279
306
  wrapped.end(response);
280
307
  return;
@@ -346,11 +373,114 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
346
373
  return state;
347
374
  }
348
375
 
376
+ type EvidenceChildFailureClassification = "provider" | "child" | "unknown";
349
377
  type ClassifiedReviewerError = Error & Readonly<{
350
- evidenceChildFailure: "provider" | "child";
378
+ evidenceChildFailure: EvidenceChildFailureClassification;
351
379
  evidenceChildOriginal?: unknown;
352
380
  }>;
353
- function classifiedError(error: unknown, evidenceChildFailure: "provider" | "child"): ClassifiedReviewerError {
381
+ function numericHttpStatus(value: unknown): number | undefined {
382
+ return isNonSuccessHttpStatus(value) ? value : undefined;
383
+ }
384
+
385
+ type StructuredRemoteProjection = {
386
+ readonly hasTestimony: boolean;
387
+ readonly httpStatus?: number;
388
+ readonly diagnostics?: unknown;
389
+ readonly body?: unknown;
390
+ readonly code?: unknown;
391
+ readonly errno?: unknown;
392
+ };
393
+
394
+ /**
395
+ * Cause-chain reader over the shared upstream-testimony authority.
396
+ * Shape walking stays here; testimony + confirmed-remote payload rules are shared.
397
+ */
398
+ function projectStructuredRemote(error: unknown): StructuredRemoteProjection {
399
+ let httpStatus: number | undefined;
400
+ let diagnostics: unknown;
401
+ let body: unknown;
402
+ let code: unknown;
403
+ let errno: unknown;
404
+ let cursor: unknown = error;
405
+ const seen = new Set<unknown>();
406
+ while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
407
+ seen.add(cursor);
408
+ const record = cursor as Record<string, unknown>;
409
+ const nodeStatus = numericHttpStatus(record.statusCode)
410
+ ?? numericHttpStatus(record.status)
411
+ ?? numericHttpStatus(record.httpStatus);
412
+ const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
413
+ ? record.diagnostics
414
+ : undefined;
415
+ const nodeHasTestimony = hasUpstreamErrorTestimony({
416
+ ...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
417
+ ...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
418
+ });
419
+ if (httpStatus === undefined && nodeStatus !== undefined) httpStatus = nodeStatus;
420
+ if (diagnostics === undefined && nodeDiagnostics !== undefined) diagnostics = nodeDiagnostics;
421
+ // Payload only from confirmed-remote nodes — never arbitrary local Error.code.
422
+ if (nodeHasTestimony) {
423
+ const payload = projectConfirmedRemotePayload(record);
424
+ if (body === undefined && payload.body !== undefined) body = payload.body;
425
+ if (code === undefined && payload.code !== undefined) code = payload.code;
426
+ if (errno === undefined && payload.errno !== undefined) errno = payload.errno;
427
+ }
428
+ cursor = record.cause;
429
+ }
430
+ return {
431
+ hasTestimony: hasUpstreamErrorTestimony({
432
+ ...(httpStatus === undefined ? {} : { httpStatus }),
433
+ ...(diagnostics === undefined ? {} : { diagnostics }),
434
+ }),
435
+ ...(httpStatus === undefined ? {} : { httpStatus }),
436
+ ...(diagnostics === undefined ? {} : { diagnostics }),
437
+ ...(body === undefined ? {} : { body }),
438
+ ...(code === undefined ? {} : { code }),
439
+ ...(errno === undefined ? {} : { errno }),
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Attach a directly observed HTTP status onto an error/aborted assistant message.
445
+ * Does not invent status from errorMessage prose; skips when already held.
446
+ */
447
+ function attachObservedHttpStatus<T extends AssistantMessage>(
448
+ message: T,
449
+ observedHttpStatus: number | undefined,
450
+ ): T {
451
+ if (observedHttpStatus === undefined) return message;
452
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
453
+ if (numericHttpStatus(observedHttpStatus) === undefined) return message;
454
+ if (projectStructuredRemote(message).httpStatus !== undefined) return message;
455
+ return Object.assign(message, {
456
+ status: observedHttpStatus,
457
+ statusCode: observedHttpStatus,
458
+ });
459
+ }
460
+ function enrichStreamEvent(event: unknown, observedHttpStatus: number | undefined): unknown {
461
+ if (observedHttpStatus === undefined || event === null || typeof event !== "object") return event;
462
+ const record = event as Record<string, unknown>;
463
+ if (record.type === "error" && record.error !== null && typeof record.error === "object") {
464
+ return {
465
+ ...record,
466
+ error: attachObservedHttpStatus(record.error as AssistantMessage, observedHttpStatus),
467
+ };
468
+ }
469
+ if (record.type === "done" && record.message !== null && typeof record.message === "object") {
470
+ return {
471
+ ...record,
472
+ message: attachObservedHttpStatus(record.message as AssistantMessage, observedHttpStatus),
473
+ };
474
+ }
475
+ if (record.partial !== null && typeof record.partial === "object") {
476
+ return {
477
+ ...record,
478
+ partial: attachObservedHttpStatus(record.partial as AssistantMessage, observedHttpStatus),
479
+ };
480
+ }
481
+ return event;
482
+ }
483
+ function classifiedError(error: unknown, evidenceChildFailure: EvidenceChildFailureClassification): ClassifiedReviewerError {
354
484
  const diagnostic = typeof error === "object" && error !== null && typeof (error as { errorMessage?: unknown }).errorMessage === "string"
355
485
  ? (error as { errorMessage: string }).errorMessage
356
486
  : error === undefined ? "" : String(error);
@@ -359,7 +489,9 @@ function classifiedError(error: unknown, evidenceChildFailure: "provider" | "chi
359
489
  : Object.assign(new Error(diagnostic, { cause: error }), { evidenceChildOriginal: error });
360
490
  const classification = "evidenceChildFailure" in wrapped
361
491
  ? (wrapped as ClassifiedReviewerError).evidenceChildFailure
362
- : evidenceChildFailure;
492
+ : evidenceChildFailure === "provider" && !projectStructuredRemote(error).hasTestimony
493
+ ? "unknown"
494
+ : evidenceChildFailure;
363
495
  return Object.assign(wrapped, { evidenceChildFailure: classification });
364
496
  }
365
497
 
@@ -461,13 +593,19 @@ export async function executeEvidenceChild(
461
593
  const lastAssistant = [...session.messages]
462
594
  .reverse()
463
595
  .find((message) => message.role === "assistant");
464
- if (lastAssistant?.role === "assistant" && lastAssistant.stopReason === "error") {
596
+ // error|aborted assistant stops share the upstream-testimony rule: provider only
597
+ // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
598
+ // for real local child/report failures (no assistant / blank report / cleanup).
599
+ if (
600
+ lastAssistant?.role === "assistant"
601
+ && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")
602
+ ) {
465
603
  throw classifiedError(
466
604
  new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }),
467
- "provider",
605
+ projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
468
606
  );
469
607
  }
470
- if (lastAssistant?.role !== "assistant" || lastAssistant.stopReason === "aborted") {
608
+ if (lastAssistant?.role !== "assistant") {
471
609
  throw classifiedError(
472
610
  new Error("Evidence child child terminated without a report", {
473
611
  cause: lastAssistant ?? session.messages,
@@ -820,22 +958,37 @@ export async function executeAuditorChild(
820
958
  else if (retentionFailure !== undefined) throw retentionFailure;
821
959
  } catch (retentionFailure) {
822
960
  if (response.stopReason !== "error") throw retentionFailure;
961
+ // Do not trim/rewrite the held errorMessage bytes.
962
+ const diagnostic = typeof response.errorMessage === "string" && response.errorMessage.trim() !== ""
963
+ ? response.errorMessage
964
+ : undefined;
965
+ const projected = projectStructuredRemote(response);
823
966
  const failure = new Error(
824
- response.errorMessage?.trim() || "provider failure",
967
+ diagnostic ?? "",
825
968
  { cause: retentionFailure },
826
969
  ) as Error & {
827
- knownCause: "provider";
970
+ knownCause: "provider" | "unrecognized";
828
971
  failureCode?: string;
829
972
  details: Record<string, unknown>;
830
973
  };
831
- failure.name = response.model || response.provider || "Error";
832
- failure.knownCause = "provider";
833
- failure.failureCode = response.provider || response.model;
974
+ if (projected.hasTestimony && (response.model || response.provider)) {
975
+ failure.name = response.model || response.provider || "Error";
976
+ failure.failureCode = response.provider || response.model;
977
+ }
978
+ failure.knownCause = projected.hasTestimony ? "provider" : "unrecognized";
834
979
  const retentionError = retentionFailure instanceof Error ? retentionFailure : undefined;
835
980
  const retentionCause = retentionError?.cause;
836
981
  failure.details = {
837
- ...(response.provider ? { provider: response.provider } : {}),
838
- ...(response.model ? { model: response.model } : {}),
982
+ ...(diagnostic === undefined ? {} : { errorMessage: diagnostic }),
983
+ ...(projected.hasTestimony && response.provider ? { provider: response.provider } : {}),
984
+ ...(projected.hasTestimony && response.model ? { model: response.model } : {}),
985
+ ...(response.api ? { api: response.api } : {}),
986
+ ...(response.rawStopReason ? { rawStopReason: response.rawStopReason } : {}),
987
+ ...(projected.httpStatus === undefined ? {} : { httpStatus: projected.httpStatus }),
988
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
989
+ ...(projected.body === undefined ? {} : { body: projected.body }),
990
+ ...(projected.code === undefined ? {} : { code: projected.code }),
991
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
839
992
  retentionFailure: {
840
993
  name: retentionError?.name ?? typeof retentionFailure,
841
994
  message: retentionError?.message ?? String(retentionFailure),
@@ -862,8 +1015,8 @@ export async function executeAuditorChild(
862
1015
  parent: binding.parent,
863
1016
  failure: {
864
1017
  cause: failure.knownCause,
865
- identity: { name: failure.name, code: failure.failureCode },
866
- diagnostic: failure.message,
1018
+ ...(failure.failureCode === undefined ? {} : { identity: { name: failure.name, code: failure.failureCode } }),
1019
+ ...(failure.message === "" ? {} : { diagnostic: failure.message }),
867
1020
  details: failure.details,
868
1021
  },
869
1022
  });
@@ -18,6 +18,12 @@ import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
18
18
  import { issueRoot, subjectPath } from "./work-subject-identity.ts";
19
19
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
20
20
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
21
+ import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
22
+ import {
23
+ hasUpstreamErrorTestimony,
24
+ isNonSuccessHttpStatus,
25
+ projectConfirmedRemotePayload,
26
+ } from "./upstream-error-testimony.ts";
21
27
 
22
28
  export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
23
29
  export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
@@ -1092,19 +1098,37 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1092
1098
  }
1093
1099
  assignProviderFailure(navigatorProviderFailureFromStatus(status));
1094
1100
  };
1095
- const humanProviderError = <T extends Record<string, unknown>>(error: T): T => {
1096
- const human = { ...error };
1097
- delete human.statusCode;
1098
- delete human.code;
1099
- delete human.navigatorFailure;
1100
- return human;
1101
+ /** Project only fields actually held on the call surface — never forge upstream payload. */
1102
+ const projectHeldUpstream = (error: unknown): Record<string, unknown> => {
1103
+ if (!exactRecord(error)) return {};
1104
+ // Shape reading stays local; testimony + confirmed-remote payload use shared authority.
1105
+ const status = typeof error.statusCode === "number"
1106
+ ? error.statusCode
1107
+ : typeof error.status === "number"
1108
+ ? error.status
1109
+ : typeof error.httpStatus === "number"
1110
+ ? error.httpStatus
1111
+ : undefined;
1112
+ const httpStatus = isNonSuccessHttpStatus(status) ? status : undefined;
1113
+ const diagnostics = Array.isArray(error.diagnostics) && error.diagnostics.length > 0
1114
+ ? error.diagnostics
1115
+ : undefined;
1116
+ const testimony = hasUpstreamErrorTestimony({
1117
+ ...(httpStatus === undefined ? {} : { httpStatus }),
1118
+ ...(diagnostics === undefined ? {} : { diagnostics }),
1119
+ });
1120
+ return {
1121
+ ...(httpStatus === undefined ? {} : { statusCode: httpStatus, status: httpStatus }),
1122
+ ...(diagnostics === undefined ? {} : { diagnostics }),
1123
+ ...(testimony ? projectConfirmedRemotePayload(error) : {}),
1124
+ };
1101
1125
  };
1102
- let providerFailureEvidenceNumber = 0;
1103
- let providerFailureEvidence: { id: string; error: unknown } | undefined;
1104
- const retainProviderFailure = (error: unknown): string => {
1105
- const id = `navigator-provider-failure-${++providerFailureEvidenceNumber}`;
1106
- providerFailureEvidence = { id, error };
1107
- return id;
1126
+ /** Keep held upstream fields; strip only the local navigatorFailure classification marker. */
1127
+ const retainUpstreamMessage = <T extends Record<string, unknown>>(error: T): T => {
1128
+ if (!("navigatorFailure" in error)) return error;
1129
+ const copy = { ...error };
1130
+ delete copy.navigatorFailure;
1131
+ return copy;
1108
1132
  };
1109
1133
  const setupFailureMessage = (error: unknown) => ({
1110
1134
  role: "assistant" as const,
@@ -1115,15 +1139,27 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1115
1139
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
1116
1140
  stopReason: "error" as const,
1117
1141
  errorMessage: error instanceof Error ? error.message : String(error),
1118
- navigatorFailureEvidenceId: retainProviderFailure(error),
1119
1142
  timestamp: Date.now(),
1143
+ ...projectHeldUpstream(error),
1120
1144
  });
1145
+ const persistNavigatorHttpObservation = async (
1146
+ status: number,
1147
+ model: unknown,
1148
+ ): Promise<void> => {
1149
+ const runDir = process.env.AK_ROLE_RUN_DIR;
1150
+ if (typeof runDir !== "string" || runDir.trim() === "") return;
1151
+ const provider = exactRecord(model) && typeof model.provider === "string" && model.provider.trim() !== ""
1152
+ ? model.provider
1153
+ : undefined;
1154
+ if (provider === undefined) return;
1155
+ await recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider });
1156
+ };
1121
1157
  const instrumentProvider = <TProvider extends NonNullable<typeof provider>>(sourceProvider: TProvider): TProvider => {
1122
1158
  type StreamFn = TProvider["stream"];
1123
1159
  type StreamSimpleFn = TProvider["streamSimple"];
1124
1160
  type StreamOptionsArg = Parameters<StreamFn>[2];
1125
1161
  type StreamSimpleOptionsArg = Parameters<StreamSimpleFn>[2];
1126
- const instrumentStreamOptions = <TOptions>(options: TOptions): TOptions => {
1162
+ const instrumentStreamOptions = <TOptions>(options: TOptions, observedStatus: { value?: number }): TOptions => {
1127
1163
  const record = (exactRecord(options) ? options : {}) as Record<string, unknown>;
1128
1164
  const previous = typeof record.onResponse === "function"
1129
1165
  ? record.onResponse as (response: { status: number; headers: Record<string, string> }, model: unknown) => void | Promise<void>
@@ -1131,12 +1167,23 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1131
1167
  return {
1132
1168
  ...record,
1133
1169
  onResponse: async (response: { status: number; headers: Record<string, string> }, model: unknown) => {
1170
+ observedStatus.value = response.status;
1134
1171
  classifyProviderResponseStatus(response.status);
1172
+ // Durable run-dossier sink for typed non-success HTTP (2xx clears).
1173
+ await persistNavigatorHttpObservation(response.status, model);
1135
1174
  await previous?.(response, model);
1136
1175
  },
1137
1176
  } as TOptions;
1138
1177
  };
1139
- const wrapProviderStream = (source: ReturnType<StreamFn>): ReturnType<StreamFn> => {
1178
+ /** Attach a directly observed non-success HTTP status when the terminal message lacks one. */
1179
+ const withObservedStatus = <T extends Record<string, unknown>>(message: T, observedStatus: number | undefined): T => {
1180
+ if (observedStatus === undefined || observedStatus >= 200 && observedStatus < 300) return message;
1181
+ if (typeof message.statusCode === "number" || typeof message.status === "number" || typeof message.httpStatus === "number") {
1182
+ return message;
1183
+ }
1184
+ return { ...message, statusCode: observedStatus, status: observedStatus };
1185
+ };
1186
+ const wrapProviderStream = (source: ReturnType<StreamFn>, observedStatus: { value?: number }): ReturnType<StreamFn> => {
1140
1187
  const wrapped = createAssistantMessageEventStream();
1141
1188
  void (async () => {
1142
1189
  let result: Awaited<ReturnType<typeof source.result>> | undefined;
@@ -1147,13 +1194,14 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1147
1194
  sawTerminal = true;
1148
1195
  if (event.type === "done" && exactRecord(event.message)) {
1149
1196
  assignProviderFailure(navigatorProviderFailureFromDiagnostics(event.message.diagnostics));
1150
- result = humanProviderError(event.message) as typeof event.message;
1197
+ // Keep held message/status/body/code/errno/diagnostics on the session surface.
1198
+ result = withObservedStatus(retainUpstreamMessage(event.message), observedStatus.value) as typeof event.message;
1151
1199
  wrapped.push({ ...event, message: result });
1152
1200
  continue;
1153
1201
  }
1154
1202
  if (event.type === "error" && exactRecord(event.error)) {
1155
1203
  classifyProviderStreamError(event.error);
1156
- result = humanProviderError(event.error) as typeof event.error;
1204
+ result = withObservedStatus(retainUpstreamMessage(event.error), observedStatus.value) as typeof event.error;
1157
1205
  wrapped.push({ ...event, error: result });
1158
1206
  continue;
1159
1207
  }
@@ -1163,24 +1211,24 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1163
1211
  // Only await result after a terminal done/error event. end(undefined) leaves result() unresolved forever.
1164
1212
  if (sawTerminal) {
1165
1213
  const terminal = await source.result();
1166
- if (result === undefined && exactRecord(terminal)) result = humanProviderError(terminal) as typeof terminal;
1167
- else if (result === undefined) result = terminal;
1214
+ if (result === undefined && exactRecord(terminal)) {
1215
+ result = withObservedStatus(retainUpstreamMessage(terminal), observedStatus.value) as typeof terminal;
1216
+ } else if (result === undefined) result = terminal;
1168
1217
  }
1169
1218
  } catch (error) {
1170
- // Contract: README.md#Navigator-attendance — synthetic provider errors retain a stable pointer to the raw rejection while exposing only human-safe text.
1219
+ // Stream throw: project held upstream fields onto the synthetic terminal message (session is durable).
1171
1220
  classifyProviderStreamError(error);
1172
- if (providerFailure === undefined) providerFailure = { source: "transport", cause: "transport" };
1221
+ if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
1173
1222
  if (!sawTerminal) {
1174
- const message = setupFailureMessage(error);
1223
+ const message = withObservedStatus(setupFailureMessage(error), observedStatus.value);
1175
1224
  wrapped.push({ type: "error", reason: "error", error: message });
1176
1225
  result = message;
1177
1226
  sawTerminal = true;
1178
1227
  }
1179
1228
  } finally {
1180
- // No terminal stream event means the provider produced no response.
1181
- // Emit a synthetic error so callers do not hang waiting for done/error, and never await an unresolved source.result().
1229
+ // No terminal stream event: unknown only — do not forge upstream payload.
1182
1230
  if (!sawTerminal) {
1183
- if (providerFailure === undefined) providerFailure = { source: "transport", cause: "transport" };
1231
+ if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
1184
1232
  const message = setupFailureMessage(new Error("Navigator provider produced no response"));
1185
1233
  wrapped.push({ type: "error", reason: "error", error: message });
1186
1234
  result = message;
@@ -1191,15 +1239,16 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1191
1239
  })();
1192
1240
  return wrapped as ReturnType<StreamFn>;
1193
1241
  };
1194
- const invokeInstrumentedStream = (invoke: () => ReturnType<StreamFn>): ReturnType<StreamFn> => {
1242
+ const invokeInstrumentedStream = (invoke: (observedStatus: { value?: number }) => ReturnType<StreamFn>): ReturnType<StreamFn> => {
1195
1243
  // Reset before invoking the selected provider so setup throws cannot leave a prior call's fact.
1196
1244
  providerFailure = undefined;
1245
+ const observedStatus: { value?: number } = {};
1197
1246
  try {
1198
- return wrapProviderStream(invoke());
1247
+ return wrapProviderStream(invoke(observedStatus), observedStatus);
1199
1248
  } catch (error) {
1200
- // Contract: README.md#Navigator-attendance — setup failures become terminal synthetic errors, with the raw rejection retained by stable evidence pointer.
1249
+ // Setup failures become terminal synthetic errors with held upstream fields projected when present.
1201
1250
  classifyProviderStreamError(error);
1202
- if (providerFailure === undefined) providerFailure = { source: "transport", cause: "transport" };
1251
+ if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
1203
1252
  const wrapped = createAssistantMessageEventStream();
1204
1253
  const message = setupFailureMessage(error);
1205
1254
  queueMicrotask(() => {
@@ -1212,12 +1261,16 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1212
1261
  return {
1213
1262
  ...sourceProvider,
1214
1263
  stream(model: Parameters<StreamFn>[0], streamContext: Parameters<StreamFn>[1], options: StreamOptionsArg) {
1215
- const instrumented = instrumentStreamOptions(options);
1216
- return invokeInstrumentedStream(() => sourceProvider.stream(model, streamContext, instrumented) as ReturnType<StreamFn>) as ReturnType<StreamFn>;
1264
+ return invokeInstrumentedStream((observedStatus) => {
1265
+ const instrumented = instrumentStreamOptions(options, observedStatus);
1266
+ return sourceProvider.stream(model, streamContext, instrumented) as ReturnType<StreamFn>;
1267
+ }) as ReturnType<StreamFn>;
1217
1268
  },
1218
1269
  streamSimple(model: Parameters<StreamSimpleFn>[0], streamContext: Parameters<StreamSimpleFn>[1], options: StreamSimpleOptionsArg) {
1219
- const instrumented = instrumentStreamOptions(options);
1220
- return invokeInstrumentedStream(() => sourceProvider.streamSimple(model, streamContext, instrumented) as ReturnType<StreamFn>) as ReturnType<StreamSimpleFn>;
1270
+ return invokeInstrumentedStream((observedStatus) => {
1271
+ const instrumented = instrumentStreamOptions(options, observedStatus);
1272
+ return sourceProvider.streamSimple(model, streamContext, instrumented) as ReturnType<StreamFn>;
1273
+ }) as ReturnType<StreamSimpleFn>;
1221
1274
  },
1222
1275
  } as TProvider;
1223
1276
  };