Skip to content

feat(serve)!: admit concurrent runs with a bounded queue instead of refusing them - #400

Open
frontierkodiak wants to merge 4 commits into
steipete:mainfrom
frontierkodiak:serve-concurrency
Open

feat(serve)!: admit concurrent runs with a bounded queue instead of refusing them#400
frontierkodiak wants to merge 4 commits into
steipete:mainfrom
frontierkodiak:serve-concurrency

Conversation

@frontierkodiak

Copy link
Copy Markdown
Contributor

Behaviour change with a chosen default. If you'd rather the cap, the queue depth, or the refusal status be different, they are all one constant away — happy to follow your call.

What

oracle serve was single-flight: a second caller got HTTP 409 busy and was expected to invent a retry policy. That shape fits a service where runs are short. These are not — a Pro answer can take ten minutes, and nearly all of it is waiting for the model rather than driving the browser.

Nothing in the browser stack required the restriction. Runs hold their own CDP page connection (connectToNewTarget), clipboard capture is page-local JS monkey-patching inside Runtime.evaluate, input and uploads are per-target, temp dirs are per-run, and a repo-wide sweep finds no module-scope mutable state in src/browser beyond frozen constants. The composer section that genuinely must be serialized already is, by the profile run lock. What was missing was a place for the next caller to wait.

Approach

A bounded number of concurrent runs (4 by default) and a FIFO queue for the rest. A queued caller is told its position over the existing log event, so older clients ignore it rather than breaking. Refusal is reserved for a full queue — 503 with Retry-After — because a caller told "later" can wait, while a caller told "no" has to guess.

Cancellation had no representation at all: the service never observed client disconnect, and BrowserRunOptions had no way to express it. Measured before this change, a client killed ten seconds into a thirty-second run held its browser tab and its slot for the remaining twenty. signal now joins the existing disconnect race, so every awaited step honours it and the existing unwinding releases the tab lease and closes the owned tab. It raises BrowserRunCancelledError, since a caller that walked away is not a run that went wrong.

Two isolation defects that single-flight was hiding are fixed with it: the client's session slug was used verbatim as the key for the server's own artifact directory (slugs are prompt-derived, so two callers could collide), and the browser tab cap is now pinned to what the service admits, so extra callers wait in the queue where the wait is visible rather than inside the lease loop where it is not.

Real behavior

Five concurrent callers against one browser, watched through /health:

t=4s   active=4 queued=1
...
t=32s  active=4 queued=1
t=36s  active=3 queued=0
t=40s  active=1 queued=0

All five exited 0, each got its own answer, and each landed in a distinct conversation — five ids, no cross-talk.

Cancellation, same setup: a client killed ten seconds into a run now releases the slot 2s after disconnect (previously 20s, i.e. natural completion), and the service records cancelled: the caller disconnected rather than a completion.

Worth knowing

ChatGPT itself rate-limits well below what the transport can drive: six conversations opened at once tripped its "Too many requests" modal repeatedly on a Pro account, while five did not. So the default cap of 4 is deliberately under that. (#395 makes that modal report itself as a rate limit instead of as a missing model.)

Breaking

A second concurrent caller is now served rather than receiving 409 busy. A client that treated 409 as its back-off signal will no longer see one; saturation is 503 queue_full.

Tests

Ten added: admission up to the limit, the caller past the limit waiting rather than failing, FIFO order, saturation only when the queue is full too, cancellation while queued and while running, double-release safety, an end-to-end concurrency run through the real HTTP path, per-run session-id isolation, and the tab cap being pinned to what the service admits.

Full suite green: 1772 passed / 43 skipped.

…efusing them

`oracle serve` was single-flight: a second caller got HTTP 409 `busy` and was
expected to invent a retry policy. That shape fits a service where runs are
short. These runs are not — a Pro answer can take ten minutes, and nearly all of
it is waiting for the model rather than driving the browser.

Nothing in the browser stack required the restriction. Runs hold their own CDP
page connection, clipboard capture is page-local, input and uploads are
per-target, and temp directories are per-run; the composer section that genuinely
must be serialized already is, by the profile run lock. What was missing was a
place for the next caller to wait.

So: a bounded number of concurrent runs (4 by default) and a FIFO queue for the
rest. A queued caller is told its position over the existing `log` event, so
older clients ignore it rather than breaking. Refusal is now reserved for a full
queue — 503 with `Retry-After` — because a caller told "later" can wait, while a
caller told "no" has to guess.

Cancellation had no representation at all: the service never observed client
disconnect. It does now, and a disconnect frees whatever the caller held —
its place in the queue, or its slot. Without that a long-lived service leaks
capacity to clients that walked away until it stops accepting work.

Two isolation defects that single-flight was hiding are fixed with it. The
client's session slug was used verbatim as the key for the server's own artifact
directory, and slugs are prompt-derived, so two callers could collide; the server
now namespaces per run. And the browser tab cap is pinned to what the service
admits, so extra callers wait in the queue where the wait is visible rather than
inside the lease loop where it is not.

`/health` reports active, queued, and capacity so a caller can decide when to
send work instead of discovering the answer by being queued.

BREAKING: a second concurrent caller is now served rather than receiving 409
`busy`. A client that treated 409 as its signal to back off will no longer see
one; saturation is 503 `queue_full`.

Claude-Session: https://claude.ai/code/session_01HsXirqcfqtr1Cae9zYCLDk
`oracle serve` observed client disconnects and freed the queue slot, but nothing
reached the run itself: `BrowserRunOptions` had no way to express cancellation,
so a disconnected caller's run continued to completion and its capacity came
back only by accident of finishing. Measured before this change, a client killed
ten seconds into a thirty-second run held its slot for the remaining twenty.

That is the wrong shape for runs this long. A browser run holds a tab and a slot
on a shared profile for minutes, and the caller is the only party that knows it
has stopped caring.

`signal` joins the existing disconnect race, so every awaited step honours it and
the existing finally does the unwinding it already knew how to do — releasing the
tab lease, closing the owned tab, stopping the monitors. Cancellation raises
`BrowserRunCancelledError` rather than a generic failure, because a caller that
walked away is not a run that went wrong, and a reader of the session record
should not go looking for a fault.

Verified live: the same interrupt now releases the slot 2s after disconnect
instead of 20s, and the service records "cancelled: the caller disconnected"
rather than a completion.

Claude-Session: https://claude.ai/code/session_01HsXirqcfqtr1Cae9zYCLDk
@clawsweeper

clawsweeper Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

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

@clawsweeper clawsweeper Bot added merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 18, 2026
@clawsweeper

clawsweeper Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed August 29, 2026, 4:13 AM ET / 08:13 UTC.

ClawSweeper review

What this changes

The branch replaces single-flight remote browser runs with bounded concurrent admission, FIFO queueing, disconnect cancellation, health counters, and per-run artifact isolation.

Merge readiness

Blocked until stronger real behavior proof is added - 7 items remain

Keep open: this is still distinct from current main, but two previously identified P1 defects remain in the introduced admission and cancellation paths, and the supplied live claim does not prove the latest fixes.

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

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The patch has substantial targeted coverage and a credible feature direction, but two P1 correctness defects and insufficient latest-head runtime proof block merge.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: The PR body contains a useful claimed terminal trace for concurrent browser work, but it does not provide auditable redacted after-fix evidence for the exact-head host-cap and remote-abort changes, and it does not cover the two source-proven blockers. 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 🦐 gold shrimp (3/6) 2 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The PR body contains a useful claimed terminal trace for concurrent browser work, but it does not provide auditable redacted after-fix evidence for the exact-head host-cap and remote-abort changes, and it does not cover the two source-proven blockers. 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 4 items Bounded queue is not enforced atomically: The handler checks saturation before awaiting request-body parsing, while RunSlots.acquire always appends when active slots are full. Several valid requests can pass the earlier check and subsequently exceed maxQueuedRuns.
Abort does not cover Chrome setup: The new abort race wraps only the manual-login tab lease; the following manual Chrome acquisition or Chrome launch awaits directly, leaving an aborted caller's admission slot occupied until setup completes.
Current main remains single-flight: The fetched main revision still uses the prior busy guard and HTTP 409 path, so the central scheduling feature is not already implemented there.
Findings 2 actionable findings [P1] Reject callers atomically when the queue is full
[P1] Race Chrome setup with the abort signal
Security None None.

How this fits together

oracle serve receives remote browser-run requests and schedules them onto a shared Chrome profile. Its admission layer decides whether a request runs, waits, or is refused before browser execution streams a result back to the caller.

flowchart LR
A[Remote callers] --> B[Serve request handler]
B --> C[Request validation]
C --> D[Admission slots]
D --> E[Browser run]
E --> F[Shared Chrome profile]
E --> G[Result stream]
D --> H[Health counters]
Loading

Decision needed

Question Recommendation
Should oracle serve replace its established immediate HTTP 409 busy response with a bounded-wait queue and HTTP 503 saturation response? Adopt bounded queueing: Keep the new scheduling model after fixing the admission and cancellation defects, with clear compatibility documentation for 409-dependent clients.

Why: This is a deliberate remote-service contract and default-policy change; correctness repairs cannot determine whether existing client backoff behavior should be preserved or migrated.

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The PR body contains a useful claimed terminal trace for concurrent browser work, but it does not provide auditable redacted after-fix evidence for the exact-head host-cap and remote-abort changes, and it does not cover the two source-proven blockers. 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.
  • Reject callers atomically when the queue is full (P1) - isSaturated is checked before the awaited body read, but acquire unconditionally appends once active slots are full. A burst can therefore pass the check together and exceed maxQueuedRuns; reserve or reject capacity within acquisition instead.
  • Race Chrome setup with the abort signal (P1) - The new raceWithAbort call only wraps the tab lease. The following Chrome acquisition/launch awaits directly, so a disconnected client can still hold an admitted slot until that slow setup completes; safely clean up any resource that wins after the abort.
  • Resolve merge risk (P1) - A burst of valid requests can bypass the configured queue limit and accumulate arbitrary waiters because capacity is checked before an asynchronous boundary.
  • Resolve merge risk (P1) - An aborted caller can still retain its admitted slot during Chrome launch or CDP setup.
  • Resolve merge risk (P1) - The proposed 409-to-queue/503 contract is intentionally breaking for clients that use 409 as their backoff signal, and the PR is currently dirty against main.

Findings

  • [P1] Reject callers atomically when the queue is full — src/remote/server.ts:166-180
  • [P1] Race Chrome setup with the abort signal — src/browser/index.ts:1122
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production vs. test delta production +289/-3, tests +370 The feature spans remote scheduling, client cancellation, and browser lifecycle, with substantial focused regression coverage.

Merge-risk options

Maintainer options:

  1. Make admission atomic before merge (recommended)
    Enforce maxQueuedRuns inside the slot-acquisition operation and add an HTTP-level burst test that proves excess callers receive 503 rather than accumulating.
  2. Pause the contract change
    Keep the existing single-flight behavior until maintainers choose the compatibility policy and the bounded-cap invariant is repaired.

Technical review

Best possible solution:

Adopt the bounded-queue contract only after slot acquisition atomically rejects excess waiters, cancellation covers all browser setup with late-resource cleanup, and the chosen upgrade behavior is explicitly confirmed.

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

Yes, by source: fill active slots, then send multiple valid requests whose body reads complete after the pre-read saturation check; each can join the waiting array despite the configured queue cap.

Is this the best way to solve the issue?

No: the intended queue needs atomic reservation or rejection after validation, and the cancellation implementation must race Chrome setup rather than only the tab-lease wait.

Full review comments:

  • [P1] Reject callers atomically when the queue is full — src/remote/server.ts:166-180
    isSaturated is checked before the awaited body read, but acquire unconditionally appends once active slots are full. A burst can therefore pass the check together and exceed maxQueuedRuns; reserve or reject capacity within acquisition instead.
    Confidence: 0.99
  • [P1] Race Chrome setup with the abort signal — src/browser/index.ts:1122
    The new raceWithAbort call only wraps the tab lease. The following Chrome acquisition/launch awaits directly, so a disconnected client can still hold an admitted slot until that slow setup completes; safely clean up any resource that wins after the abort.
    Confidence: 0.98

Overall correctness: patch is incorrect
Overall confidence: 0.99

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against bbc1b3b0261d.

Labels

Label justifications:

  • P2: This is a meaningful but bounded remote-service behavior change rather than an active core-runtime outage.
  • merge-risk: 🚨 availability: The introduced admission race can exceed the configured wait limit and retain capacity after an abort during setup.
  • merge-risk: 🚨 compatibility: The branch intentionally replaces established HTTP 409 busy responses with queueing and HTTP 503 saturation.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body contains a useful claimed terminal trace for concurrent browser work, but it does not provide auditable redacted after-fix evidence for the exact-head host-cap and remote-abort changes, and it does not cover the two source-proven blockers. 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

Acceptance criteria:

  • [P1] pnpm vitest run tests/remote/server.test.ts.
  • [P1] Add and run focused browser lifecycle coverage for abort during Chrome acquisition or launch.

What I checked:

  • Bounded queue is not enforced atomically: The handler checks saturation before awaiting request-body parsing, while RunSlots.acquire always appends when active slots are full. Several valid requests can pass the earlier check and subsequently exceed maxQueuedRuns. (src/remote/server.ts:166, e3a6e67df085)
  • Abort does not cover Chrome setup: The new abort race wraps only the manual-login tab lease; the following manual Chrome acquisition or Chrome launch awaits directly, leaving an aborted caller's admission slot occupied until setup completes. (src/browser/index.ts:1122, e3a6e67df085)
  • Current main remains single-flight: The fetched main revision still uses the prior busy guard and HTTP 409 path, so the central scheduling feature is not already implemented there. (src/remote/server.ts:108, bbc1b3b0261d)
  • Feature-history routing: History shows recent remote lifecycle work in commit 653c621 and remote option forwarding in commit aabc221; these predate the proposed branch and identify relevant current-main owners. (src/remote/server.ts:478, 653c621b7ff7)

Likely related people:

  • steipete: History shows sustained work across the remote server and browser lifecycle, including the current-main cookie-sync change. (role: recent area contributor; confidence: high; commits: 3a185f55918a; files: src/remote/server.ts, src/browser/index.ts)
  • rtl-ai: Commit 653c621 added serve-owned tab cleanup, which this PR's disconnect and tab-release paths build upon. (role: introduced adjacent remote lifecycle behavior; confidence: high; commits: 653c621b7ff7; files: src/remote/server.ts, src/browser/index.ts)
  • Piotr Durlej: Commit aabc221 established forwarding of browser-run options through the remote bridge, adjacent to the new abort-signal forwarding. (role: introduced remote option forwarding; confidence: medium; commits: aabc22137a8b; files: src/remote/server.ts, src/remote/client.ts)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Make queue admission atomic and cover concurrent saturation through the real HTTP path.
  • Race abort through Chrome setup and prove late-acquired resources are released.
  • Post redacted after-fix saturation and remote-abort traces without endpoints, tokens, or other private data; updating the PR body should trigger re-review.

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.

History

Review history (3 earlier review cycles)
  • reviewed 2026-08-18T19:05:30.067Z sha 3947c1a :: found issues before merge. :: [P1] Forward AbortSignal to remote requests | [P1] Make pre-connection setup cancellable | [P1] Preserve the configured shared-browser tab cap
  • reviewed 2026-08-18T19:24:08.472Z sha e3a6e67 :: needs real behavior proof before merge. :: [P1] Reserve queue capacity atomically | [P1] Race cancellation through Chrome acquisition | [P2] Align the promised default with the effective cap | [P3] Remove the stale tab-cap claim
  • reviewed 2026-08-21T21:22:18.012Z sha e3a6e67 :: needs real behavior proof before merge. :: [P1] Enforce queue capacity when acquiring a slot | [P1] Race cancellation through Chrome acquisition | [P2] Align the effective default with the advertised four runs | [P3] Remove the release-owned changelog section

…ng the tab cap

Three gaps in the first pass, all raised in review and all correct.

**The tab cap is not the service's to overwrite.** Pinning
`browserConfig.maxConcurrentTabs` to the admission limit silently replaced an
operator's lower choice — exactly what someone staying under an account's
throttling would have set. The dependency runs the other way: the tab cap is the
physical constraint on a shared profile, so the service now reads the host's
configured cap and admits at most that many, logging when it clamps.

**`signal` has to mean the same thing on both sides of the bridge.** The remote
executor never observed it, so a caller aborting a remote run cancelled nothing:
the request stayed open, the service never saw a disconnect, and the run kept its
slot and its browser tab until it finished on its own. That is worse than not
having cancellation, because the caller believes it worked. The executor now
destroys its request on abort and refuses to send one that was aborted first.

**Cancellation arrived too late to matter.** The abort race was installed after
the tab-lease wait, Chrome startup, and the CDP connection — the slowest part of
a cold run, and the part most likely to be waiting on a peer. Several abandoned
requests could hold every slot until their browser timeouts. The race is now
built before setup begins. A lease granted after the caller gave up is handed
back rather than leaked, since a slot abandoned mid-queue would otherwise sit for
six hours.

Claude-Session: https://claude.ai/code/session_01HsXirqcfqtr1Cae9zYCLDk
@frontierkodiak

Copy link
Copy Markdown
Contributor Author

Review follow-up

All three P1 findings were correct and are fixed in db4c….

Preserve the configured shared-browser tab cap. You're right that the dependency was backwards. The tab cap is the physical constraint on a shared profile and belongs to the host; pinning it to whatever the service admits silently discards an operator's lower choice — and "lower to avoid account throttling" is precisely the case, since ChatGPT rate-limits well below what the transport can drive. The service now reads the host's configured maxConcurrentTabs and admits at most that many, logging when it clamps:

[serve] Admitting 3 concurrent run(s): the shared-profile tab cap (3) is lower than the requested 4.

Forward AbortSignal to remote requests. Also right, and the worst of the three: a caller aborting a remote run cancelled nothing while believing it had. The executor now destroys its HTTP request on abort — which is how the service learns to cancel, via its own disconnect handling — and refuses to send a request whose signal was already aborted.

Make pre-connection setup cancellable. The race was installed after the tab-lease wait, Chrome startup, and the CDP connection: the slowest stretch of a cold run and the one most likely to be waiting on a peer. It is now built before setup begins.

On your parenthetical about leaking — losing the race does not cancel the acquisition, so a lease granted after the caller gave up is now handed back explicitly. Otherwise cancelling during a queue wait burns a slot on the shared profile for the six-hour stale window, which is worse than not honouring the cancellation at all.

Two tests added for the bridge case: a caller aborting a remote run is observed as an abort inside the run, and an already-aborted caller never sends the request.

Full suite: 1774 passed / 43 skipped.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added 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. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. 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.

1 participant