@oh-my-pi/pi-utils 18.1.20 → 18.1.22
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 +17 -0
- package/dist/types/dirs.d.ts +2 -0
- package/dist/types/fetch-retry.d.ts +2 -1
- package/package.json +2 -2
- package/src/browsers.ts +39 -25
- package/src/dirs.ts +5 -0
- package/src/fetch-retry.ts +38 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.22] - 2026-09-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `extractRetryHint` sleeping hours past the provider's stated wait when a timezone-naive `reset at` timestamp overshoots the relative retry hint: the skewed stamp is now ignored instead of winning longest-wins ([#12070](https://github.com/can1357/oh-my-pi/pull/12070) by [@H4vC](https://github.com/H4vC)).
|
|
10
|
+
|
|
11
|
+
## [18.1.21] - 2026-09-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `getBrowserProfilesDir()` (`~/.omp/browser-profiles`; XDG: `$XDG_STATE_HOME/omp/browser-profiles`) for profiles of Chromium browsers spawned by the browser tool.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Timed out stalled Chrome-for-Testing metadata requests after 30 seconds when looking up download metadata
|
|
20
|
+
- Concurrent browser installations share one download without replacing a running browser, and stalled downloads time out with partial files cleaned up for retry.
|
|
21
|
+
|
|
5
22
|
## [18.1.19] - 2026-09-12
|
|
6
23
|
|
|
7
24
|
### Added
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -178,6 +178,8 @@ export declare function getPythonGatewayDir(): string;
|
|
|
178
178
|
export declare function getPuppeteerDir(): string;
|
|
179
179
|
/** Get the browser relay extension install directory (~/.omp/browser-relay). */
|
|
180
180
|
export declare function getBrowserRelayDir(): string;
|
|
181
|
+
/** Get the profile root for Chromium browsers the browser tool spawns via `app.path` (~/.omp/browser-profiles). */
|
|
182
|
+
export declare function getBrowserProfilesDir(): string;
|
|
181
183
|
/** Get DOCS_RS cache directory () */
|
|
182
184
|
export declare function getDocsRsCacheDir(): string;
|
|
183
185
|
/** Get the auto-QA grievances SQLite database path (~/.omp/autoqa.db; XDG: $XDG_DATA_HOME/omp/autoqa.db). */
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-suggested retry delay extraction. Merges the patterns historically used
|
|
3
|
-
* by the OpenAI Codex and Google Gemini retry helpers.
|
|
4
3
|
*
|
|
5
4
|
* Header sources (checked in order):
|
|
6
5
|
* - `retry-after-ms` (milliseconds)
|
|
@@ -16,6 +15,8 @@
|
|
|
16
15
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
17
16
|
* - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
|
|
18
17
|
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
18
|
+
* (offset-bearing only; a timezone-naive stamp is provider wall clock in
|
|
19
|
+
* an unknown zone and only resolves when no relative signal is present)
|
|
19
20
|
*
|
|
20
21
|
* Returns `undefined` if no signal is found, or `0` when the provider
|
|
21
22
|
* explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
|
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.22",
|
|
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.22"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "^1.3.14"
|
package/src/browsers.ts
CHANGED
|
@@ -5,9 +5,13 @@ import * as fsp from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { type ArchiveLimits, extractArchive } from "./ar";
|
|
8
|
+
import { withFileLock } from "./file-lock";
|
|
8
9
|
|
|
9
10
|
const CHROME_FOR_TESTING_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public";
|
|
10
11
|
const CHROME_METADATA_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing";
|
|
12
|
+
// Installation is outside browser/Eval operation deadlines, but a stalled CDN
|
|
13
|
+
// must still release the cached install promise and permit a later attempt.
|
|
14
|
+
const DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Archive ceilings for the managed browser download. The default archive
|
|
@@ -251,29 +255,39 @@ export async function install(options: InstallOptions): Promise<InstalledBrowser
|
|
|
251
255
|
return { browser: options.browser, buildId: options.buildId, platform, path: installPath, executablePath };
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
await fsp.mkdir(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
258
|
+
await fsp.mkdir(path.dirname(installPath), { recursive: true });
|
|
259
|
+
return withFileLock(
|
|
260
|
+
`${installPath}.install`,
|
|
261
|
+
async () => {
|
|
262
|
+
// The in-process launch promise cannot serialize separate OMP sessions.
|
|
263
|
+
// Recheck under the OS lock: never replace a winner's running Chrome.
|
|
264
|
+
if (!(await pathExists(executablePath))) {
|
|
265
|
+
const nonce = `${process.pid}-${crypto.randomUUID()}`;
|
|
266
|
+
const archivePath = path.join(options.cacheDir, `.browser-${nonce}.zip`);
|
|
267
|
+
const stagingPath = path.join(options.cacheDir, `.browser-${nonce}`);
|
|
268
|
+
try {
|
|
269
|
+
await downloadArchive(
|
|
270
|
+
getDownloadUrl(options.browser, platform, options.buildId, options.baseUrl),
|
|
271
|
+
archivePath,
|
|
272
|
+
options.downloadProgressCallback,
|
|
273
|
+
);
|
|
274
|
+
await extractArchive(archivePath, stagingPath, { limits: BROWSER_ARCHIVE_LIMITS });
|
|
275
|
+
if (!(await pathExists(path.join(stagingPath, path.relative(installPath, executablePath))))) {
|
|
276
|
+
throw new Error(`Browser archive did not contain its expected executable: ${executablePath}`);
|
|
277
|
+
}
|
|
278
|
+
await fsp.rm(installPath, { recursive: true, force: true });
|
|
279
|
+
await fsp.rename(stagingPath, installPath);
|
|
280
|
+
} finally {
|
|
281
|
+
await Promise.all([
|
|
282
|
+
fsp.rm(archivePath, { force: true }).catch(() => {}),
|
|
283
|
+
fsp.rm(stagingPath, { recursive: true, force: true }).catch(() => {}),
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { browser: options.browser, buildId: options.buildId, platform, path: installPath, executablePath };
|
|
288
|
+
},
|
|
289
|
+
{ retries: Math.ceil(DOWNLOAD_TIMEOUT_MS / 100) + 1, retryDelayMs: 100 },
|
|
290
|
+
);
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
function chromeChannelName(tag: string): string | undefined {
|
|
@@ -292,7 +306,7 @@ function chromeChannelName(tag: string): string | undefined {
|
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
async function fetchMetadata<T>(filename: string): Promise<T> {
|
|
295
|
-
const response = await fetch(`${CHROME_METADATA_BASE_URL}/${filename}
|
|
309
|
+
const response = await fetch(`${CHROME_METADATA_BASE_URL}/${filename}`, { signal: AbortSignal.timeout(30_000) });
|
|
296
310
|
if (!response.ok)
|
|
297
311
|
throw new Error(`Failed to fetch Chrome-for-Testing metadata (${response.status} ${response.statusText})`);
|
|
298
312
|
return (await response.json()) as T;
|
|
@@ -345,7 +359,7 @@ async function downloadArchive(
|
|
|
345
359
|
destination: string,
|
|
346
360
|
onProgress: ((progress: BrowserDownloadProgress) => void) | undefined,
|
|
347
361
|
): Promise<void> {
|
|
348
|
-
const response = await fetch(url);
|
|
362
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
349
363
|
if (!response.ok || !response.body) {
|
|
350
364
|
throw new Error(`Browser download failed (${response.status} ${response.statusText}) from ${url}`);
|
|
351
365
|
}
|
package/src/dirs.ts
CHANGED
|
@@ -708,6 +708,11 @@ export function getBrowserRelayDir(): string {
|
|
|
708
708
|
return dirs.rootSubdir("browser-relay", "data");
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
/** Get the profile root for Chromium browsers the browser tool spawns via `app.path` (~/.omp/browser-profiles). */
|
|
712
|
+
export function getBrowserProfilesDir(): string {
|
|
713
|
+
return dirs.rootSubdir("browser-profiles", "state");
|
|
714
|
+
}
|
|
715
|
+
|
|
711
716
|
/** Get DOCS_RS cache directory () */
|
|
712
717
|
export function getDocsRsCacheDir(): string {
|
|
713
718
|
return dirs.rootSubdir("webcache", "cache");
|
package/src/fetch-retry.ts
CHANGED
|
@@ -19,9 +19,18 @@ const CN_RESET_AT_PATTERN = /将在\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-
|
|
|
19
19
|
// "retry-after-ms=98497000" / "retry-after-ms: 7200000" / "retry-after-ms = 7200000"
|
|
20
20
|
const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
|
|
21
21
|
|
|
22
|
+
// A timezone-naive `reset at` stamp (no `Z`/offset) is the provider's
|
|
23
|
+
// wall clock in an unknown zone: it cannot be converted to a delay without
|
|
24
|
+
// guessing the zone, so it resolves only as a fallback when the body
|
|
25
|
+
// carries no unambiguous relative signal.
|
|
26
|
+
|
|
27
|
+
// A conflict probe needs no new signal: when a naive `reset at` wall stamp
|
|
28
|
+
// disagrees with the merged relative wait, the merged wait (which already
|
|
29
|
+
// ignores the naive stamp) sleeps first, and the retry after it is the
|
|
30
|
+
// probe — success proves skew, a fresh 429 re-anchors with live timing.
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Server-suggested retry delay extraction. Merges the patterns historically used
|
|
24
|
-
* by the OpenAI Codex and Google Gemini retry helpers.
|
|
25
34
|
*
|
|
26
35
|
* Header sources (checked in order):
|
|
27
36
|
* - `retry-after-ms` (milliseconds)
|
|
@@ -37,6 +46,8 @@ const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
|
|
|
37
46
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
38
47
|
* - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
|
|
39
48
|
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
49
|
+
* (offset-bearing only; a timezone-naive stamp is provider wall clock in
|
|
50
|
+
* an unknown zone and only resolves when no relative signal is present)
|
|
40
51
|
*
|
|
41
52
|
* Returns `undefined` if no signal is found, or `0` when the provider
|
|
42
53
|
* explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
|
|
@@ -98,6 +109,15 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
98
109
|
// when the parse returns undefined, which would sleep a session the
|
|
99
110
|
// provider told to retry immediately.
|
|
100
111
|
let retryNow = false;
|
|
112
|
+
|
|
113
|
+
// Timezone-naive `reset at` stamps (no `Z`/offset) are the provider's
|
|
114
|
+
// wall clock in an unknown zone — converting them to a delay requires
|
|
115
|
+
// guessing the zone. They resolve AFTER every unambiguous signal below,
|
|
116
|
+
// and only as a fallback when none was found.
|
|
117
|
+
let longestNaiveMs: number | undefined;
|
|
118
|
+
const considerNaive = (ms: number | undefined): void => {
|
|
119
|
+
if (ms !== undefined && ms > 0 && (longestNaiveMs === undefined || ms > longestNaiveMs)) longestNaiveMs = ms;
|
|
120
|
+
};
|
|
101
121
|
const consider = (ms: number | undefined): void => {
|
|
102
122
|
if (ms !== undefined && ms > 0 && (longestMs === undefined || ms > longestMs)) longestMs = ms;
|
|
103
123
|
};
|
|
@@ -119,14 +139,23 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
119
139
|
}
|
|
120
140
|
for (const pattern of [WILL_RESET_AT_PATTERN, CN_RESET_AT_PATTERN]) {
|
|
121
141
|
const match = pattern.exec(body);
|
|
122
|
-
if (match?.[1])
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
if (!match?.[1]) continue;
|
|
143
|
+
// Offset-bearing stamps are unambiguous and compete by longest-wins.
|
|
144
|
+
// Naive stamps (provider wall clock, unknown zone) resolve after the
|
|
145
|
+
// relative signals below, and only when nothing unambiguous was
|
|
146
|
+
// found — never by guessing the zone against a conflicting signal.
|
|
147
|
+
const normalized = match[1].replace(" ", "T");
|
|
148
|
+
const hasOffset = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized);
|
|
149
|
+
if (hasOffset) {
|
|
150
|
+
const parsed = Date.parse(normalized);
|
|
127
151
|
if (!Number.isNaN(parsed) && parsed > Date.now()) {
|
|
128
152
|
consider(parsed - Date.now());
|
|
129
153
|
}
|
|
154
|
+
} else {
|
|
155
|
+
const parsed = Date.parse(`${normalized}Z`);
|
|
156
|
+
if (!Number.isNaN(parsed) && parsed > Date.now()) {
|
|
157
|
+
considerNaive(parsed - Date.now());
|
|
158
|
+
}
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
161
|
const accountResetMatch = WILL_RESET_IN_PATTERN.exec(body);
|
|
@@ -188,7 +217,9 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
188
217
|
considerClamped(resetSeconds > 1_000_000_000 ? resetSeconds * 1000 - Date.now() : resetSeconds * 1000);
|
|
189
218
|
}
|
|
190
219
|
}
|
|
191
|
-
|
|
220
|
+
// Elapsed naive stamps were ignored before this change and stay ignored:
|
|
221
|
+
// only an explicit zero/expired relative signal is authoritative retry-now.
|
|
222
|
+
return longestMs ?? longestNaiveMs ?? (retryNow ? 0 : undefined);
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
function unitToMs(unit: string): number | undefined {
|