Skip to content

fix(codex): stop retrying a doomed pool credential refresh, and say it was local (#4546) - #4639

Merged
lidge-jun merged 3 commits into
devfrom
codex/4546-wph-pool-refresh-join
Sep 14, 2026
Merged

lidge-jun merged 3 commits into
devfrom
codex/4546-wph-pool-refresh-join

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

A Codex pool account whose forced credential refresh fails now backs off, loses selection preference, and says on the record that the refusal was local. Before this, one unhealthy account could pin the whole pool at 503 indefinitely while healthy siblings sat idle.

Reproduced live. Every request routed to one pool account returned 503 with code server_is_overloaded — five of five sequential probes, two different models, ~600ms each. The stored record was healthy: present, no deletedAt, refresh token present, grant fingerprint present, expiry a week out. Not one line reached the service log. Switching the active account made it stop on the very next request.

The refusal was this proxy's own poolCredentialRefreshIncompleteResponse, and three things made it permanent. isTerminalPoolRefreshFailure admitted only TokenRefreshError with reason revoked or expired, so a missing record, a missing grant fingerprint, a token-endpoint 5xx and a generation CAS loss all became a retryable 503 whose body asks the client to retry — a loop that sustains the condition it waits out. Selection kept returning to the account because it still read healthy. And nothing distinguished this locally synthesized refusal from a genuine upstream overload.

src/codex/pool-refresh-backoff.ts adds a per-account cooldown. Consecutive non-terminal failures open a bounded growing window (2s through 60s); inside it no new forced refresh starts and isCodexPoolRefreshCooling removes the account from every selection path, while the binding is kept and the account is not quarantined — a token-endpoint 5xx must not retire a healthy account (#2887). A failure inside an open window does not grow it, so a burst of concurrent requests cannot race one account to the ceiling. The first successful refresh clears everything.

CodexCredentialUnavailableError replaces the bare Error for a missing record or fingerprint and is classified terminal, so the operator is told to sign in again instead of being asked to retry something that can never succeed. isTerminalPoolRefreshFailure now delegates to one definition.

The 503 wire contract is deliberately unchanged — its status and exact wording are what make the client apply retry-after backoff, and the word "reauthentication" would flip the classification. What changes is the record: the entry carries terminalSource: "synthetic" and a distinct localTerminalReason, because the dashboard renders this sentence under a field named "Upstream reason", which is what sent an experienced maintainer to the provider's status page for a fault that never left his own process.

Stacked on #4638.

Verification

Not run, by explicit instruction: the local suite, bun run typecheck, bun install, and any build. The only proof for this unit is hosted CI at the exact final head SHA; this push used --no-verify.

New coverage: tests/codex-integration/codex-pool-refresh-backoff.test.ts pins the growing bounded window, that a failure inside an open window does not grow it, the ceiling, automatic clearing on success, that one cooling account never cools a sibling, that the cooldown error is retryable and avoids the word that would flip its classification, and that a missing credential is terminal while a token-endpoint 5xx and a CAS loss stay transient. Registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json.

Known open: the compact path's refusal at compact.ts does not yet receive a logCtx, so its refusals are not marked synthesized. The cooldown steps are constants rather than operator configuration.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Reliability

    • Added automatic cooldowns after repeated temporary Codex credential refresh failures, with bounded retry delays.
    • Accounts in cooldown are temporarily skipped, allowing eligible accounts or fallback routing to handle requests.
    • Successful refreshes clear the cooldown, and account removal clears related retry state.
  • Error Handling

    • Improved distinction between temporary refresh failures and unavailable or permanently invalid credentials.
    • Locally generated refresh refusals are now clearly identified in request logs.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 15:21
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: ecc1dacc-4b1c-4e6b-80c5-fbb9ba620b8d

📥 Commits

Reviewing files that changed from the base of the PR and between a223a25 and 6966470.

📒 Files selected for processing (9)
  • scripts/test-layout/layout.json
  • src/codex/account-lifecycle.ts
  • src/codex/account-store.ts
  • src/codex/pool-refresh-backoff.ts
  • src/codex/routing.ts
  • src/server/request-log.ts
  • src/server/responses/core.ts
  • tests/codex-integration/codex-pool-refresh-backoff.test.ts
  • tests/fixtures/test-layout-expected.json

📝 Walkthrough

Walkthrough

The change adds per-account Codex refresh backoff, classifies terminal and operational failures, excludes cooling accounts from routing, clears cooldown state during account resets, and marks locally synthesized refresh refusals in request logs. Tests and layout fixtures cover the new behavior.

Changes

Codex pool refresh backoff

Layer / File(s) Summary
Refresh failure classification and cooldown
src/codex/pool-refresh-backoff.ts, src/codex/account-store.ts, src/codex/account-lifecycle.ts, tests/codex-integration/codex-pool-refresh-backoff.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json
src/codex/pool-refresh-backoff.ts:1-127 adds bounded per-account cooldown state. src/codex/account-store.ts:535-871 classifies failures, blocks refresh during cooldown, records retryable failures, and clears state on success. Missing credentials now use CodexCredentialUnavailableError at 911-913. Account purge clears cooldown state at src/codex/account-lifecycle.ts:46. Tests cover thresholds, isolation, clearing, and terminal classification.
Cooldown-aware account routing
src/codex/routing.ts
src/codex/routing.ts:1343-1739 excludes cooling accounts from selection and rotation, reports a transient block reason, detours bound threads, and clears cooldowns when the thread-account map resets.
Terminal classification and local refusal logging
src/server/responses/core.ts, src/server/request-log.ts
src/server/responses/core.ts:2527-2662 uses shared terminal-failure classification and passes request-log context through refresh refusal handling. src/server/responses/core.ts:6019 supplies that context during recovery. src/server/request-log.ts:822-830 marks proxy-synthesized refusals as synthetic.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Codex request
  participant forceRefreshCodexPoolToken
  participant pool-refresh-backoff
  participant routing
  participant request-log
  Codex request->>forceRefreshCodexPoolToken: Refresh pool credential
  forceRefreshCodexPoolToken->>pool-refresh-backoff: Check and record cooldown state
  pool-refresh-backoff-->>forceRefreshCodexPoolToken: Allow refresh or return cooldown error
  routing->>pool-refresh-backoff: Check account eligibility
  pool-refresh-backoff-->>routing: Return cooling status
  Codex request->>request-log: Mark local refusal when refresh is incomplete
Loading

Suggested reviewers: luvs01

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/4546-wph-pool-refresh-join

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T15:27:23.452957Z e7dc6f9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7dc6f9554

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/codex/routing.ts
&& getCodexQuotaHealthSnapshot(accountId, quotaScope, now) === null
&& !isCodexQuotaAvoided(accountId, quotaScope, now)
&& !isCodexAccountSoftAvoided(accountId, now)
&& !isCodexPoolRefreshCooling(accountId, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the owned architecture documents for this cooldown

This adds a new authentication and pool-selection invariant—failed credential refreshes now create a separate cooldown that changes eligibility and affinity handling—but none of the architecture documents mapped to src/codex/ or src/server/ were updated. Please document the cooldown, its lifecycle, and its relationship to credential generations in the applicable owned structure documents so the repository's architecture source of truth does not continue describing the previous routing behavior.

AGENTS.md reference: src/AGENTS.md:L10-L11

Useful? React with 👍 / 👎.

Comment on lines +32 to +38
type RefreshFailureBackoff = {
consecutiveFailures: number;
cooldownUntil: number;
reason: string;
};

const backoffByAccount = new Map<string, RefreshFailureBackoff>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fence refresh cooldowns to the credential generation

Keying this state only by account ID lets a failure for credential generation G suppress a newly published generation G+1. For example, if a transient refresh failure opens a cooldown and the user reauthenticates the account during that window, saveCodexAccountCredential advances the generation but never clears this map, so automatic routing still excludes the valid replacement for up to 60 seconds and the stale failure count can affect later refreshes. Store the credential generation/fingerprint with the cooldown and ignore or clear entries when the account record advances, including alias propagation.

Useful? React with 👍 / 👎.

Comment thread src/server/request-log.ts
Comment on lines +826 to +828
export function markLocalRequestLogRefusal(logCtx: RequestLogContext, reason: string): void {
logCtx.localTerminalReason = reason;
logCtx.terminalSource = "synthetic";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not classify a post-401 refusal as a zero-send local answer

When a stored-pool request receives an upstream 401 and its forced refresh then fails, the request has already made a real upstream send, but this helper sets localTerminalReason. That field's existing contract says no upstream request was issued, and addFinalRequestLog treats its presence as locallyAnswered when finalizing usage, so the resulting row can contradict its attempt/spend data and apply zero-send usage semantics. Keep terminalSource: "synthetic" for the locally generated final response, but use a distinct refusal-origin field rather than localTerminalReason for this post-send case.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

이 PR은 090_remaining_stack.md 일곱 칸 표에는 없지만, #4546 스택 tip(codex/4546-wpg-spend-instrumentation) 위에 붙은 풀 자격 증명 refresh 백오프(wph) 입니다. 지금 dev의 반열림 probe lease(#4626)와는 다른 축입니다. lease는 라우팅 회복을 묶고, 이 PR은 「강제 refresh가 실패한 계정」이 풀 전체를 503으로 고정하는 운영 사고를 끊습니다.

본문이 재현까지 적었습니다. 저장된 계정은 멀쩡해 보이는데 요청마다 poolCredentialRefreshIncompleteResponse로 503·server_is_overloaded가 나고, 활성 계정만 바꾸면 바로 회복됩니다. 서비스 로그에 upstream 한 줄도 안 남은 채 프록시가 스스로 거절한 케이스입니다.

src/codex/pool-refresh-backoff.ts가 연속 비종단 실패에 2s→60s 쿨다운을 걸고, 쿨다운 중에는 강제 refresh를 다시 시작하지 않으며, 선택 선호를 형제에게 넘기고, 로그 이유 codex_pool_refresh_incomplete로컬 거절임을 남깁니다. 성공 refresh는 창을 지웁니다. 계정 스토어·라우팅·request-log·core 배선과 통합 테스트가 같이 옵니다.

#4546 비용 나선과 직접 맞닿습니다. 캐시·쿼터 도메인(#4624)과 probe lease(#4626)가 있어도, 한 계정의 실패한 refresh 루프가 풀을 붙잡으면 건강한 형제가 놀고 토큰만 돕니다. 다만 base가 wpg라서 dev에 바로 합치려면 앞 스택이 먼저 랜딩돼야 합니다. CI는 아직 test 1/4 실패·나머지 pending 구간이 있습니다.

라인 src/codex/pool-refresh-backoff.ts - 프로세스 메모리 Map입니다. 재시작 후 쿨다운이 사라지는 것은 주석 의도와 맞지만, 다중 인스턴스에서는 인스턴스마다 다시 시도할 수 있습니다. 단일 프록시 가정인지, 나중에 공유 저장이 필요한지 한 줄로 적어 두면 wpe 원장과 경계가 분명해집니다.

라인 src/server/responses/core.ts / request-log - 로컬 거절을 upstream overload와 구분하는 로그·응답 코드가 실제 클라이언트에 어떻게 보이는지(재시도 가능 헤더·본문)만 확인하면 좋습니다. 재시도 가능(retryable)으로 남기면 클라이언트가 같은 계정에 다시 붙을 수 있어, 선택 선호 강등과 메시지가 같이 가야 합니다.

메인테이너의 판단이 필요한 지점

너의 추천
증상·재현·범위가 뚜렷하니 우선순위는 높입니다. 랜딩은 앞 스택 CI가 안정된 뒤 기차로 올리거나, 운영 긴급이면 backoff 모듈만 dev에 최소 단위로 분리하는 쪽을 고르세요. 지금은 스택 base 위에서 호스티드 CI 최종 SHA 녹색을 확인한 뒤 머지하는 쪽을 추천합니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/4546-wpg-spend-instrumentation branch from bd75d0d to 92f5ddc Compare September 14, 2026 15:31
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from e7dc6f9 to 4ecb95f Compare September 14, 2026 15:32
@lidge-jun
lidge-jun force-pushed the codex/4546-wpg-spend-instrumentation branch from 92f5ddc to 7382f6a Compare September 14, 2026 15:49
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 4ecb95f to 67c4115 Compare September 14, 2026 15:49
@lidge-jun
lidge-jun force-pushed the codex/4546-wpg-spend-instrumentation branch from 7382f6a to 27b5170 Compare September 14, 2026 16:08
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch 2 times, most recently from a0918cd to 34614c7 Compare September 14, 2026 16:14
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 34614c7 to fe8917d Compare September 14, 2026 16:30
@lidge-jun
lidge-jun force-pushed the codex/4546-wpg-spend-instrumentation branch from 2478c48 to 6c96bdb Compare September 14, 2026 17:46
Base automatically changed from codex/4546-wpg-spend-instrumentation to dev September 14, 2026 17:46
…t was local (#4546)

Reproduced live: every request routed to one pool account returned 503 server_is_overloaded, five of five sequential probes, with a healthy stored record and not one line in the service log. The refusal was this proxy's own poolCredentialRefreshIncompleteResponse, and because only revoked/expired counted as terminal, a missing record or a token-endpoint 5xx became an endlessly retryable 503 on an account selection kept returning to.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
Hosted CI failed six rows of the pool 401 refresh suite with 503 where 401 or 200 were expected. Withholding on the FIRST non-terminal failure was wrong twice over: a single token-endpoint blip is the ordinary case the next attempt clears, and a withheld refresh never runs, so an account whose grant is actually revoked could no longer discover that - the terminal 401 it owes the operator became a retryable 503 that never resolves. The cooldown now withholds only after three consecutive failures, and the do-not-grow-inside-the-window rule applies only while it is actually withholding, so a client retrying once a second can still reach the threshold.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
…account state (#4546)

A cooldown is per-account runtime state learned alongside the thread bindings, but it outlived clearThreadAccountMap. An account that had failed a refresh therefore stayed out of selection after the roster it belonged to was gone - which is what kept a replayed account unselectable on the NEXT request in the pool 401 suite.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 29c9aa3 to 6966470 Compare September 14, 2026 17:46
@lidge-jun
lidge-jun merged commit c3106e3 into dev Sep 14, 2026
1 check passed
@lidge-jun
lidge-jun deleted the codex/4546-wph-pool-refresh-join branch September 14, 2026 17:46
lidge-jun added a commit that referenced this pull request Sep 14, 2026
…nd clear one root (#4546) (#4657)

* fix(server): name the ceiling that refused, and let an operator see and clear one root (#4546)

The workflow budget could refuse a task and leave nothing behind to explain it.
Two specific gaps, both measured against the code rather than assumed.

The refusal never reached the request log at all. runAdmittedHttpTurn returns
before it calls work(), and every addFinalRequestLog in that file is inside
work, so there was no row and no context to mark. And the ceiling name never
reached the client either: classifyError rewrites every 429 to
rate_limit_error / rate_limit_exceeded, so the body was shaped exactly like a
provider rate limit and the workflow_budget_exhausted type argument was
discarded on the way out. All four count denials also shared one sentence about
a "concurrent-work limit", which was true of exactly one of them -- a task that
had hit the SEND ceiling was told to wait for turns to finish, and waiting never
helped because nothing was running.

Each denial now has its own sentence naming its ceiling and saying this proxy
decided it without contacting anyone. The wire status and type are unchanged on
purpose, since altering them changes how every client retries, so the
machine-readable name rides alongside on x-opencodex-local-refusal. Nothing
upstream sets that header, which is what makes its presence conclusive. Both
call sites now go through one src/server/workflow-refusal.ts instead of two
inline blocks that had drifted apart; where a log context exists, the row is
marked synthetic through the same helper #4639 introduced.

A refusal that parsed no body, chose no model and contacted no provider is not a
usage row, and forcing one would put a fabricated model and provider into
usage.jsonl. It goes instead into a bounded ring of recent budget events inside
the budget itself, recorded at every refusal return site in admitWorkflowTurn --
including the spend denials, which are decided inside the ledger branch and
never surface to the caller that formats the response.

GET /api/workflow-budget reads the tracked roots or one root, and
POST /api/workflow-budget/clear clears exactly one. The clear is bounded in a
specific way: it moves the windowed send ring and the child map and nothing
else. active belongs to turns still in flight, and zeroing it would let their
releases drive the count negative and hand out slots already taken. The spend
ledger is money an operator did not ask to forgive, and a count ceiling is not a
licence to reset it. The lifetime send total survives too, so clearing a ceiling
cannot launder the record of what the root actually did. A test drives that
last one directly: after a clear, an exhausted token budget still refuses.

Both routes are declared deferred-verb in the route registry. They are owed CLI
verbs and the ledger is process memory, so unlike the Lab routes there is no
local projection the CLI could read instead.

Local suite, typecheck, install and build: NOT RUN, per the standing
instruction. Hosted CI at the exact head is the only proof. Pushed --no-verify.

* fix(server): put the workflow refusal on the request log and expose its header (#4546)

CI caught one test and an independent plan review caught two real gaps.

The test failure was mine and it was the fixture-assumption mistake again: the
tracked-roots ordering test released each lease, and release() stamps lastSeenMs
from the wall clock because it feeds eviction ordering rather than a ceiling.
Three injected timestamps collapsed into three near-identical real ones. The
leases now stay open, which is what the test was actually about.

The review's blocking finding was that skipping the request-log row was a choice,
not a constraint. addFinalRequestLog takes whatever model and provider the
context carries, and the /v1/responses caller already seeds unknown/unknown
before the turn runs -- the same placeholder the native passthrough path writes.
So the refusal now writes a real row through that context: terminalSource
synthetic, a local reason, and an error code naming the ceiling, which wins over
the generic 429 classification in the logs column. Only /v1/responses threads it,
because the workflow root is the Codex x-codex-parent-thread-id header and no
other inbound wire carries it.

Second finding: the marker header was invisible to the dashboard. The data plane
never sets Access-Control-Expose-Headers, so cross-origin JavaScript could not
read it and the marker was useful to curl and to nothing else. The refusal now
exposes it.

The wire body is deliberately still rate_limit_error / rate_limit_exceeded. The
review asked for a distinct error code through classifyError; that changes how
every client classifies a 429 from this proxy, and the surface an operator
actually reads is the log row, which now carries the name.

Also recorded the two routes in structure/gui-and-management-api.md.

Local suite, typecheck, install and build: NOT RUN. Hosted CI at the exact head.

* fix(lib): look the refusal row up by requestId, which is the field it has (#4546)

The row was written correctly -- the count assertion passed -- but the lookup
read entry.id, and RequestLogEntry names that field requestId. find() returned
undefined and the terminalSource assertion read a property of nothing.

Local suite, typecheck, install and build: NOT RUN. Hosted CI at the exact head.

* fix(server): reach every surface's log, and actually let a browser read the header (#4546)

Second review round found that both of the first round's fixes stopped short.

The claim that only /v1/responses carries the Codex parent-thread header was
wrong. /v1/responses/compact carries it too -- its existing tests send it -- and
runAdmittedHttpTurn reads it for every caller, so a compact refusal still left
nothing on /api/logs. Every inbound surface that opens a request-log row now
threads it: compact, images, context history, alpha search, messages, chat
completions, audio transcriptions and live. /v1/messages/count_tokens is the one
that does not, because it opens no row at all.

Exposing the marker header was pointless while the admission refusal returned a
raw Response. Access-Control-Expose-Headers without Access-Control-Allow-Origin
is unreadable to cross-origin JavaScript, so the header remained useful to curl
and to nothing else. The refusal is wrapped in withCors now.

The pre-dispatch ceiling check in the responses path is a second refusal taken
after admission already succeeded, so nothing inside the budget module saw it: it
was the one refusal an operator could hit that left no event behind. It records
one now, through a seam that only a caller which decided the refusal itself uses,
so admitWorkflowTurn's own denials are not double-counted.

The review also pointed out that a unit test on the helper proves the helper and
not the wiring, and the wiring is exactly where this went wrong twice. A source
guard now asserts that every runAdmittedHttpTurn call site but one threads a
refusal row, and that the refusal is CORS-wrapped.

Local suite, typecheck, install and build: NOT RUN. Hosted CI at the exact head.

* test(server): stop pinning a call's terminator in the loopback CORS oracles (#4546)

Two source-oracle tests asserted the literal "req,\n          policy,\n        ));"
inside the Anthropic and chat branches. The invariant they exist for is that
withCors finishes with the receiving listener's policy rather than the public
config. The closing "));" was the outer runAdmittedHttpTurn call's terminator,
which has nothing to do with that, and both went red the moment that call gained
a fourth argument. The assertions now stop at the closing paren of withCors, so
they still fail on config and no longer fail on an unrelated argument.

Local suite, typecheck, install and build: NOT RUN. Hosted CI at the exact head.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant