Skip to content

Commit fee2e04

Browse files
committed
refactor: replace Bun.build with fossilize for Node SEA binaries
- Replace Bun.build({ compile: true }) with fossilize --no-bundle - Switch esbuild output from ESM to CJS (Node SEA requirement) - Add import-meta-url.js shim for CJS format - Target node22 to downlevel 'using' declarations - Embed Ink sidecar via fossilize --assets + node:sea.getAsset() - Update text-import-plugin: file imports return path string (no ESM external) - Drop musl targets (Node doesn't publish musl binaries) - Remove bun:sqlite fallback from sqlite.ts (Node-only now) - Remove setup-bun from CI, add rcodesign for macOS signing - Remove Bun global from biome.jsonc - Suppress SQLite ExperimentalWarning in bin.ts - Add dist-build/ to .gitignore and biome excludes
1 parent 82779e8 commit fee2e04

15 files changed

Lines changed: 561 additions & 364 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,19 @@ jobs:
7575
{
7676
echo 'matrix<<MATRIX_EOF'
7777
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
78-
# PRs build linux-x64 (smoke test + e2e) and linux-x64-musl (Alpine smoke test)
78+
# PRs build linux-x64 (smoke test + e2e)
7979
echo '{"include":[
80-
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true},
81-
{"target":"linux-x64-musl", "os":"ubuntu-latest", "can-test":false}
80+
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true}
8281
]}'
8382
else
84-
# main, release/**, workflow_call: full cross-platform matrix
83+
# main, release/**, workflow_call: full cross-platform matrix.
84+
# All targets cross-compiled from ubuntu-latest via fossilize.
8585
echo '{"include":[
86-
{"target":"darwin-arm64", "os":"macos-latest", "can-test":true},
87-
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true},
88-
{"target":"linux-x64-musl", "os":"ubuntu-latest", "can-test":false},
89-
{"target":"windows-x64", "os":"windows-latest","can-test":true},
90-
{"target":"darwin-x64", "os":"macos-latest", "can-test":false},
91-
{"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false},
92-
{"target":"linux-arm64-musl", "os":"ubuntu-latest", "can-test":false}
86+
{"target":"darwin-arm64", "os":"ubuntu-latest", "can-test":false},
87+
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true},
88+
{"target":"windows-x64", "os":"ubuntu-latest", "can-test":false},
89+
{"target":"darwin-x64", "os":"ubuntu-latest", "can-test":false},
90+
{"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false}
9391
]}'
9492
fi
9593
echo 'MATRIX_EOF'
@@ -246,9 +244,6 @@ jobs:
246244
matrix: ${{ fromJSON(needs.changes.outputs.build-targets) }}
247245
steps:
248246
- uses: actions/checkout@v6
249-
- uses: oven-sh/setup-bun@v2
250-
with:
251-
bun-version: "1.3.13"
252247
- uses: pnpm/action-setup@v4
253248
- uses: actions/setup-node@v6
254249
with:
@@ -262,6 +257,28 @@ jobs:
262257
if: steps.cache.outputs.cache-hit != 'true'
263258
shell: bash
264259
run: pnpm install --frozen-lockfile
260+
- name: Setup codesign dependencies
261+
env:
262+
APPLE_CERT_DATA: ${{ secrets.CSC_LINK }}
263+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
264+
run: |
265+
curl -L 'https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.29.0/apple-codesign-0.29.0-x86_64-unknown-linux-musl.tar.gz' -o 'rcodesign.tar.gz'
266+
echo 'dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002 rcodesign.tar.gz' | sha256sum -c
267+
tar -xzf rcodesign.tar.gz --strip-components=1
268+
mv rcodesign /usr/local/bin/rcodesign
269+
rm rcodesign.tar.gz
270+
if [ -n "$APPLE_CERT_DATA" ]; then
271+
echo "$APPLE_CERT_DATA" | base64 --decode > /tmp/certs.p12
272+
echo 'APPLE_CERT_PATH=/tmp/certs.p12' >> $GITHUB_ENV
273+
fi
274+
if [ -n "$APPLE_API_KEY" ]; then
275+
echo "$APPLE_API_KEY" | base64 -d > /tmp/apple_key.json
276+
cat /tmp/apple_key.json | jq .private_key -r > /tmp/apple_key.pem
277+
echo "APPLE_API_KEY_ISSUER_ID=$(cat /tmp/apple_key.json | jq .issuer_id -r | tr -d '\n\r')" >> $GITHUB_ENV
278+
echo "APPLE_API_KEY_ID=$(cat /tmp/apple_key.json | jq .key_id -r | tr -d '\n\r')" >> $GITHUB_ENV
279+
echo "APPLE_API_KEY_P8_PATH=/tmp/apple_key.pem" >> $GITHUB_ENV
280+
echo 'APPLE_API_KEY_PATH=/tmp/apple_key.json' >> $GITHUB_ENV
281+
fi
265282
- name: Set nightly version
266283
# Inject the nightly version (computed once in the changes job) into
267284
# package.json before the build so it gets baked into the binary.
@@ -278,7 +295,11 @@ jobs:
278295
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
279296
# Set on main/release branches so build.ts runs binpunch + creates .gz
280297
RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }}
281-
run: bun run build --target ${{ matrix.target }}
298+
# Codesigning: only on main/release pushes (fork PRs lack secrets)
299+
FOSSILIZE_SIGN: ${{ github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'release/')) && 'y' || 'n' }}
300+
APPLE_CERT_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
301+
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
302+
run: pnpm run build -- --target ${{ matrix.target }}
282303
- name: Smoke test
283304
if: matrix.can-test
284305
shell: bash
@@ -288,11 +309,6 @@ jobs:
288309
else
289310
./dist-bin/sentry-${{ matrix.target }} --help
290311
fi
291-
- name: Smoke test (musl/Alpine)
292-
if: matrix.target == 'linux-x64-musl'
293-
run: |
294-
docker run --rm -v "$PWD/dist-bin:/dist-bin:ro" alpine:latest \
295-
sh -c "apk add --no-cache libstdc++ libgcc >/dev/null 2>&1 && /dist-bin/sentry-linux-x64-musl --help"
296312
- name: Upload binary artifact
297313
uses: actions/upload-artifact@v7
298314
with:

.github/workflows/docs-preview.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ jobs:
3333
steps:
3434
- uses: actions/checkout@v6
3535

36-
- uses: oven-sh/setup-bun@v2
37-
with:
38-
bun-version: "1.3.13"
3936
- uses: pnpm/action-setup@v4
4037

4138
# Astro 6 requires Node >= 22.12. Pin an explicit version so the docs
@@ -58,7 +55,7 @@ jobs:
5855
run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
5956

6057
- name: Generate docs content
61-
run: bun run generate:schema && bun run generate:docs
58+
run: pnpm run generate:schema && pnpm run generate:docs
6259

6360
- name: Build Docs for Preview
6461
working-directory: docs
@@ -70,8 +67,8 @@ jobs:
7067
SENTRY_RELEASE: ${{ steps.version.outputs.version }}
7168
PUBLIC_SENTRY_RELEASE: ${{ steps.version.outputs.version }}
7269
run: |
73-
bun install --frozen-lockfile
74-
bun run build
70+
pnpm install --frozen-lockfile
71+
pnpm run build
7572
7673
- name: Inject debug IDs and upload sourcemaps
7774
if: env.SENTRY_AUTH_TOKEN != ''
@@ -80,8 +77,8 @@ jobs:
8077
SENTRY_ORG: sentry
8178
SENTRY_PROJECT: cli-website
8279
run: |
83-
bun run --bun src/bin.ts sourcemap inject docs/dist/
84-
bun run --bun src/bin.ts sourcemap upload docs/dist/ \
80+
pnpm run cli sourcemap inject docs/dist/
81+
pnpm run cli sourcemap upload docs/dist/ \
8582
--release "${{ steps.version.outputs.version }}" \
8683
--url-prefix "~/"
8784

.github/workflows/eval-skill-fork.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ jobs:
3636
with:
3737
ref: ${{ github.event.pull_request.head.sha }}
3838

39-
- uses: oven-sh/setup-bun@v2
40-
with:
41-
bun-version: "1.3.13"
4239
- uses: pnpm/action-setup@v4
4340

4441
- uses: actions/cache@v5
@@ -50,11 +47,11 @@ jobs:
5047
run: pnpm install --frozen-lockfile
5148

5249
- name: Generate docs and skill files
53-
run: bun run generate:schema && bun run generate:docs
50+
run: pnpm run generate:schema && pnpm run generate:docs
5451

5552
- name: Eval SKILL.md
5653
id: eval
57-
run: bun run eval:skill
54+
run: pnpm run eval:skill
5855
env:
5956
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
6057
continue-on-error: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package-lock.json
66
out
77
dist
88
dist-bin
9+
dist-build
910
*.tgz
1011

1112
# fossilize build cache

.lore.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<!-- lore:019dacc1-e7a1-761a-acbf-e44c3b6ccedc -->
1414
* **DSN cache invalidation uses two-level mtime tracking (sourceMtimes + dirMtimes)**: DSN cache invalidation — two-level mtime tracking: \`sourceMtimes\` (DSN-bearing files, catches in-place edits) + \`dirMtimes\` (every walked dir, catches new files) + root mtime fast-path + 24h TTL. Dropping either map is a correctness regression. Walker emits mtimes via \`onDirectoryVisit\` hook + \`recordMtimes\` option; DSN scanner uses \`grepFiles({pattern: DSN\_PATTERN, recordMtimes: true, onDirectoryVisit})\`. \`scanCodeForFirstDsn\` stays on direct walker loop (worker init ~20ms dominates). Invariants: \`processMatch\` must record mtime for EVERY file with host-validated DSN via \`fileHadValidDsn\` flag independent of \`seen.has(raw)\`. \`scanDirectory\` catch MUST return empty \`dirMtimes: {}\`, NOT partial map; \`ConfigError\` re-throws.
1515

16+
<!-- lore:019e4cbd-d791-74d9-afcf-6e3d1a65f83d -->
17+
* **Gateway auth: per-session registry + global fallback in auth.ts**: \`packages/gateway/src/auth.ts\`: \`AuthCredential\` (api-key|bearer). Two-level auth lookup: \`sessionAuth\` Map (per-session) → \`lastSeenAuth\` global fallback via \`resolveAuth(sessionID?)\`. \`authFingerprint()\` = SHA-256 truncated to 16 hex chars. \`setSessionAuth\`/\`getSessionAuth\`/\`deleteSessionAuth\` manage session scope; \`setLastSeenAuth\`/\`getLastSeenAuth\` manage global fallback.
18+
1619
<!-- lore:019db20c-d294-7dad-a831-66c66f6fe9b0 -->
1720
* **Grep worker pool: binary-transferable matches + streaming dispatch in src/lib/scan/**: Grep worker pool (\`src/lib/scan/worker-pool.ts\` + \`grep-worker.js\`): lazy singleton, size \`min(8, max(2, availableParallelism()))\`. Matches encoded as \`Uint32Array\` quads \`\[pathIdx, lineNum, lineOffset, lineLength]\` + \`linePool\` string, transferred via \`postMessage(msg, \[ints.buffer])\` (~40% faster than structuredClone). Worker imported via \`with { type: 'text' }\`\`Blob\` + \`URL.createObjectURL\`; \`new Worker(new URL(...))\` HANGS in \`bun build --compile\` binaries. FIFO \`pending\` queue per worker — per-dispatch \`addEventListener\` causes wrong-request resolution. \`ref()\`/\`unref()\` idempotent booleans, NOT refcounted — only unref when \`inflight\` drops to 0; spawn unref'd. Disable via \`SENTRY\_SCAN\_DISABLE\_WORKERS=1\`. Track dispatched/failed batches with \`Promise.allSettled\`; throw if all failed so DSN cache doesn't persist false-negatives.
1821

@@ -53,9 +56,6 @@
5356

5457
### Gotcha
5558

56-
<!-- lore:019e4c36-52d4-7188-b0aa-c0f9d5709531 -->
57-
* **http.createServer async callback — unhandled promise rejections crash test server**: (gotcha) \`http.createServer(async (req, res) => { await ... })\` — Node accepts async callbacks but ignores their return value. If the inner \`await\` throws, it causes an unhandled promise rejection that crashes the test server. Fix: wrap entire async callback body in \`try...catch\` and call \`res.end()\` or \`res.destroy()\` in the catch block. Applies to \`test/mocks/server.ts\` and any test HTTP server using async request handlers.
58-
5959
<!-- lore:019db776-111b-73db-b4ad-b762dfd4808f -->
6060
* **MastraClient has no dispose API — use AbortController for cleanup**: MastraClient has no \`close()\`/\`dispose()\` API — cleanup via \`ClientOptions.abortSignal\` (constructor) or per-prompt \`signal\`. Without explicit abort, fetch keep-alive sockets hold the event loop alive past natural exit. Pattern in \`src/lib/init/wizard-runner.ts\`: create \`AbortController\` per \`runWizard\`, pass \`abortSignal: controller.signal\` to \`new MastraClient(...)\`, abort via \`using \_ = { \[Symbol.dispose]: () => controller.abort() }\`. Custom \`fetch\` wrapper must preserve \`init.signal\` via spread. Tests capture \`ClientOptions\` via \`spyOn(MastraClient.prototype, 'getWorkflow').mockImplementation(function() { capturedOpts.push(this.options); ... })\`.
6161

@@ -66,10 +66,10 @@
6666
* **SQLite transaction() ROLLBACK can throw, discarding original error**: (gotcha) SQLite transaction ROLLBACK error-swallowing trap: In \`src/lib/db/sqlite.ts\`, \`transaction()\` catches errors and runs \`this.db.exec('ROLLBACK')\`. If ROLLBACK itself throws, the original error is lost. Fix: \`const origErr = e; try { this.db.exec('ROLLBACK'); } catch (rbErr) { log.debug(...); } throw origErr;\`
6767

6868
<!-- lore:019e4b91-b3fb-7e62-b607-8d1ec425a6a0 -->
69-
* **Vitest worker pool requires pool:forks + UV\_USE\_IO\_URING=0 on GitHub Actions**: (gotcha) Vitest worker pool + CI issues: (1) On GitHub Actions, io\_uring crashes Node.js workers (exit 134/SIGABRT). Fix: set \`pool: 'forks'\` in \`vitest.config.ts\` AND \`UV\_USE\_IO\_URING=0\` env var in CI — it's a kernel capability issue, not a Node version issue. (2) Tests internally calling \`Bun.spawn\` must be skipped in Vitest Node workers via \`skipIf\`. (3) npm build smoke test uses system Node — \`setup-node\` (with \`node-version: ${{ matrix.node }}\`) must not be deleted from the npm build CI job; smoke test on \`dist/bin.cjs\` rejects Node < 22.15 and fails silently if setup-node is missing. (4) Vitest 4 removed \`test(name, fn, { timeout })\` signature — options must be second arg: \`test(name, { timeout }, fn)\`. Bare numeric timeout \`beforeAll(fn, 60\_000)\` remains valid.
69+
* **Vitest worker pool requires pool:forks + UV\_USE\_IO\_URING=0 on GitHub Actions**: (gotcha) Vitest/CI issues: (1) GitHub Actions io\_uring crashes Node.js workers (exit 134/SIGABRT). Fix: \`pool: 'forks'\` in \`vitest.config.ts\` AND \`UV\_USE\_IO\_URING=0\` in CI. (2) Tests calling \`Bun.spawn\` internally must be skipped in Vitest Node workers via \`skipIf\`. (3) npm build smoke test uses system Node — \`setup-node\` (with \`node-version: ${{ matrix.node }}\`) must not be deleted from npm build CI job; smoke test on \`dist/bin.cjs\` rejects Node < 22.15 and fails silently if setup-node is missing. (4) Vitest 4: options must be second arg: \`test(name, { timeout }, fn)\`; bare numeric timeout \`beforeAll(fn, 60\_000)\` remains valid. (5) \`http.createServer(async (req, res) => {...})\` — unhandled rejections crash test server; wrap body in try/catch. (6) \`dorny/paths-filter\` diffs against base — empty commits produce all-false outputs, silently skipping jobs; make a real file change to trigger CI. (7) \`node:sqlite\` requires \`--experimental-sqlite\` on Node 22 — top-level import crashes before any try/catch. (8) Lazy \`require()\` in test fixtures bypasses Vite's \`.js→.ts\` resolver — use top-level \`import\`.
7070

7171
<!-- lore:019e464f-8fcb-7d16-b7ee-f3b157e1565e -->
72-
* **whichSync must use 'command -v' not 'which' for PATH-restricted lookups**: (gotcha) Bun→Node.js API replacements: \`Bun.which(cmd,{PATH})\` → \`whichSync()\` from \`src/lib/which.ts\` (uses 'command -v'). \`Bun.spawn\` → \`spawn(cmd,args,{stdio:\['pipe','pipe','pipe'],...opts})\`; \`proc.exited\` → \`new Promise(r=>proc.on('close',c=>r(c??1)))\`; stdout via \`proc.stdout.on('data',(d)=>{out+=d;})\`. \*\*CRITICAL: always attach \`proc.on('error',noop)\` — Node crashes on unhandled spawn errors.\*\* \`Bun.spawnSync\` → \`spawnSync\`; \`proc.success\`→\`proc.status===0\`. \`Bun.write\`→\`writeFileSync\`. \`Bun.sleep(ms)\`→\`import {setTimeout as sleepMs} from 'node:timers/promises'\`. \`new Bun.Glob(p).match(i)\`→\`picomatch(p,{dot:true})(i)\`. \`Bun.randomUUIDv7()\`→\`uuidv7()\`. \`Bun.semver.order()\`→\`compare()\` from \`semver\` (guard with \`semverValid(v)\`). \`Bun.file().writer()\`→\`createWriteStream\`. Node version: \`engines.node >=22.15\` (zstd requires 22.15+). CI builds \`\["22","24"]\`; E2E jobs MUST use \`actions/setup-node\` with \`node-version: 22\`. Tests using \`Bun.spawn\` internally must be skipped in Vitest Node workers via \`skipIf\`. PRESERVE intentional Bun usage: \`Bun.build()\` in \`script/build.ts\` for native binary compilation must stay Bun; \`build-binary\` CI job retains \`oven-sh/setup-bun\`; \`script/nod \[truncated — entry too long]
72+
* **whichSync must use 'command -v' not 'which' for PATH-restricted lookups**: (gotcha) Bun→Node.js API replacements: \`Bun.which(cmd,{PATH})\` → \`whichSync()\` from \`src/lib/which.ts\` (uses 'command -v'). \`Bun.spawn\` → \`spawn(cmd,args,{stdio:\['pipe','pipe','pipe'],...opts})\`; \`proc.exited\` → \`new Promise(r=>proc.on('close',c=>r(c??1)))\`; stdout via \`proc.stdout.on('data',(d)=>{out+=d;})\`. \*\*CRITICAL: always attach \`proc.on('error',noop)\` — Node crashes on unhandled spawn errors.\*\* \`Bun.spawnSync\` → \`spawnSync\`; \`proc.success\`→\`proc.status===0\`. \`Bun.write\`→\`writeFileSync\`. \`Bun.sleep(ms)\`→\`import {setTimeout as sleepMs} from 'node:timers/promises'\`. \`new Bun.Glob(p).match(i)\`→\`picomatch(p,{dot:true})(i)\`. \`Bun.randomUUIDv7()\`→\`uuidv7()\`. \`Bun.semver.order()\`→\`compare()\` from \`semver\` (guard \`semverValid(v)\`). \`Bun.file().writer()\`→\`createWriteStream\`. Node: \`engines.node >=22.15\` (zstd). CI matrix \`\["22","24"]\`; E2E jobs MUST use \`actions/setup-node\` with \`node-version: 22\`. Migration phases: (1) switch package manager bun→pnpm, (2) replace Bun APIs with Node.js equivalents, (3) replace \`bun run \<script>\` with \`pnpm run\`/\`tsx file.ts\`, (4) remove \`setup-bun\` from CI. Exception: \`build.ts\` uses \`Bun.build()\` — stays on Bun; \`build-binary\` CI job retains \`oven-sh \[truncated — entry too long]
7373

7474
<!-- lore:019db0c9-9cc7-7352-b1f2-61b34b87b252 -->
7575
* **Whole-buffer matchAll slower than split+test when aggregated over many files**: (gotcha) Grep/scan traps in \`src/lib/scan/\`: (1) Whole-buffer \`regex.exec\` 12× faster per-file but ~1.6× SLOWER over 10k files — early-exit at \`maxResults\` via \`mapFilesConcurrent.onResult\` wins. (2) Literal prefilter is FILE-LEVEL gate (\`indexOf\`→skip); per-line verify breaks cross-newline patterns and Unicode length-changing \`toLowerCase\`. (3) Extractor \`hasTopLevelAlternation\`+\`skipGroup\` must call \`skipCharacterClass\` (PCRE \`\[]abc]\` ≠ JS empty class). (4) Wake-latch race: use latched \`pendingWake\` flag, not \`let notify=null; await new Promise(r=>notify=r)\`. (5) \`mapFilesConcurrent\` filters \`null\` but NOT \`\[]\` — return \`null\` for no-op files. (6) \`collectGlob\`/\`collectGrep\` must NOT forward \`maxResults\` to iterator; drain uncapped, set \`truncated=true\`.
@@ -84,5 +84,5 @@
8484

8585
### Preference
8686

87-
<!-- lore:019e4a9c-430a-74a8-a5e4-8dd98c672cef -->
88-
* **Always wait for Sentry Seer and Cursor BugBot CI jobs before merging and address all unresolved review comments**: (preference) PR/CI discipline: Run adversarial review rounds (security, edge cases, error handling, lint, test coverage), severity-tiered (CRITICAL/MEDIUM/LOW/NON-BLOCKING), explicit MERGE/NO-MERGE verdict. Wait for 'Sentry Seer' and 'Cursor BugBot' CI jobs; address all unresolved comments. \`dorny/paths-filter\` diffs against base — empty commits produce all-false outputs, silently skipping jobs; make a real file change to trigger CI. Lint: fix errors immediately with minimal surgical changes — prefix unused/shadowing vars with \`\_\`, use optional chaining. Re-run lint to confirm exit code 0 before committing. \`node:sqlite\` requires \`--experimental-sqlite\` on Node 22 — top-level import crashes before any try/catch. Always exclude \`build/\` and \`dist/\` from Biome analysis to avoid type-limit warnings. Biome version mismatch: local may pass while CI fails — CI is authoritative; apply \`biome format --write\` on failing file. Lazy \`require()\` in test fixtures bypasses Vite's \`.js→.ts\` resolver — use top-level \`import\` instead.
87+
<!-- lore:019e4cbd-d784-7468-a410-e34b8629df72 -->
88+
* **Always honor Retry-After header when present in LLM adapter**: Always honor Retry-After from server in \`backoffMs()\` in \`packages/gateway/src/llm-adapter.ts\`: if \`retryAfterMs != null\`, return \`Math.min(retryAfterMs, cap)\` where cap is \`RETRY\_AFTER\_CAP\_URGENT\_MS=8\_000\` for urgent or \`RETRY\_AFTER\_CAP\_BACKGROUND\_MS=120\_000\` for background. Never ignore Retry-After in favor of computed backoff. TRANSIENT\_CODES={429,500,502,503,529}; MAX\_RETRIES\_RATE\_LIMIT=3, SERVER=3, URGENT=2. Backoff (no Retry-After): 429 background=60s/120s/180s; urgent=min(1000×2^n,4000); 5xx background=min(1000×2^n,8000). Bearer tokens inject \`billingBlock\` as first system block; \`signBody()\` replaces \`cch=00000\` placeholder with xxHash64. System prompt caching uses \`cache\_control:{type:'ephemeral',ttl:'1h'}\`. \`opts.thinking\` NOT forwarded to bare API calls. Circuit breaker tripped on non-urgent 429s via \`tripCircuitBreaker()\`.

0 commit comments

Comments
 (0)