@linxiraos/pi-utils 1.1.11 → 1.1.13

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 CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.11] - 2026-09-08
5
+ ## [1.1.13] - 2026-09-10
6
6
 
7
- ### Fixed
7
+ - 上游 v18.1.16 同步:retry 提示解析升级为 longest-wins 合并(账号重置 + 追加 retry-after 同时出现时取更长窗口);`retry-after-ms` 支持 `:`/` =` 分隔形式;provider 明确要求立即重试(=0 或已过期时间戳)时返回 0 而非 undefined,避免误用启发式退避。
8
+
9
+ ## [1.1.12] - 2026-09-10
8
10
 
9
- - Fixed `filterChildShellEnv` applying the Zeta process's own launch-environment provenance (the pre-dotenv `NODE_ENV` and launcher-owned names read from `/proc/self/environ`) to caller-supplied environment objects; launch provenance now only applies when filtering the live `process.env`/`Bun.env`, and an explicit env resolves its dotenv mode from its own `NODE_ENV`.
11
+ - worker 宿主选择器族统一为 `__zeta_worker_*` 前缀(与主 CLI 对齐)。
10
12
 
11
13
  ## [1.1.10] - 2026-09-07
12
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @linxiraos/pi-utils
2
2
 
3
- Shared utilities for [oh-my-pi](https://github.com/can1357/oh-my-pi) packages. Zero ceremony, Bun-first.
3
+ Shared utilities for Zeta packages. Zero ceremony, Bun-first.
4
4
 
5
5
  ## Notable modules
6
6
 
@@ -33,5 +33,4 @@ Ships TypeScript source directly (no build step); requires Bun ≥ 1.3.14.
33
33
 
34
34
  ## References
35
35
 
36
- - [Monorepo README](https://github.com/can1357/oh-my-pi#readme)
37
36
  - [CHANGELOG](./CHANGELOG.md)
@@ -119,7 +119,7 @@ export declare function getProjectAgentDir(cwd?: string): string;
119
119
  export declare function getReportsDir(): string;
120
120
  /** Get the logs directory (~/.zeta/logs). */
121
121
  export declare function getLogsDir(): string;
122
- /** Get this process's dated log path (~/.zeta/logs/omp.YYYY-MM-DD.PID.log). */
122
+ /** Get this process's dated log path (~/.zeta/logs/zeta.YYYY-MM-DD.PID.log). */
123
123
  export declare function getLogPath(date?: Date, pid?: number): string;
124
124
  /**
125
125
  * Get the plugins directory (~/.zeta/plugins or its XDG equivalent).
@@ -263,9 +263,9 @@ export declare function getAgentModulesDir(agentDir?: string): string;
263
263
  export declare function getMemoriesDir(agentDir?: string): string;
264
264
  /** Get the terminal sessions directory (~/.zeta/agent/terminal-sessions). */
265
265
  export declare function getTerminalSessionsDir(agentDir?: string): string;
266
- /** Get the crash log path (~/.zeta/agent/omp-crash.log). */
266
+ /** Get the crash log path (~/.zeta/agent/zeta-crash.log). */
267
267
  export declare function getCrashLogPath(agentDir?: string): string;
268
- /** Get the debug log path (~/.zeta/agent/omp-debug.log). */
268
+ /** Get the debug log path (~/.zeta/agent/zeta-debug.log). */
269
269
  export declare function getDebugLogPath(agentDir?: string): string;
270
270
  /** Get the secret placeholder key path (~/.zeta/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/zeta/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
271
271
  export declare function getSecretPlaceholderKeyPath(): string;
@@ -295,7 +295,7 @@ export declare function getMCPConfigPath(scope: "user" | "project", cwd?: string
295
295
  export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string): string;
296
296
  /**
297
297
  * Application label for usage attribution (`OMP_APP_NAME`), defaulting to
298
- * `omp`. Embedders that drive omp programmatically (robomp, CI bots, …) set
298
+ * `zeta`. Embedders that drive zeta programmatically (robomp, CI bots, …) set
299
299
  * the env var so broker-side per-client burn tracking can answer "what did
300
300
  * app X use" instead of folding everything into one install-wide bucket.
301
301
  */
@@ -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 {
@@ -26,7 +26,8 @@ export interface WhichOptions extends Bun.WhichOptions {
26
26
  */
27
27
  cache?: WhichCachePolicy;
28
28
  }
29
- export declare const whichFresh: typeof Bun.which;
29
+ declare function darwinWhich(command: string, options?: Bun.WhichOptions): string | null;
30
+ export declare const whichFresh: typeof darwinWhich;
30
31
  /**
31
32
  * Locate binary on PATH (with flexible caching).
32
33
  *
@@ -35,3 +36,4 @@ export declare const whichFresh: typeof Bun.which;
35
36
  * @returns Filesystem path if found, else null
36
37
  */
37
38
  export declare function $which(command: string, options?: WhichOptions): string | null;
39
+ export {};
@@ -1,5 +1,5 @@
1
1
  /** Prefix reserved for argv selectors dispatched by the shared CLI worker host. */
2
- export declare const WORKER_HOST_SELECTOR_PREFIX = "__omp_worker_";
2
+ export declare const WORKER_HOST_SELECTOR_PREFIX = "__zeta_worker_";
3
3
  /** Whether an argv value selects a worker hosted by the shared CLI entrypoint. */
4
4
  export declare function isWorkerHostSelector(value: string | undefined): value is string;
5
5
  /** Called by CLI entrypoints whose main module dispatches worker argv selectors. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-utils",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "utilities",
@@ -46,12 +46,15 @@
46
46
  "./*.js": "./src/*.ts"
47
47
  },
48
48
  "scripts": {
49
- "check": "tsgo -p tsconfig.json --noEmit",
49
+ "check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
50
50
  "check:types": "tsgo -p tsconfig.json --noEmit",
51
- "test": "bun test --parallel"
51
+ "lint": "oxlint .",
52
+ "test": "bun test --parallel",
53
+ "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
54
+ "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
52
55
  },
53
56
  "dependencies": {
54
- "@linxiraos/pi-natives": "1.1.11"
57
+ "@linxiraos/pi-natives": "1.1.13"
55
58
  },
56
59
  "devDependencies": {
57
60
  "@types/bun": "^1.3.14"
package/src/ar/entries.ts CHANGED
@@ -73,7 +73,7 @@ export function resolveArchiveLinkPath(
73
73
  ): string {
74
74
  let resolvedPath = archivePath;
75
75
  const seen = new Set<string>();
76
- for (let rewrites = 0; !seen.has(resolvedPath); ) {
76
+ for (let rewrites = 0; !seen.has(resolvedPath);) {
77
77
  seen.add(resolvedPath);
78
78
  let replacement: string | undefined;
79
79
  for (let end = resolvedPath.length; end > 0; end = resolvedPath.lastIndexOf("/", end - 1)) {
package/src/ar/iso.ts CHANGED
@@ -324,7 +324,7 @@ function decodeIdentifier(identifier: Uint8Array, joliet: boolean): string {
324
324
 
325
325
  function findSuspSkip(record: IsoRecord): number | undefined {
326
326
  const bytes = record.systemUse;
327
- for (let offset = 0; offset + 7 <= bytes.byteLength; ) {
327
+ for (let offset = 0; offset + 7 <= bytes.byteLength;) {
328
328
  const length = bytes[offset + 2]!;
329
329
  if (length < 4 || offset + length > bytes.byteLength) return undefined;
330
330
  if (
@@ -359,7 +359,7 @@ async function parseSusp(
359
359
  let relocation: SuspData["relocation"];
360
360
  for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
361
361
  const area = queue[queueIndex]!;
362
- for (let offset = 0; offset + 4 <= area.byteLength; ) {
362
+ for (let offset = 0; offset + 4 <= area.byteLength;) {
363
363
  const signature = String.fromCharCode(area[offset]!, area[offset + 1]!);
364
364
  const length = area[offset + 2]!;
365
365
  const version = area[offset + 3]!;
@@ -466,7 +466,7 @@ async function readDirectoryRecords(
466
466
  const end = safeAdd(start, record.size, "directory extent range");
467
467
  const bytes = await budget.read(source, start, end, "directory extent");
468
468
  const records: IsoRecord[] = [];
469
- for (let offset = 0; offset < bytes.byteLength; ) {
469
+ for (let offset = 0; offset < bytes.byteLength;) {
470
470
  const sectorRemaining = blockSize - (offset % blockSize);
471
471
  const length = bytes[offset]!;
472
472
  if (length === 0) {
@@ -586,7 +586,7 @@ async function readIsoImpl(source: ByteSource, options: Parameters<FormatReader>
586
586
  ) {
587
587
  suspSkip = findSuspSkip(records[0]!);
588
588
  }
589
- for (let index = 0; index < records.length; ) {
589
+ for (let index = 0; index < records.length;) {
590
590
  const record = records[index]!;
591
591
  if (record.identifier.byteLength === 1 && (record.identifier[0] === 0 || record.identifier[0] === 1)) {
592
592
  index++;
@@ -45,7 +45,7 @@ class Bits {
45
45
  read(count: number): number {
46
46
  if (count === 0) return 0;
47
47
  let value = 0;
48
- for (let left = count; left > 0; ) {
48
+ for (let left = count; left > 0;) {
49
49
  if (this.pos >= this.bytes.byteLength - 4) corrupt("truncated compressed data");
50
50
  const take = Math.min(left, 8 - this.bit);
51
51
  value = value * 2 ** take + ((this.bytes[this.pos]! >>> (8 - this.bit - take)) & (2 ** take - 1));
@@ -340,7 +340,7 @@ class VmBits {
340
340
 
341
341
  read(count: number): number {
342
342
  let value = 0;
343
- for (let left = count; left > 0; ) {
343
+ for (let left = count; left > 0;) {
344
344
  const bytePosition = this.#position >>> 3;
345
345
  if (bytePosition >= this.#bytes.byteLength) corrupt("truncated RarVM code");
346
346
  const bitPosition = this.#position & 7;
@@ -393,7 +393,7 @@ function applyFilters(output: Uint8Array, filters: PendingFilter[]): void {
393
393
  continue;
394
394
  }
395
395
  const compareOpcode = filter.type === 2 ? 0xe9 : 0xe8;
396
- for (let position = 0; position + 4 < data.byteLength; ) {
396
+ for (let position = 0; position + 4 < data.byteLength;) {
397
397
  const opcode = data[position++]!;
398
398
  if (opcode !== 0xe8 && opcode !== compareOpcode) continue;
399
399
  const offset = position + (filter.fileOffset >>> 0);
@@ -437,7 +437,7 @@ function readLengthTable(bits: Bits, previous: Uint8Array, size: number): Uint8A
437
437
  const preTable = new Huffman(20);
438
438
  preTable.build(preLengths, 0, 20);
439
439
  const lengths = new Uint8Array(size);
440
- for (let position = 0; position < size; ) {
440
+ for (let position = 0; position < size;) {
441
441
  const symbol = preTable.decode(bits);
442
442
  if (symbol < 16) {
443
443
  lengths[position] = (symbol + previous[position]!) & 15;
@@ -33,7 +33,7 @@ class Bits {
33
33
  if (n === 0) return 0;
34
34
  if (n < 0 || n > 31) fail("invalid bit count");
35
35
  let value = 0;
36
- for (let left = n; left > 0; ) {
36
+ for (let left = n; left > 0;) {
37
37
  if (this.pos >= this.bytes.byteLength - 16) fail("truncated compressed data");
38
38
  const take = Math.min(left, 8 - this.bit);
39
39
  value = value * 2 ** take + ((this.bytes[this.pos]! >>> (8 - this.bit - take)) & (2 ** take - 1));
@@ -211,7 +211,7 @@ export class Rar5Decoder {
211
211
  if ((flags & 0x80) === 0) return;
212
212
 
213
213
  const levelLengths = new Uint8Array(20);
214
- for (let i = 0; i < 20; ) {
214
+ for (let i = 0; i < 20;) {
215
215
  const length = bits.read(4);
216
216
  if (length === 15) {
217
217
  const zeros = bits.read(4);
@@ -226,7 +226,7 @@ export class Rar5Decoder {
226
226
  level.build(levelLengths);
227
227
  const tableLength = version === 1 ? TABLE_SIZE : TABLE_SIZE - 16;
228
228
  const lengthsCompact = new Uint8Array(tableLength);
229
- for (let i = 0; i < tableLength; ) {
229
+ for (let i = 0; i < tableLength;) {
230
230
  const symbol = level.decode(bits);
231
231
  if (symbol < 16) {
232
232
  lengthsCompact[i++] = symbol;
@@ -358,7 +358,7 @@ function applyFilters(output: Uint8Array, filters: Filter[]): void {
358
358
  }
359
359
  } else if (filter.type === 1 || filter.type === 2) {
360
360
  const fileSize = 1 << 24;
361
- for (let pos = 0; pos + 4 < data.byteLength; ) {
361
+ for (let pos = 0; pos + 4 < data.byteLength;) {
362
362
  const opcode = data[pos++]!;
363
363
  if (opcode !== 0xe8 && (filter.type === 1 || opcode !== 0xe9)) continue;
364
364
  const offset = (filter.start + pos) & (fileSize - 1);
package/src/ar/unix-ar.ts CHANGED
@@ -197,7 +197,7 @@ export function readUnixArEntriesFromBuffer(bytes: Uint8Array, options: FormatRe
197
197
  const records: RawArMember[] = [];
198
198
  let longNames: Uint8Array | undefined;
199
199
  let metadataSize = 0;
200
- for (let position = SIGNATURE.length; position < bytes.byteLength; ) {
200
+ for (let position = SIGNATURE.length; position < bytes.byteLength;) {
201
201
  if (bytes.byteLength - position < HEADER_SIZE)
202
202
  throw new ArchiveError("Invalid ar archive: truncated member header");
203
203
  const header = parseHeader(readMemoryRange(bytes, position, position + HEADER_SIZE));
@@ -255,7 +255,7 @@ async function readUnixArImpl(source: ByteSource, options: FormatReadOptions): P
255
255
  const records: RawArMember[] = [];
256
256
  let longNames: Uint8Array | undefined;
257
257
  let metadataSize = 0;
258
- for (let position = SIGNATURE.length; position < source.size; ) {
258
+ for (let position = SIGNATURE.length; position < source.size;) {
259
259
  const headerBytes = await readExact(source, position, position + HEADER_SIZE, "member header");
260
260
  const header = parseHeader(headerBytes);
261
261
  metadataSize += HEADER_SIZE;
package/src/dates.ts CHANGED
@@ -100,7 +100,7 @@ export function format(value: Date | number, pattern: string): string {
100
100
  const date = asDate(value);
101
101
  let result = "";
102
102
 
103
- for (let index = 0; index < pattern.length; ) {
103
+ for (let index = 0; index < pattern.length;) {
104
104
  if (pattern[index] === "'") {
105
105
  if (pattern[index + 1] === "'") {
106
106
  result += "'";
package/src/dirs.ts CHANGED
@@ -568,7 +568,7 @@ export function getLogsDir(): string {
568
568
  return dirs.rootSubdir("logs", "state");
569
569
  }
570
570
 
571
- /** Get this process's dated log path (~/.zeta/logs/omp.YYYY-MM-DD.PID.log). */
571
+ /** Get this process's dated log path (~/.zeta/logs/zeta.YYYY-MM-DD.PID.log). */
572
572
  export function getLogPath(date = new Date(), pid = process.pid): string {
573
573
  return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.${pid}.log`);
574
574
  }
@@ -893,12 +893,12 @@ export function getTerminalSessionsDir(agentDir?: string): string {
893
893
  return dirs.agentSubdir(agentDir, "terminal-sessions", "state");
894
894
  }
895
895
 
896
- /** Get the crash log path (~/.zeta/agent/omp-crash.log). */
896
+ /** Get the crash log path (~/.zeta/agent/zeta-crash.log). */
897
897
  export function getCrashLogPath(agentDir?: string): string {
898
- return dirs.agentSubdir(agentDir, "omp-crash.log", "state");
898
+ return dirs.agentSubdir(agentDir, "zeta-crash.log", "state");
899
899
  }
900
900
 
901
- /** Get the debug log path (~/.zeta/agent/omp-debug.log). */
901
+ /** Get the debug log path (~/.zeta/agent/zeta-debug.log). */
902
902
  export function getDebugLogPath(agentDir?: string): string {
903
903
  return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
904
904
  }
@@ -1018,13 +1018,13 @@ let cachedInstallId: string | null = null;
1018
1018
  const INSTALL_ID_FILE = "install-id";
1019
1019
  /**
1020
1020
  * Application label for usage attribution (`OMP_APP_NAME`), defaulting to
1021
- * `omp`. Embedders that drive omp programmatically (robomp, CI bots, …) set
1021
+ * `zeta`. Embedders that drive zeta programmatically (robomp, CI bots, …) set
1022
1022
  * the env var so broker-side per-client burn tracking can answer "what did
1023
1023
  * app X use" instead of folding everything into one install-wide bucket.
1024
1024
  */
1025
1025
  export function getAppName(): string {
1026
1026
  const value = process.env.OMP_APP_NAME?.trim();
1027
- return value ? value : "omp";
1027
+ return value ? value : "zeta";
1028
1028
  }
1029
1029
 
1030
1030
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -530,7 +530,7 @@ function renderList(
530
530
 
531
531
  function renderBlocks(blocks: readonly Block[]): string {
532
532
  let html = "";
533
- for (let index = 0; index < blocks.length; ) {
533
+ for (let index = 0; index < blocks.length;) {
534
534
  const block = blocks[index];
535
535
  if (block.kind === "html") {
536
536
  html += block.html;
@@ -549,7 +549,7 @@ function renderBlocks(blocks: readonly Block[]): string {
549
549
  listItems.push(candidate);
550
550
  index++;
551
551
  }
552
- for (let itemIndex = 0; itemIndex < listItems.length; ) {
552
+ for (let itemIndex = 0; itemIndex < listItems.length;) {
553
553
  const rendered = renderList(listItems, itemIndex, listItems[itemIndex].list?.level ?? 0);
554
554
  html += rendered.html;
555
555
  itemIndex = rendered.next;
@@ -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=([0-9]+)\b/i;
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
- if (totalMs > 0) return totalMs;
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
- return parsed - Date.now();
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) return value * unitMs;
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) && ms > 0) return 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) return value * unitMs;
153
+ if (unitMs !== undefined) consider(value * unitMs);
134
154
  }
135
155
  }
136
156
  }
137
- return undefined;
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 /\b(?:the\s+)?socket connection (?:was )?closed unexpectedly\b/i.test(message);
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/which.ts CHANGED
@@ -192,8 +192,13 @@ function darwinWhich(command: string, options?: Bun.WhichOptions): string | null
192
192
  return null;
193
193
  }
194
194
 
195
- // Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'
196
- export const whichFresh = os.platform() === "darwin" ? darwinWhich : Bun.which;
195
+ // Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'.
196
+ // Look `Bun.which` up per call rather than capturing it at import, so a `Bun.which`
197
+ // stub installed later (the per-test seam) is honoured on every platform.
198
+ export const whichFresh =
199
+ os.platform() === "darwin"
200
+ ? darwinWhich
201
+ : (command: string, options?: Bun.WhichOptions): string | null => Bun.which(command, options);
197
202
 
198
203
  // Derive stable cache key from command and lookup options
199
204
  function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
@@ -1,7 +1,7 @@
1
1
  import { stripWindowsExtendedLengthPathPrefix } from "./path";
2
2
 
3
3
  /** Prefix reserved for argv selectors dispatched by the shared CLI worker host. */
4
- export const WORKER_HOST_SELECTOR_PREFIX = "__omp_worker_";
4
+ export const WORKER_HOST_SELECTOR_PREFIX = "__zeta_worker_";
5
5
 
6
6
  /** Whether an argv value selects a worker hosted by the shared CLI entrypoint. */
7
7
  export function isWorkerHostSelector(value: string | undefined): value is string {
@@ -10,7 +10,7 @@ export function isWorkerHostSelector(value: string | undefined): value is string
10
10
 
11
11
  /**
12
12
  * Main-module path declared by self-dispatching CLI entrypoints — entries
13
- * whose top-level argv handling routes hidden `__omp_*` worker selectors.
13
+ * whose top-level argv handling routes hidden `__zeta_**` worker selectors.
14
14
  * Worker spawn sites re-enter this module via `new Worker(entry, { argv })`,
15
15
  * so every distribution (source, npm bundle, compiled binary) needs exactly
16
16
  * one JavaScript entrypoint. Never set under `bun test`, SDK embedding, or