@mono-agent/agent-runtime 0.18.2 → 0.19.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.
- package/MIGRATION.md +9 -1
- package/README.md +2 -0
- package/package.json +3 -1
- package/src/agent/tools/read.js +81 -1
- package/src/ai/providers/pi-native/result-builder.js +11 -0
- package/src/ai/providers/pi-native.js +22 -2
- package/src/ai/providers/transport-errors.js +154 -0
- package/types/ai/providers/transport-errors.d.ts +44 -0
package/MIGRATION.md
CHANGED
|
@@ -17,6 +17,14 @@ the configuration schema.
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
+
## 0.19.0
|
|
21
|
+
|
|
22
|
+
- **Oversized `Read` images:** raster image results with an edge above 8,000
|
|
23
|
+
pixels are now normalized before provider embedding. Source files are never
|
|
24
|
+
modified; safe images retain their exact bytes, resized GIF/WebP input keeps
|
|
25
|
+
its animation, and resized BMP input becomes PNG. Undecodable image data now
|
|
26
|
+
fails before the runtime creates an image content block.
|
|
27
|
+
|
|
20
28
|
## 0.18.2
|
|
21
29
|
|
|
22
30
|
- **Normalized native-subagent activity:** Claude SDK/CLI and Codex app-server
|
|
@@ -482,7 +490,7 @@ a compatibility subpath.
|
|
|
482
490
|
|
|
483
491
|
## Version
|
|
484
492
|
|
|
485
|
-
This guide describes the published `0.
|
|
493
|
+
This guide describes the published `0.19.x` package contract. Keep
|
|
486
494
|
`@mono-agent/agent-runtime`, `@mono-agent/runtime-adapter`, and other
|
|
487
495
|
`@mono-agent/*` packages on the same lockstep version when upgrading. The paired
|
|
488
496
|
runtime adapter no longer exposes `piReasoningSummary` in its run-options type.
|
package/README.md
CHANGED
|
@@ -1094,6 +1094,8 @@ The kernel's tool-bloat guard (`agent/tool-bloat.js`, internal) enforces a 256 K
|
|
|
1094
1094
|
|
|
1095
1095
|
Hosts that don't supply `persistArtifact` get the truncation summary but no on-disk capture.
|
|
1096
1096
|
|
|
1097
|
+
Before that byte cap runs, the builtin `Read` tool normalizes raster images with an edge longer than 8,000 px to fit within an 8,000 × 8,000 px box. Resizing preserves aspect ratio and the source format (resized BMP input becomes PNG), retains GIF/WebP animation, and never modifies the source file. Images already within the limit are embedded byte-for-byte unchanged.
|
|
1098
|
+
|
|
1097
1099
|
### Context compaction
|
|
1098
1100
|
|
|
1099
1101
|
The sole pi bridge runs on pi-agent-core's native `AgentHarness`. pi performs **no**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mono-agent/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -144,9 +144,11 @@
|
|
|
144
144
|
"@mozilla/readability": "0.6.0",
|
|
145
145
|
"@opencode-ai/sdk": "^1.15.13",
|
|
146
146
|
"@vscode/ripgrep": "1.18.0",
|
|
147
|
+
"bmp-ts": "1.0.9",
|
|
147
148
|
"cross-spawn": "^7.0.6",
|
|
148
149
|
"defuddle": "0.19.2",
|
|
149
150
|
"linkedom": "0.18.13",
|
|
151
|
+
"sharp": "0.35.3",
|
|
150
152
|
"unpdf": "1.8.0",
|
|
151
153
|
"zod": "^4.3.6"
|
|
152
154
|
},
|
package/src/agent/tools/read.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { extname } from "node:path";
|
|
3
|
+
import { decode as decodeBmp } from "bmp-ts";
|
|
4
|
+
import sharp from "sharp";
|
|
3
5
|
import {
|
|
4
6
|
DEFAULT_MAX_READ_CHARS,
|
|
5
7
|
DEFAULT_READ_LINES,
|
|
@@ -20,6 +22,82 @@ const IMAGE_MIME_BY_EXT = {
|
|
|
20
22
|
".bmp": "image/bmp",
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
// Anthropic rejects images with an edge longer than 8,000 px. Normalize Read
|
|
26
|
+
// results to that shared provider-safe ceiling before the tool-result byte cap
|
|
27
|
+
// runs, while leaving the source file untouched.
|
|
28
|
+
const MAX_INLINE_IMAGE_EDGE_PX = 8_000;
|
|
29
|
+
const ANIMATED_IMAGE_MIME_TYPES = new Set(["image/gif", "image/webp"]);
|
|
30
|
+
const OUTPUT_MIME_BY_FORMAT = {
|
|
31
|
+
png: "image/png",
|
|
32
|
+
jpeg: "image/jpeg",
|
|
33
|
+
gif: "image/gif",
|
|
34
|
+
webp: "image/webp",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} target
|
|
39
|
+
* @param {string} filePath
|
|
40
|
+
* @param {string} imageMime
|
|
41
|
+
*/
|
|
42
|
+
async function readImageForModel(target, filePath, imageMime) {
|
|
43
|
+
const source = readFileSync(target);
|
|
44
|
+
const inputOptions = { animated: ANIMATED_IMAGE_MIME_TYPES.has(imageMime) };
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
let width;
|
|
48
|
+
let height;
|
|
49
|
+
let createPipeline;
|
|
50
|
+
|
|
51
|
+
if (imageMime === "image/bmp") {
|
|
52
|
+
// The prebuilt Sharp binaries do not include a BMP loader. Decode to raw
|
|
53
|
+
// RGBA first, then let Sharp handle the provider-safe resize and PNG output.
|
|
54
|
+
const decoded = decodeBmp(source, { toRGBA: true });
|
|
55
|
+
width = decoded.width;
|
|
56
|
+
height = Math.abs(decoded.height);
|
|
57
|
+
createPipeline = () => sharp(decoded.data, {
|
|
58
|
+
raw: { width, height, channels: 4 },
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
const metadata = await sharp(source, inputOptions).metadata();
|
|
62
|
+
width = metadata.width;
|
|
63
|
+
// Sharp exposes animated images as a vertical stack internally. Providers
|
|
64
|
+
// care about the dimensions of each frame, not the height of that stack.
|
|
65
|
+
height = metadata.pageHeight ?? metadata.height;
|
|
66
|
+
createPipeline = () => sharp(source, inputOptions);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
|
70
|
+
throw new Error("could not determine positive pixel dimensions");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (width <= MAX_INLINE_IMAGE_EDGE_PX && height <= MAX_INLINE_IMAGE_EDGE_PX) {
|
|
74
|
+
return { data: source, mimeType: imageMime };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let pipeline = createPipeline()
|
|
78
|
+
.autoOrient()
|
|
79
|
+
.resize({
|
|
80
|
+
width: MAX_INLINE_IMAGE_EDGE_PX,
|
|
81
|
+
height: MAX_INLINE_IMAGE_EDGE_PX,
|
|
82
|
+
fit: "inside",
|
|
83
|
+
withoutEnlargement: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Sharp cannot emit BMP, so resized BMP input becomes lossless PNG.
|
|
87
|
+
if (imageMime === "image/bmp") pipeline = pipeline.png();
|
|
88
|
+
|
|
89
|
+
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
90
|
+
const mimeType = OUTPUT_MIME_BY_FORMAT[info.format];
|
|
91
|
+
if (mimeType === undefined) {
|
|
92
|
+
throw new Error(`unsupported normalized image format: ${info.format}`);
|
|
93
|
+
}
|
|
94
|
+
return { data, mimeType };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
+
return { error: `Error: Unable to read image ${filePath}: ${reason}` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
/**
|
|
24
102
|
* @param {{file_path: string, offset?: number, start_line?: number, limit?: number, max_output_chars?: number, workdir?: string}} params
|
|
25
103
|
* @param {{sandboxPolicy?: any, ctx?: any}} [options]
|
|
@@ -34,7 +112,9 @@ export async function readToolImpl({ file_path, offset = 0, start_line, limit, m
|
|
|
34
112
|
// capped by the shared tool-result bloat guard.
|
|
35
113
|
const imageMime = IMAGE_MIME_BY_EXT[extname(target).toLowerCase()];
|
|
36
114
|
if (imageMime !== undefined) {
|
|
37
|
-
|
|
115
|
+
const image = await readImageForModel(target, file_path, imageMime);
|
|
116
|
+
if (image.error !== undefined) return image.error;
|
|
117
|
+
return { kind: "image", data: image.data.toString("base64"), mimeType: image.mimeType };
|
|
38
118
|
}
|
|
39
119
|
const content = readFileSync(target, "utf8");
|
|
40
120
|
let lines = content.split("\n");
|
|
@@ -323,6 +323,8 @@ export function buildDiagnostics(params) {
|
|
|
323
323
|
lastToolName,
|
|
324
324
|
structuredRetry,
|
|
325
325
|
contextCompactionDiagnostics,
|
|
326
|
+
transportErrorCode,
|
|
327
|
+
transportErrorSource,
|
|
326
328
|
} = params;
|
|
327
329
|
return {
|
|
328
330
|
provider_session_id: providerSessionId,
|
|
@@ -335,6 +337,15 @@ export function buildDiagnostics(params) {
|
|
|
335
337
|
pi_max_retries: maxRetries,
|
|
336
338
|
pi_transport_requested: piTransport,
|
|
337
339
|
...(lastToolName ? { last_tool_name: lastToolName } : {}),
|
|
340
|
+
// Present only when an opaque transport failure was resolved to a real
|
|
341
|
+
// reason. `provider_transport_error_source` says whether that came from the
|
|
342
|
+
// error's own cause chain (exact) or from process-wide correlation.
|
|
343
|
+
...(transportErrorCode
|
|
344
|
+
? {
|
|
345
|
+
provider_transport_error_code: transportErrorCode,
|
|
346
|
+
provider_transport_error_source: transportErrorSource,
|
|
347
|
+
}
|
|
348
|
+
: {}),
|
|
338
349
|
...structuredOutputRetryDiagnostics(
|
|
339
350
|
structuredRetry.attempts,
|
|
340
351
|
structuredRetry.reason,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "./pi-messages.js";
|
|
45
45
|
import { emitCaptured } from "./pi-events.js";
|
|
46
46
|
import { normalizePiErrorMessage } from "./pi-errors.js";
|
|
47
|
+
import { annotateProviderErrorMessage, installTransportErrorProbe } from "./transport-errors.js";
|
|
47
48
|
import {
|
|
48
49
|
runStructuredOutputFinalizationRetry,
|
|
49
50
|
shouldRetryStructuredOutputFinalization,
|
|
@@ -252,6 +253,10 @@ function splitUserContent(content) {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
256
|
+
// Idempotent; arms the undici diagnostics-channel probe so a transport
|
|
257
|
+
// failure during this run can be resolved back to a real reason even after
|
|
258
|
+
// an intermediate layer flattens the Error to its message.
|
|
259
|
+
installTransportErrorProbe();
|
|
255
260
|
const resolved = options.model;
|
|
256
261
|
const start = Date.now();
|
|
257
262
|
const events = [];
|
|
@@ -682,7 +687,15 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
682
687
|
: (stopReason === "error" || stopReason === "aborted"
|
|
683
688
|
? lastAssistant?.errorMessage || runError?.message || "Pi agent aborted before final output"
|
|
684
689
|
: (runError ? runError.message || String(runError) : null));
|
|
685
|
-
|
|
690
|
+
// pi-agent-core stores `error.message` on the failure assistant message and
|
|
691
|
+
// discards the cause, so `runError` is usually the only object still
|
|
692
|
+
// carrying one. When neither has a cause, fall back to the correlated
|
|
693
|
+
// transport probe rather than surfacing a bare "terminated".
|
|
694
|
+
const annotatedError = annotateProviderErrorMessage(
|
|
695
|
+
normalizePiErrorMessage(rawErrorMessage),
|
|
696
|
+
runError,
|
|
697
|
+
);
|
|
698
|
+
const errorMessage = annotatedError.message;
|
|
686
699
|
|
|
687
700
|
const structuredRetry = {
|
|
688
701
|
attempts: structuredOutputFinalizationRetryAttempts,
|
|
@@ -702,6 +715,8 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
702
715
|
lastToolName: runState.lastToolName,
|
|
703
716
|
structuredRetry,
|
|
704
717
|
contextCompactionDiagnostics: runState.compaction.diagnostics,
|
|
718
|
+
transportErrorCode: annotatedError.causeCode,
|
|
719
|
+
transportErrorSource: annotatedError.causeSource,
|
|
705
720
|
});
|
|
706
721
|
const errorDetails = buildErrorDetails({
|
|
707
722
|
errorMessage,
|
|
@@ -812,7 +827,12 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
812
827
|
// leaf for host/runtime-side throws that landed after the harness already
|
|
813
828
|
// mutated the live session (guards preserved in cleanupSessionOnThrow).
|
|
814
829
|
await cleanupSessionOnThrow(runState, { durableRepo });
|
|
815
|
-
|
|
830
|
+
// The throw path still holds the original Error, so its cause chain is the
|
|
831
|
+
// authoritative source here — no correlation guesswork needed.
|
|
832
|
+
const errorMessage = annotateProviderErrorMessage(
|
|
833
|
+
normalizePiErrorMessage(err?.message || String(err)),
|
|
834
|
+
err,
|
|
835
|
+
).message;
|
|
816
836
|
const isRetryable = retryableProviderFailureInfo({
|
|
817
837
|
errorText: errorMessage,
|
|
818
838
|
failureKind: "provider_unavailable",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Recover the real reason behind opaque provider transport failures.
|
|
2
|
+
//
|
|
3
|
+
// Node's fetch (undici) reports a cut response body as `TypeError: terminated`
|
|
4
|
+
// with the actual reason — UND_ERR_BODY_TIMEOUT, ECONNRESET, "other side
|
|
5
|
+
// closed" — only on `error.cause`. Several layers between the socket and us
|
|
6
|
+
// flatten errors to `error.message`, most notably pi-agent-core's
|
|
7
|
+
// `handleRunFailure`, which stores `error.message` and drops the cause before
|
|
8
|
+
// any mono-agent code sees the Error object. The result is a run that fails
|
|
9
|
+
// with the single uninformative word "terminated".
|
|
10
|
+
//
|
|
11
|
+
// Two recovery paths, in order of trustworthiness:
|
|
12
|
+
//
|
|
13
|
+
// 1. `describeErrorCause` walks the cause chain of an Error we still hold.
|
|
14
|
+
// Exact, but only available where the original object survives.
|
|
15
|
+
// 2. `recentTransportErrorCode` reads a bounded ring of `undici:request:error`
|
|
16
|
+
// diagnostics-channel events. This sees the error before any library
|
|
17
|
+
// flattens it, at the cost of attribution: the channel is process-wide, so
|
|
18
|
+
// with concurrent requests in flight we cannot prove which run a given
|
|
19
|
+
// socket error belongs to. Correlation is reported as such, and a window
|
|
20
|
+
// containing conflicting codes reports ambiguity instead of guessing.
|
|
21
|
+
|
|
22
|
+
import diagnosticsChannel from "node:diagnostics_channel";
|
|
23
|
+
|
|
24
|
+
// Messages that carry no diagnostic content on their own. Annotating only these
|
|
25
|
+
// keeps already-descriptive provider errors untouched.
|
|
26
|
+
const OPAQUE_ERROR_RE = /^(?:terminated|fetch failed|connection error\.?|network error|socket hang up|premature close|other side closed)$/i;
|
|
27
|
+
|
|
28
|
+
const MAX_RECORDED = 32;
|
|
29
|
+
// A stream cut is observed on the socket within milliseconds of the rejection
|
|
30
|
+
// surfacing. Anything older is a different request's failure.
|
|
31
|
+
const DEFAULT_CORRELATION_WINDOW_MS = 5_000;
|
|
32
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
33
|
+
|
|
34
|
+
/** @type {{code: string, at: number, origin: string|null}[]} */
|
|
35
|
+
const recorded = [];
|
|
36
|
+
let installed = false;
|
|
37
|
+
|
|
38
|
+
function codeFromError(error) {
|
|
39
|
+
if (!error || typeof error !== "object") return null;
|
|
40
|
+
const code = error.code ?? error.errno;
|
|
41
|
+
if (typeof code === "string" && code.trim()) return code.trim();
|
|
42
|
+
// Undici's timeout errors expose a stable `name` even when `code` is absent.
|
|
43
|
+
const name = typeof error.name === "string" ? error.name.trim() : "";
|
|
44
|
+
if (name && name !== "Error" && name !== "TypeError") return name;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walk an error's `cause` chain and return the first transport-level code found.
|
|
50
|
+
* Returns null when the chain carries no code (i.e. nothing worth appending).
|
|
51
|
+
* @param {unknown} error
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
export function describeErrorCause(error) {
|
|
55
|
+
/** @type {any} */
|
|
56
|
+
let current = error;
|
|
57
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth += 1) {
|
|
58
|
+
// Skip the outermost error: its code (if any) is what the caller already
|
|
59
|
+
// has. We want the reason underneath it.
|
|
60
|
+
if (depth > 0) {
|
|
61
|
+
const code = codeFromError(current);
|
|
62
|
+
if (code) return code;
|
|
63
|
+
}
|
|
64
|
+
current = current?.cause;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to undici's request-error channel. Idempotent and safe to call on
|
|
71
|
+
* every run; a Node build without the channel simply records nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function installTransportErrorProbe() {
|
|
74
|
+
if (installed) return;
|
|
75
|
+
installed = true;
|
|
76
|
+
try {
|
|
77
|
+
diagnosticsChannel.subscribe("undici:request:error", (/** @type {any} */ message) => {
|
|
78
|
+
const error = message?.error;
|
|
79
|
+
const code = codeFromError(error) || describeErrorCause(error);
|
|
80
|
+
if (!code) return;
|
|
81
|
+
/** @type {string|null} */
|
|
82
|
+
let origin = null;
|
|
83
|
+
try {
|
|
84
|
+
const raw = message?.request?.origin;
|
|
85
|
+
origin = typeof raw === "string" ? raw : (raw?.origin ?? null);
|
|
86
|
+
} catch {
|
|
87
|
+
origin = null;
|
|
88
|
+
}
|
|
89
|
+
recorded.push({ code, at: Date.now(), origin });
|
|
90
|
+
if (recorded.length > MAX_RECORDED) recorded.splice(0, recorded.length - MAX_RECORDED);
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
// Channel unavailable on this runtime: fall back to cause-chain walking only.
|
|
94
|
+
installed = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Most recent transport error code seen within the correlation window.
|
|
100
|
+
* `ambiguous` is true when the window holds more than one distinct code, in
|
|
101
|
+
* which case attribution to a specific run would be a guess.
|
|
102
|
+
* @param {{withinMs?: number, now?: number}} [opts]
|
|
103
|
+
* @returns {{code: string, ambiguous: boolean}|null}
|
|
104
|
+
*/
|
|
105
|
+
export function recentTransportErrorCode(opts = {}) {
|
|
106
|
+
const withinMs = Number.isFinite(Number(opts.withinMs))
|
|
107
|
+
? Number(opts.withinMs)
|
|
108
|
+
: DEFAULT_CORRELATION_WINDOW_MS;
|
|
109
|
+
const now = Number.isFinite(Number(opts.now)) ? Number(opts.now) : Date.now();
|
|
110
|
+
const fresh = recorded.filter((entry) => now - entry.at <= withinMs);
|
|
111
|
+
if (fresh.length === 0) return null;
|
|
112
|
+
const distinct = new Set(fresh.map((entry) => entry.code));
|
|
113
|
+
return { code: fresh[fresh.length - 1].code, ambiguous: distinct.size > 1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Append the underlying transport reason to an otherwise contentless provider
|
|
118
|
+
* error. Descriptive messages, and messages that already name their cause, are
|
|
119
|
+
* returned unchanged.
|
|
120
|
+
*
|
|
121
|
+
* @param {string|null} message normalized provider error text
|
|
122
|
+
* @param {unknown} [error] the original Error, when it survived
|
|
123
|
+
* @returns {{message: string|null, causeCode: string|null, causeSource: "cause_chain"|"transport_probe"|null}}
|
|
124
|
+
*/
|
|
125
|
+
export function annotateProviderErrorMessage(message, error) {
|
|
126
|
+
const text = String(message || "").trim();
|
|
127
|
+
if (!text) return { message: message ?? null, causeCode: null, causeSource: null };
|
|
128
|
+
if (!OPAQUE_ERROR_RE.test(text)) return { message: text, causeCode: null, causeSource: null };
|
|
129
|
+
|
|
130
|
+
const fromChain = describeErrorCause(error);
|
|
131
|
+
if (fromChain) {
|
|
132
|
+
return { message: `${text} (${fromChain})`, causeCode: fromChain, causeSource: "cause_chain" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const correlated = recentTransportErrorCode();
|
|
136
|
+
if (correlated && !correlated.ambiguous) {
|
|
137
|
+
return {
|
|
138
|
+
message: `${text} (${correlated.code}, correlated)`,
|
|
139
|
+
causeCode: correlated.code,
|
|
140
|
+
causeSource: "transport_probe",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { message: text, causeCode: null, causeSource: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Test seam: drop recorded transport errors. */
|
|
147
|
+
export function resetTransportErrorProbeForTests() {
|
|
148
|
+
recorded.length = 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Test seam: record a transport error without a real socket. */
|
|
152
|
+
export function recordTransportErrorForTests(code, at = Date.now()) {
|
|
153
|
+
recorded.push({ code, at, origin: null });
|
|
154
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk an error's `cause` chain and return the first transport-level code found.
|
|
3
|
+
* Returns null when the chain carries no code (i.e. nothing worth appending).
|
|
4
|
+
* @param {unknown} error
|
|
5
|
+
* @returns {string|null}
|
|
6
|
+
*/
|
|
7
|
+
export function describeErrorCause(error: unknown): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Subscribe to undici's request-error channel. Idempotent and safe to call on
|
|
10
|
+
* every run; a Node build without the channel simply records nothing.
|
|
11
|
+
*/
|
|
12
|
+
export function installTransportErrorProbe(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Most recent transport error code seen within the correlation window.
|
|
15
|
+
* `ambiguous` is true when the window holds more than one distinct code, in
|
|
16
|
+
* which case attribution to a specific run would be a guess.
|
|
17
|
+
* @param {{withinMs?: number, now?: number}} [opts]
|
|
18
|
+
* @returns {{code: string, ambiguous: boolean}|null}
|
|
19
|
+
*/
|
|
20
|
+
export function recentTransportErrorCode(opts?: {
|
|
21
|
+
withinMs?: number;
|
|
22
|
+
now?: number;
|
|
23
|
+
}): {
|
|
24
|
+
code: string;
|
|
25
|
+
ambiguous: boolean;
|
|
26
|
+
} | null;
|
|
27
|
+
/**
|
|
28
|
+
* Append the underlying transport reason to an otherwise contentless provider
|
|
29
|
+
* error. Descriptive messages, and messages that already name their cause, are
|
|
30
|
+
* returned unchanged.
|
|
31
|
+
*
|
|
32
|
+
* @param {string|null} message normalized provider error text
|
|
33
|
+
* @param {unknown} [error] the original Error, when it survived
|
|
34
|
+
* @returns {{message: string|null, causeCode: string|null, causeSource: "cause_chain"|"transport_probe"|null}}
|
|
35
|
+
*/
|
|
36
|
+
export function annotateProviderErrorMessage(message: string | null, error?: unknown): {
|
|
37
|
+
message: string | null;
|
|
38
|
+
causeCode: string | null;
|
|
39
|
+
causeSource: "cause_chain" | "transport_probe" | null;
|
|
40
|
+
};
|
|
41
|
+
/** Test seam: drop recorded transport errors. */
|
|
42
|
+
export function resetTransportErrorProbeForTests(): void;
|
|
43
|
+
/** Test seam: record a transport error without a real socket. */
|
|
44
|
+
export function recordTransportErrorForTests(code: any, at?: number): void;
|