Skip to content

Bound the ambient wait when confirming a claude-swap switch - #3750

Closed
keepitmello wants to merge 1 commit into
steipete:mainfrom
keepitmello:fix/claude-swap-bounded-ambient-wait
Closed

keepitmello wants to merge 1 commit into
steipete:mainfrom
keepitmello:fix/claude-swap-bounded-ambient-wait

Conversation

@keepitmello

Copy link
Copy Markdown
Contributor

Fixes the "stuck after switching" half of #3736.

What happens

UsageStore.switchClaudeSwapAccount holds the transient switch state until
await refreshProvider(.claude) returns. That refresh reads Claude Code's own
ambient credential, and when that read has no answer it does not come back at
all — so the switch never completes:

  • the card stays on Details for B / Loading… while chip A stays highlighted,
  • and every later click is dropped, because handleClaudeSwapAccountSelection
    returns early while claudeSwapTransientState.task != nil.

The segmented control is then inert until relaunch. The issue reported the first
symptom; the second is why waiting it out never helped.

Observed with two accounts, where the adapter had already answered and the menu
still never moved:

11:32:15.256  chip clicked
11:32:15.962  adapter --switch-to 2   0.00s
11:32:16.035  adapter --list          0.02s   activeAccountNumber already 2
11:32:25      menu still "Loading…", chip still on the old account

It is not the adapter: the same thing happens with cswap and with a different
executable that speaks the same --list / --switch-to schema and answers in
0.02–0.04 s.

The change

The ambient wait is bounded instead of removed. ClaudeSwapSwitchReconciliationTests
pins the serialization contract ("ambient completion keeps switching serialized
until the real adapter list finishes"), and detaching the refresh entirely breaks
it — and would likely reopen the "previous account's data after switching" class
of bugs (#1785, #2731). Past the bound, reconciliation continues on the adapter
list and the ambient snapshot lands whenever it completes.

5 s is a first proposal; the value is easy to move if you would rather have it
shorter, configurable, or expressed as a switch-phase deadline.

Verified

before after
first switch confirmed not within 10 s 2.0–2.5 s
3 consecutive switches 2nd and 3rd clicks dropped all three confirmed
  • swift build
  • swift test --filter ClaudeSwap — 182 tests in 21 suites, all passing
  • make check — 0 violations in 2417 files
  • Packaged bundle on macOS 26 (Apple Silicon), CodexBar 0.61.1, two claude-swap
    accounts, segmented layout: three consecutive chip switches confirmed, adapter
    invocations logged for each.

An unbounded await on refreshProvider(.claude) can never return when Claude Code's own credential read has no answer. The transient switch state then stays set, so the menu keeps the requested account pending and handleClaudeSwapAccountSelection refuses every later selection until relaunch.
@clawsweeper

clawsweeper Bot commented Sep 19, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 19, 2026
@clawsweeper

clawsweeper Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed September 18, 2026, 11:16 PM ET / September 19, 2026, 03:16 UTC.

ClawSweeper review

What this changes

Adds a five-second timeout race around the Claude usage refresh performed after switching a claude-swap account.

Merge readiness

Blocked before merge - 8 items remain

The remaining stall is still actionable, but this patch does not enforce its timeout: the task group waits for the suspended ambient waiter before returning. The reported successful switches do not exercise the deadline.

Priority: P2
Reviewed head: b626085d5df470f8ac5c9815a374aec6427a04af
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦪 silver shellfish (2/6) The scope is focused and includes a useful real-run report, but the central timeout is ineffective and expiration remains unproven.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: The supplied macOS bundle run reports successful switches through the changed store path, but its 2.0–2.5-second result does not exercise five-second expiration. Add redacted after-fix terminal output, logs, or a recording showing deadline recovery while ambient refresh remains pending and a subsequent switch succeeds. Redact account details, credentials, and private endpoints. Updating the PR body should trigger review; otherwise ask a maintainer for @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🦪 silver shellfish (2/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The supplied macOS bundle run reports successful switches through the changed store path, but its 2.0–2.5-second result does not exercise five-second expiration. Add redacted after-fix terminal output, logs, or a recording showing deadline recovery while ambient refresh remains pending and a subsequent switch succeeds. Redact account details, credentials, and private endpoints. Updating the PR body should trigger review; otherwise ask a maintainer for @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 7 items Introduced timeout cannot abandon its waiter: The new task group awaits ambient.value in one child and a timer in another. After the timer finishes, cancelAll does not release the child awaiting the unstructured ambient task; group scope exit still waits for that child. Switch-state cleanup remains after this helper.
Swift task-group lifetime contract: The introduced withTaskGroup directly depends on Swift structured concurrency. The official proposal specifies that task groups await all children before returning, including after cancellation: Swift structured concurrency.
Existing bounded-wait implementation and regression pattern: UsageStore.runWithTimeout uses a single-completion continuation race instead of draining a task group. UsageStoreTimeoutTests explicitly covers a cancellation-ignoring operation; the Claude switch tests instead release ambient refresh before awaiting switch completion.
Findings 1 actionable finding [P2] Make the timeout return without draining the ambient waiter
Security None None.

How this fits together

CodexBar switches Claude accounts through an external adapter, then refreshes Claude usage and the adapter’s account list. The menu disables further switching until this reconciliation completes.

flowchart TD
    A[Account chip selected] --> B[External account switch]
    B --> C[Ambient Claude refresh]
    C --> D[Proposed timeout race]
    D --> E[Await adapter account list]
    E --> F[Clear pending state and enable chips]
Loading

Decision needed

Question Recommendation
May a completed adapter switch unlock further switching after five seconds while ambient Claude usage is still refreshing? Approve a bounded ambient wait: Permit unlocking after verified adapter reconciliation, provided delayed ambient results remain safe across subsequent switches.

Why: The recent owner-authored reconciliation fix and current documentation explicitly retain the guard until both refreshes finish; changing that lifetime requires intent beyond repairing the race.

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The supplied macOS bundle run reports successful switches through the changed store path, but its 2.0–2.5-second result does not exercise five-second expiration. Add redacted after-fix terminal output, logs, or a recording showing deadline recovery while ambient refresh remains pending and a subsequent switch succeeds. Redact account details, credentials, and private endpoints. Updating the PR body should trigger review; otherwise ask a maintainer for @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Make the timeout return without draining the ambient waiter (P2) - When refreshProvider(.claude) remains suspended beyond five seconds, the timer completes and cancelAll() runs, but withTaskGroup still awaits every child before returning. Cancelling the child suspended on ambient.value neither cancels the unstructured ambient task nor releases that await. The helper therefore remains blocked, switch-state cleanup never runs, and subsequent chip selections remain disabled. Use a single-completion timeout race, following the existing UsageStore.runWithTimeout pattern, and cover expiration with ambient refresh held pending.
  • Resolve merge risk (P1) - The proposed early release of the switch guard changes an explicitly retained reconciliation contract; owner agreement is unresolved.
  • Resolve merge risk (P2) - The reported improvement occurs before the deadline, so its connection to the new timeout remains unestablished.
  • Complete next step (P2) - Obtain owner agreement on the guard lifetime, repair the timeout race, and demonstrate expiration recovery through the changed runtime path.
  • Improve patch quality - Replace the draining task-group race and add a regression that keeps ambient refresh pending beyond the deadline.
  • Improve patch quality - Provide redacted real-path evidence of timeout recovery and another successful switch.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

  • [P2] Make the timeout return without draining the ambient waiter — Sources/CodexBar/Providers/Claude/UsageStore+ClaudeSwapRefresh.swift:221-226
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test delta Production +28/-1; tests +0/-0 The added timeout has a stated purpose, but no new regression exercises expiration while ambient refresh remains pending.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #3736
Summary: This PR attempts the unresolved slow-refresh portion of the canonical issue, but its deadline implementation is ineffective.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Decide the mitigation before merge
    With owner agreement, use the existing single-completion timeout pattern to bound ambient waiting while preserving adapter reconciliation, truthful pending state, and stale-result protection.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Technical review

Best possible solution:

With owner agreement, use the existing single-completion timeout pattern to bound ambient waiting while preserving adapter reconciliation, truthful pending state, and stale-result protection.

Do we have a high-confidence way to reproduce the issue?

Yes, from source: hold ambient refresh pending after the adapter completes and the new task group still cannot exit when its timer wins. No runtime tests were executed during this read-only review.

Is this the best way to solve the issue?

No. The structured task-group race cannot enforce the claimed bound; the repository already has a continuation-based timeout pattern suitable for adaptation.

Full review comments:

  • [P2] Make the timeout return without draining the ambient waiter — Sources/CodexBar/Providers/Claude/UsageStore+ClaudeSwapRefresh.swift:221-226
    When refreshProvider(.claude) remains suspended beyond five seconds, the timer completes and cancelAll() runs, but withTaskGroup still awaits every child before returning. Cancelling the child suspended on ambient.value neither cancels the unstructured ambient task nor releases that await. The helper therefore remains blocked, switch-state cleanup never runs, and subsequent chip selections remain disabled. Use a single-completion timeout race, following the existing UsageStore.runWithTimeout pattern, and cover expiration with ambient refresh held pending.
    Confidence: 0.99

Overall correctness: patch is incorrect
Overall confidence: 0.99

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 6e603f578647.

Labels

Label changes:

  • add P2: This addresses stalled switching in the optional Claude multi-account integration, with a focused correctness blocker.
  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦐 gold shrimp and patch quality is 🦪 silver shellfish.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The supplied macOS bundle run reports successful switches through the changed store path, but its 2.0–2.5-second result does not exercise five-second expiration. Add redacted after-fix terminal output, logs, or a recording showing deadline recovery while ambient refresh remains pending and a subsequent switch succeeds. Redact account details, credentials, and private endpoints. Updating the PR body should trigger review; otherwise ask a maintainer for @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P2: This addresses stalled switching in the optional Claude multi-account integration, with a focused correctness blocker.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦐 gold shrimp and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The supplied macOS bundle run reports successful switches through the changed store path, but its 2.0–2.5-second result does not exercise five-second expiration. Add redacted after-fix terminal output, logs, or a recording showing deadline recovery while ambient refresh remains pending and a subsequent switch succeeds. Redact account details, credentials, and private endpoints. Updating the PR body should trigger review; otherwise ask a maintainer for @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

Likely related people:

  • unknown: The claimed source-line change could not be verified from bounded local history. (role: source history unknown; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@steipete

Copy link
Copy Markdown
Owner

Maintainer review of b626085d5df470f8ac5c9815a374aec6427a04af: the proposed five-second timeout does not actually bound the wait. withTaskGroup waits for every child before its scope exits; cancelling the child awaiting ambient.value does not finish the unstructured ambient task. I reproduced the same structure with a 100 ms timeout and two seconds of synthetic ambient work: it returned after 2.005 seconds. No provider or Keychain access was involved.

There is a second reconciliation concern when repairing the timeout. Current provider refreshes wait for predecessor requests before starting, and the Claude adapter-list refresh is scheduled only after that start. A second switch while the first ambient refresh remains suspended can therefore reach reconciliation without obtaining its own fresh adapter list.

Please add deterministic coverage for timeout expiry while ambient work is still held, a second switch before the first refresh finishes, fresh adapter/account identity, and rejection of late results. The fix needs to preserve the serialized account-reconciliation contract from #3736/#3740. Keeping this open for that repair; the existing two-second success path does not exercise timeout expiry.

steipete added a commit that referenced this pull request Sep 21, 2026
Bound the ambient refresh wait without cancelling the provider request, and
refresh the adapter independently when a stalled predecessor delays that read.
Keep credential transactions serialized through the current adapter list.

Adopts #3750 with a bounded join instead of a task group that still waits for
its suspended child. Refs #3736.

Co-authored-by: keepitmello <keepitmello@users.noreply.github.com>
(cherry picked from commit b73c145)
@steipete

Copy link
Copy Markdown
Owner

Adopted with a bounded-join rewrite in #3812. The timeout ends only the ambient wait, while adapter reconciliation and serialized credential transactions remain intact. Synthetic regressions cover a suspended ambient probe, a later switch queued behind it, retained switch errors, and late ambient results. Thanks @keepitmello!

@steipete steipete closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants