@oh-my-pi/pi-utils 18.1.9 → 18.1.11
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/CHANGELOG.md +10 -0
- package/dist/types/fetch-retry.d.ts +4 -2
- package/dist/types/procmgr.d.ts +2 -0
- package/package.json +2 -2
- package/src/fetch-retry.ts +72 -15
- package/src/procmgr.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.11] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `extractRetryHint` dropping the longer timing signal when an error body carries both an account reset and an appended retry hint: competing signals now merge by longest window instead of first match, so retries honor the provider's full backoff.
|
|
10
|
+
|
|
5
11
|
## [18.1.7] - 2026-09-03
|
|
6
12
|
|
|
7
13
|
### Added
|
|
8
14
|
|
|
9
15
|
- Added the public `getTinyWorkerRuntimeDir()` utility, which returns the standard `~/.omp/run/tiny` directory for tiny-worker runtime data.
|
|
10
16
|
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed retry classification for Bun's bare `Socket is closed` transport error.
|
|
20
|
+
|
|
11
21
|
## [18.1.6] - 2026-09-03
|
|
12
22
|
|
|
13
23
|
### Added
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* - `Please retry in 250ms` / `Please retry in 12s`
|
|
15
15
|
* - `"retryDelay": "34.074824224s"` (JSON error detail field)
|
|
16
16
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
17
|
-
* - `retry-after-ms=98497000`
|
|
17
|
+
* - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
|
|
18
18
|
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
19
19
|
*
|
|
20
|
-
* Returns `undefined` if no signal is found
|
|
20
|
+
* Returns `undefined` if no signal is found, or `0` when the provider
|
|
21
|
+
* explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
|
|
22
|
+
* reset timestamp that has already elapsed).
|
|
21
23
|
*/
|
|
22
24
|
export declare function extractRetryHint(source: Response | Headers | null | undefined, body?: string): number | undefined;
|
|
23
25
|
export interface FetchWithRetryOptions extends RequestInit {
|
package/dist/types/procmgr.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare function isCmdShell(shell: string): boolean;
|
|
|
32
32
|
* parse `-l` as the command and fail with `The term '-l' is not recognized`.
|
|
33
33
|
*/
|
|
34
34
|
export declare function isPowerShell(shell: string): boolean;
|
|
35
|
+
/** Whether the executable is a known shell whose command language uses POSIX quoting. */
|
|
36
|
+
export declare function isPosixShell(shell: string): boolean;
|
|
35
37
|
/**
|
|
36
38
|
* Resolve a basic shell (bash or sh) as fallback.
|
|
37
39
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.1.
|
|
3
|
+
"version": "18.1.11",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
57
|
+
"@oh-my-pi/pi-natives": "18.1.11"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "^1.3.14"
|
package/src/fetch-retry.ts
CHANGED
|
@@ -16,8 +16,8 @@ const WILL_RESET_IN_PATTERN = /(?:will\s+)?reset in\s+~?\s*([0-9.]+)\s*(ms|sec|s
|
|
|
16
16
|
const WILL_RESET_AT_PATTERN =
|
|
17
17
|
/(?:will\s+)?reset at\s+([0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:?[0-9]{2})?)/i;
|
|
18
18
|
const CN_RESET_AT_PATTERN = /将在\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}:[0-9]{2})\s*重置/;
|
|
19
|
-
// "retry-after-ms=98497000"
|
|
20
|
-
const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms
|
|
19
|
+
// "retry-after-ms=98497000" / "retry-after-ms: 7200000" / "retry-after-ms = 7200000"
|
|
20
|
+
const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Server-suggested retry delay extraction. Merges the patterns historically used
|
|
@@ -35,10 +35,12 @@ const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms=([0-9]+)\b/i;
|
|
|
35
35
|
* - `Please retry in 250ms` / `Please retry in 12s`
|
|
36
36
|
* - `"retryDelay": "34.074824224s"` (JSON error detail field)
|
|
37
37
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
38
|
-
* - `retry-after-ms=98497000`
|
|
38
|
+
* - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
|
|
39
39
|
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
40
40
|
*
|
|
41
|
-
* Returns `undefined` if no signal is found
|
|
41
|
+
* Returns `undefined` if no signal is found, or `0` when the provider
|
|
42
|
+
* explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
|
|
43
|
+
* reset timestamp that has already elapsed).
|
|
42
44
|
*/
|
|
43
45
|
export function extractRetryHint(source: Response | Headers | null | undefined, body?: string): number | undefined {
|
|
44
46
|
const headers = source instanceof Headers ? source : (source?.headers ?? undefined);
|
|
@@ -83,6 +85,28 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
83
85
|
|
|
84
86
|
if (!body) return undefined;
|
|
85
87
|
|
|
88
|
+
// A body can carry several timing signals at once: the account-reset
|
|
89
|
+
// window plus header timing folded into the message text (already the
|
|
90
|
+
// max across response headers — see getRetryAfterMsFromHeaders in
|
|
91
|
+
// pi-ai). Honor the longest: retrying before either window clears
|
|
92
|
+
// re-hits a still-blocked credential and burns the retry budget.
|
|
93
|
+
let longestMs: number | undefined;
|
|
94
|
+
// A parsed-but-non-positive signal is a provider "retry now": an explicit
|
|
95
|
+
// `retry-after…=0` or an absolute reset that already elapsed. It must
|
|
96
|
+
// survive as 0 rather than collapse into "no hint found" — consumers
|
|
97
|
+
// substitute a heuristic wait (30-minute quota guess, default backoff)
|
|
98
|
+
// when the parse returns undefined, which would sleep a session the
|
|
99
|
+
// provider told to retry immediately.
|
|
100
|
+
let retryNow = false;
|
|
101
|
+
const consider = (ms: number | undefined): void => {
|
|
102
|
+
if (ms !== undefined && ms > 0 && (longestMs === undefined || ms > longestMs)) longestMs = ms;
|
|
103
|
+
};
|
|
104
|
+
const considerClamped = (ms: number | undefined): void => {
|
|
105
|
+
if (ms === undefined) return;
|
|
106
|
+
if (ms > 0) consider(ms);
|
|
107
|
+
else retryNow = true;
|
|
108
|
+
};
|
|
109
|
+
|
|
86
110
|
const quotaMatch = QUOTA_RESET_PATTERN.exec(body);
|
|
87
111
|
if (quotaMatch) {
|
|
88
112
|
const hours = quotaMatch[1] ? Number.parseInt(quotaMatch[1], 10) : 0;
|
|
@@ -90,7 +114,7 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
90
114
|
const seconds = Number.parseFloat(quotaMatch[3]!);
|
|
91
115
|
if (!Number.isNaN(seconds)) {
|
|
92
116
|
const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000;
|
|
93
|
-
|
|
117
|
+
consider(totalMs > 0 ? totalMs : undefined);
|
|
94
118
|
}
|
|
95
119
|
}
|
|
96
120
|
for (const pattern of [WILL_RESET_AT_PATTERN, CN_RESET_AT_PATTERN]) {
|
|
@@ -101,27 +125,23 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
101
125
|
const hasOffset = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized);
|
|
102
126
|
const parsed = Date.parse(hasOffset ? normalized : `${normalized}Z`);
|
|
103
127
|
if (!Number.isNaN(parsed) && parsed > Date.now()) {
|
|
104
|
-
|
|
128
|
+
consider(parsed - Date.now());
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
|
-
// Account-reset hints ("will reset in …") take precedence over short
|
|
109
|
-
// retry hints ("please retry in 5s"): a body carrying both must honour the
|
|
110
|
-
// longer account window, not the shorter generic one. QUOTA_RESET_PATTERN
|
|
111
|
-
// ("reset after …") above already runs first and stays first.
|
|
112
132
|
const accountResetMatch = WILL_RESET_IN_PATTERN.exec(body);
|
|
113
133
|
if (accountResetMatch?.[1]) {
|
|
114
134
|
const value = Number.parseFloat(accountResetMatch[1]);
|
|
115
135
|
if (Number.isFinite(value) && value > 0) {
|
|
116
136
|
const unitMs = unitToMs(accountResetMatch[2]!);
|
|
117
|
-
if (unitMs !== undefined)
|
|
137
|
+
if (unitMs !== undefined) consider(value * unitMs);
|
|
118
138
|
}
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
const retryAfterMsMatch = RETRY_AFTER_MS_BODY_PATTERN.exec(body);
|
|
122
142
|
if (retryAfterMsMatch?.[1]) {
|
|
123
143
|
const ms = Number(retryAfterMsMatch[1]);
|
|
124
|
-
if (Number.isFinite(ms)
|
|
144
|
+
if (Number.isFinite(ms)) considerClamped(ms);
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
for (const pattern of [PLEASE_RETRY_PATTERN, RETRY_DELAY_FIELD_PATTERN, TRY_AGAIN_PATTERN]) {
|
|
@@ -130,11 +150,45 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
130
150
|
const value = Number.parseFloat(match[1]);
|
|
131
151
|
if (Number.isFinite(value) && value > 0) {
|
|
132
152
|
const unitMs = unitToMs(match[2]!);
|
|
133
|
-
if (unitMs !== undefined)
|
|
153
|
+
if (unitMs !== undefined) consider(value * unitMs);
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
|
-
|
|
157
|
+
|
|
158
|
+
// Legacy text forms (also honored by older local parsers): a plain
|
|
159
|
+
// `retry-after` delta in seconds or as an HTTP date, and
|
|
160
|
+
// `x-ratelimit-reset[-ms]` counters. They compete in the same maximum —
|
|
161
|
+
// a longer legacy hint must not lose to a shorter reset phrase. Their
|
|
162
|
+
// non-positive readings (explicit zero, elapsed epoch/date) are the
|
|
163
|
+
// clamped retry-now signals above.
|
|
164
|
+
const retryAfterMatch = /retry-after\s*[:=]\s*([^\s,;]+)/i.exec(body);
|
|
165
|
+
if (retryAfterMatch) {
|
|
166
|
+
const value = retryAfterMatch[1]!;
|
|
167
|
+
const seconds = Number(value);
|
|
168
|
+
if (Number.isFinite(seconds)) {
|
|
169
|
+
considerClamped(seconds * 1000);
|
|
170
|
+
} else {
|
|
171
|
+
const dateMs = Date.parse(value);
|
|
172
|
+
if (!Number.isNaN(dateMs)) considerClamped(dateMs - Date.now());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(body);
|
|
177
|
+
if (resetMsMatch) {
|
|
178
|
+
const resetMs = Number(resetMsMatch[1]);
|
|
179
|
+
if (!Number.isNaN(resetMs)) {
|
|
180
|
+
considerClamped(resetMs > 1_000_000_000_000 ? resetMs - Date.now() : resetMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resetMatch = /x-ratelimit-reset\s*[:=]\s*(\d+)/i.exec(body);
|
|
185
|
+
if (resetMatch) {
|
|
186
|
+
const resetSeconds = Number(resetMatch[1]);
|
|
187
|
+
if (!Number.isNaN(resetSeconds)) {
|
|
188
|
+
considerClamped(resetSeconds > 1_000_000_000 ? resetSeconds * 1000 - Date.now() : resetSeconds * 1000);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return longestMs ?? (retryNow ? 0 : undefined);
|
|
138
192
|
}
|
|
139
193
|
|
|
140
194
|
function unitToMs(unit: string): number | undefined {
|
|
@@ -391,7 +445,10 @@ export function isRetryableStatus(status: number): boolean {
|
|
|
391
445
|
* proxies surface these for any HTTP/2 stream reset.
|
|
392
446
|
*/
|
|
393
447
|
export function isUnexpectedSocketCloseMessage(message: string): boolean {
|
|
394
|
-
return
|
|
448
|
+
return (
|
|
449
|
+
/\b(?:the\s+)?socket connection (?:was )?closed unexpectedly\b/i.test(message) ||
|
|
450
|
+
/^(?:error:\s*)?socket is closed\.?$/i.test(message.trim())
|
|
451
|
+
);
|
|
395
452
|
}
|
|
396
453
|
|
|
397
454
|
const TRANSIENT_MESSAGE_PATTERN =
|
package/src/procmgr.ts
CHANGED
|
@@ -82,6 +82,13 @@ export function isPowerShell(shell: string): boolean {
|
|
|
82
82
|
return basename === "powershell.exe" || basename === "powershell" || basename === "pwsh.exe" || basename === "pwsh";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const POSIX_SHELL_PATTERN = /(?:^|[\\/])(?:sh|bash|dash|ash|ksh|zsh)(?:\.exe)?$/i;
|
|
86
|
+
|
|
87
|
+
/** Whether the executable is a known shell whose command language uses POSIX quoting. */
|
|
88
|
+
export function isPosixShell(shell: string): boolean {
|
|
89
|
+
return POSIX_SHELL_PATTERN.test(shell);
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
/**
|
|
86
93
|
* Get shell prefix for wrapping commands (profilers, strace, etc.).
|
|
87
94
|
*/
|