fix(review): bound in-flight conversations across scheduled scans - #690
all-hands-bot wants to merge 10 commits into
Conversation
The scheduled reviewer bounds new conversations per scan but not the conversations that stay active across scans, so repeated scans stacked Docker runtimes until the host ran out of memory. Add an opt-in in-flight registry to AgentConversationDispatcher and have the scheduled scan start at most min(max_new_per_run, max_in_flight - in_flight) reviews, releasing capacity when a conversation is completed, errored, stale, or missing. Closes #689 Co-authored-by: openhands <openhands@all-hands.dev>
|
The capacity check still applies only to scheduled scans. In Please make admission atomic in the shared dispatcher and apply it to every launch path, including event mode. A read-count-then-create sequence is racy across concurrent automation runs; use the Automation KV store's optimistic concurrency or atomic |
|
@all-hands-bot fix the capacity race and event-mode bypass described above, then update this PR. |
The capacity check read the live count and then started conversations, which races across concurrent automation runs, and the event launch path called _start_review directly, bypassing the intake entirely, so a burst of review_requested deliveries could still create unbounded Docker runtimes. Move the authoritative bound into the shared dispatcher, at the moment a conversation is created or resumed: reserve the slot with the Automation KV store's optimistic if_version check (nx for the first write), retry a lost race against fresh state, and refuse past the cap. Both the scheduled drain and the event path launch through deliver, so both are bounded. A refused delivery returns a deferred disposition, starts no runtime, and writes no delivery record, leaving the GitHub request unconsumed for a later scan; the drain stops at the first deferral. The scan's earlier read stays as a pre-filter only. The worker passes MAX_IN_FLIGHT to the dispatcher, and a failed creation releases its reservation. Co-authored-by: openhands <openhands@all-hands.dev>
|
Done — pushed Capacity raceAdmission is now atomic and lives in
Event-mode bypassThe gate is now in Deferral semanticsA delivery refused at the cap returns Resuming an inactive conversation ( Tests addedDispatcher (
Worker (
Verification
Docs updated in Generated by OpenHands AI on behalf of the user. |
The atomic admission reserves a slot just before RemoteConversation.create runs, but _prune treated any 404 from the agent server as "conversation no longer exists" and released the slot. In the window between reserving and creating, a concurrent admission could therefore prune the reservation, free a slot that was about to be used, and start a runtime past the cap. With max_in_flight=1 two deliveries could both start. Hold a reservation whose conversation is still missing for a short grace window before believing the 404, so the in-flight count cannot drop below the runtimes being started. Once the grace passes, a 404 really is a gone conversation and the slot is released. Adds a regression test that starts two deliveries under a cap of one with a server that has not created the first conversation yet, and updates the 404-release tests to age the reservation past the grace.
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Assessment
Reviewed head 8d08a82 against the repository guidance in AGENTS.md, the current PR body/threads/checks, and issue #689 (still ready-for-dev, priority:high). The change is in scope: it stays inside the reviewer extension's own skills/github-pr-reviewer worker plus the shared skills/github/scripts/agent_conversation.py dispatcher, does not touch the review prompt, verdict parsing, secret selection, per-scan ordering, or deduplication, and every check for this head passes. The one PR Description Check failure (run 985) was superseded by the successful re-run 986 on the same SHA.
I verified the mechanics the fix depends on: the Automation KV store really implements the optimistic-concurrency contract the dispatcher assumes (meta=true returns a document version; if_version=N and nx=true on PUT reject a lost race with 409), and the per-automation store is a single locked document, which is what makes the read-modify-write meaningful. The atomic admission, the deferral that leaves the GitHub request unconsumed, the reservation release on a failed create, and the pending-grace window are all driven by real dispatcher code and covered by the new tests (including the 10-thread burst that admits exactly the cap, and the mixed event + scheduled case sharing one cap).
Executed locally:
uv run pytest -q tests/test_agent_conversation_dispatch.py tests/test_github_reviewer_delivery.py- 134 passeduv run pytest -q- 1110 passed, 23 skippeduv run python scripts/sync_extensions.py --check- no drift
I found no material correctness, security, or design issue. Non-blocking note below.
Non-blocking observation
MAX_IN_FLIGHT is dead configuration in the agent-driven standalone path: it is defined (line 67) and overridden from config.json (line 178) in skills/github-pr-reviewer/scripts/main.py, but nothing in that file ever reads it to bound starts - the only logic that consumes max_in_flight lives in the catalog worker.py / shared dispatcher. Meanwhile SKILL.md Step 6 now tells the agent-driven setup to set MAX_IN_FLIGHT = 4 "to cap the review conversations running at once across scans". A reader of that table could set the constant and see no effect on the manual path (which is uncapped per-run in both base and head). Worth either wiring main.py to the same cap or noting that the constant is only honored by the catalog worker. This does not affect the OSS Agent Canvas deployment this PR targets, which uses the catalog worker.
The remaining behavior I traced - dedupe reusing a runtime, deferred not consuming the request, release on failed create, in_flight() as a pre-filter with _admit as the authoritative bound, and the other dispatcher consumers (github-issue-to-pr, github-issue-triage) not opting in - is consistent and correct.
✅ APPROVED
neubig
left a comment
There was a problem hiding this comment.
The PR explicitly states that the OSS Agent Canvas deployment was unreachable and that no live validation was performed. The extensions review guide requires exercising the changed automation through its documented entrypoint in a supported environment; the concurrency tests are valuable regression coverage but do not replace runtime evidence for the dispatcher/deployment behavior. Please attach current-head evidence from the scheduled and event-driven reviewer entrypoints showing the cap under concurrent delivery, or obtain maintainer confirmation that the unavailable deployment limitation is acceptable. This comment was generated by an AI agent (OpenHands) on behalf of Graham Neubig.
|
Blocked on a rebase — Generated (should be regenerated, not hand-merged):
Hand-written (needs real resolution):
The 15 hunks in generated artifacts shouldn't be resolved by hand — that's how catalog drift gets introduced silently. Resolve the 7 hand-written hunks, then re-run The substance looks good: the atomic reservation in This comment was posted by an AI agent (OpenHands) on behalf of @neubig. |
Resolve the hand-written conflicts against main's Cloud/local dispatcher split (#712), unchanged-head dedupe (#699), and full-backlog scan (#703): - agent_conversation.py: keep main's `deliver` -> `_deliver_cloud` / `_deliver_local` structure. The PR's admission gate moves into `_deliver_local` (create, resume of an idle/paused conversation, and a new-revision turn), which now returns a "deferred" disposition that `deliver` reports without writing a delivery record. `deliver` keeps the PR's registry bookkeeping (`_remember`) and reads/writes through `_kv_value` / `_kv_put` because `_kv_request` now returns the whole body. The Cloud path is unchanged here; how the cap applies to it follows in a separate commit. - test_agent_conversation_dispatch.py: keep both new sections, the PR's in-flight/admission tests and main's Cloud tests. - SKILL.md: troubleshooting rows describe the cap on top of main's full-backlog scan (no rotating window). - manifest.json: main's descriptions plus the PR's in-flight cap clause. - A deferred-drain test stub accepts main's `head=` keyword; the ReviewIntake blank line left by main's ScanCursor removal is dropped; fixture `max_in_flight` lines take their neighbours' indentation. - skills/index.js and automations/bundle-index.js regenerated with `npm run build`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now delivers through the OpenHands API when a run has no Agent Server (#712). Every Cloud review conversation holds a sandbox of its own, so `max_in_flight` keeps one meaning wherever the automation runs instead of being silently ignored on Cloud and Enterprise: - `_deliver_cloud` reserves a slot before each launch: a new or replacement conversation (under the id actually started), running an idle conversation's turn, and sending a revision that may wake a paused sandbox. A refusal is `deferred` against the subject's current id and starts nothing; a failed start releases its reservation. - `_execution_status` reads a Cloud conversation from the OpenHands API. A paused, errored, or missing sandbox holds no slot; a sandbox that is still starting does. - An unlisted Cloud start is held for `_CLOUD_START_GRACE_SECONDS`, the window delivery already allows before restarting it, rather than the shorter local grace. - `in_flight()` and `_admit()` no longer require an open workspace on a Cloud run, which has none. Tests cover deferral at the cap for new and resumed subjects, slot ownership for a replacement id, capacity per sandbox/execution state, the Cloud start grace, release on a failed start, and a ten-delivery burst under a cap of two. Docs describe the Cloud behavior and that an event-only automation has no scheduled scan to retry a deferral. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count The mixed event/scheduled test fills the cap before the scan counts, so the scan's pre-filter stops it and the dispatcher's admission is never reached on the scheduled path; its comment said the scan was deferred. Correct the comment and add the race the atomic admission exists for: an event takes the deployment's only slot between the scan's count and its drain, the stale count lets the scheduled candidate through, and the real dispatcher defers it at launch without writing a delivery record. With admission disabled the test creates two conversations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring in #647 and #688. Mechanical resolution: - agent_conversation.py: keep both constant groups (Cloud sandbox states and `_MAX_ERROR_RETRIES`); `_deliver_local` takes both the PR's `subject` and #688's `can_retry`; `deliver` writes #688's `error_retries` into the record, then keeps the PR's `_kv_put` and registry bookkeeping. The retry branches are not gated by the in-flight cap yet; that follows in a separate commit. - worker.py: `ReviewIntake.drain` keeps the PR's stop on `deferred` and #688's count of `created` and `retried` against the per-scan bound. - test_agent_conversation_dispatch.py: keep #688's Cloud retry tests and the PR's Cloud in-flight section. - github-pr-reviewer manifest and fixture: keep the PR's 1.10.0 over main's 1.9.2. - automations/bundle-index.js regenerated with `npm run build`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#688 re-sends a matched delivery whose conversation ended in ERROR, which runs the conversation again. On both delivery paths that retry now passes the same admission as a start or resume: - `_deliver_local` and `_deliver_cloud` reserve a slot before the retry and return `deferred` when the deployment is at its cap. - `deliver` returns a deferral before the `error_retries` bookkeeping and the record write, so a deferred retry keeps its delivery unconsumed and does not spend one of its bounded attempts. - A retry that raises while starting releases its slot. `ReviewIntake.drain` still stops at the first `deferred` result before it counts `created` and `retried` against the per-scan bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`github-issue-to-pr` and `github-issue-triage` ship the same `agent_conversation.py`, which this PR changes. Their behavior is unchanged because in-flight tracking is opt-in, but the catalog schema asks for a bundle bump whenever shipped files change: 1.2.2 -> 1.2.3 and 1.3.1 -> 1.3.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
enyst
left a comment
There was a problem hiding this comment.
I'm an AI agent (Claude Code, based on Opus 5.5) helping Engel Nyst (@enyst) with project work.
Brought this up to date with main at 516285e. The conflicts from neubig's 2026-10-04 list are resolved, and the description is updated. What changed, besides the mechanical merge:
- The cap now covers the Cloud path too.
mainsplit delivery into an OpenHands API path and a local Agent Server path. Admission is enforced on both: a new start, resuming an idle conversation, sending a revision, and #688's ERROR retry each need a slot. On Cloud, a slot's state comes from the API. - The retry from #688 goes through admission. A retry past the cap is deferred without using up one of its two attempts, and a failed retry releases its slot.
- One test was strengthened. The earlier mixed event + scheduled test only ever reached the scan's pre-count.
test_a_scheduled_launch_behind_a_stale_count_is_refused_by_admissionnow drives the real admission check on the scheduled path.
Full suite: 1216 passed, 23 skipped.
I'm leaving a comment rather than approving. neubig's 2026-10-03 request for runtime evidence, or a maintainer decision that the automated coverage is enough, still applies to this head, and nothing here was run against a live deployment.
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Assessment
Reviewed head 516285e fresh (base main, open, not merged, not draft) against AGENTS.md and .agents/skills/custom-codereview-guide.md, the PR discussion, the current checks, and issue #689 (open, ready-for-dev, priority:high). The change is in scope: it is confined to the reviewer extension's own skills/github-pr-reviewer worker plus the shared skills/github/scripts/agent_conversation.py dispatcher, and does not touch the review prompt, verdict parsing, secret selection, ordering, or deduplication.
The two concerns raised on the earlier head are addressed here:
- Admission is atomic.
_admitreserves a slot with a KV read-modify-write guarded bynxfor the first write andif_versionafter, retries a lost race against a fresh read, and returnsdeferred(writing no delivery record) past the cap. I verified the store contract against the releasedopenhands-automationpackage: inkv_router.py,meta=truereturns the documentversion, annx/if_versionPUT answers 409, and lock timeouts also answer 409, so the CAS this code depends on is real. - The event path no longer bypasses the cap.
review_requestedstill skips the per-scan maximum but routes through_start_review->dispatcher.deliver->_admit, so a burst is admitted only up tomax_in_flight.test_event_mode_cap_defers_an_extra_request_and_leaves_it_unconsumedandtest_mixed_event_and_scheduled_share_one_capexercise exactly this, and the drain stops at the first deferral without counting it.
I found no material code defect. Capacity is released on the SDK's terminal statuses (finished/error/stuck, matching ConversationExecutionStatus.is_terminal()), on a 404 past the reservation grace, on the abandonment window, and on the Cloud stopped-sandbox statuses; a failed launch releases its reservation; in_flight() is a pre-filter with _admit authoritative.
Executed locally on this head (no tracked files modified):
uv run pytest -q tests/test_agent_conversation_dispatch.py tests/test_github_reviewer_delivery.py-> 183 passeduv run pytest -q-> 1216 passed, 23 skippeduv run python scripts/sync_extensions.py --check-> exit 0 (only the pre-existing, non-blockingissue-duplicate-checkercoverage warning)
Blocking items
-
Runtime-parity checkpoint unmet, and a maintainer request is open. The repository review guide lists "Executable instructions and runtime parity" as a blocking checkpoint: a changed automation must be exercised through its documented entrypoint in a supported environment. This head states it was not - the OSS Agent Canvas deployment was unreachable and no Cloud run was made. neubig's 2026-10-03 review asks for current-head runtime evidence from the scheduled and event-driven entrypoints under concurrent delivery, or a maintainer decision that the automated coverage is enough. That request is unresolved on this head, and the author's own comment agrees. Code inspection cannot satisfy it; it needs a live run or a maintainer sign-off.
-
The PR description gate is red on this head.
Validate PR descriptionfails (both runs for516285e) because theHUMAN:section is empty: the validator requires a >=20-character human note beforeAGENT:. This is a repo policy gate, not a code defect, but it is a current-head, merge-relevant fact. It is not in the branch ruleset's required set (test,sync-extensions,validate-claude-code, all passing), so it is a process blocker rather than a merge-protection one.
Note (non-blocking)
MAX_IN_FLIGHT is read and validated in the standalone agent-driven main.py, but only the catalog worker.py/dispatcher consumes it. The PR body now discloses this ("honored by the catalog worker.py, not by the standalone ... main.py flow"), so it is documented rather than misleading.
🔄 CHANGES REQUESTED
neubig
left a comment
There was a problem hiding this comment.
🟡 Acceptable
[TESTING GAPS]
- Failed CI: The current head has a failing
Validate PR descriptioncheck. A red head cannot be approved. - Missing runtime evidence: This changes the production admission/dispatch path for scheduled and event-triggered reviews, but the PR explicitly says the OSS Agent Canvas deployment was unreachable and no live validation was performed. The repository guide requires following the changed automation's documented entrypoint in a supported environment. Please attach current-head runtime evidence from both scheduled and event-driven delivery under concurrent load, showing the global cap and slot release behavior. Unit tests and fake KV coverage do not replace this required live proof.
The security screen found no embedded secrets, prompt-injection payload, dependency addition, unsafe workflow change, or pipe-to-shell behavior. The implementation and test coverage appear thoughtful, but the failed validation and acknowledged evidence gap are independently blocking.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🔴 HIGH
This is cross-run concurrency and backpressure logic on a credentialed production automation path; an error can recreate resource exhaustion or stall reviews globally.
VERDICT:
❌ Needs rework: Fix the failing PR-description check and provide live current-head runtime evidence for the affected delivery paths.
KEY INSIGHT:
Atomic admission is the correct architecture, but concurrency control is merge-ready only after the real dispatcher/KV/runtime path proves the modelled behavior.
This review was generated by OpenHands AI on behalf of neubig.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing. See the customization docs.- Re-request a review - the reviewer reads guidelines from the PR branch.
- When merged, the guideline file goes through normal code review.
Resolve with AI? Install the iterate skill and run
/iterate.Was this review helpful? React with 👍 or 👎.
HUMAN:
AGENT:
Why
The scheduled GitHub PR reviewer limited new conversations per scan, but not conversations still running across scans. On the OSS Agent Canvas VM, scans accumulated Docker runtimes until containers were OOM-killed. The host then ran out of memory, kept 159 GB of runtime data, and lost guest networking (#689). The
review_requestedevent path skipped the scan's intake entirely, so a burst of webhook deliveries could also start unbounded runtimes. A count-then-start check also races across concurrent automation runs.Summary
max_in_flightcap (default 4) atomically in the sharedAgentConversationDispatcher, which both the scheduled scan and the event path call. A slot is reserved with a conditional read-modify-write of a registry in the Automation KV store (nxfor the first write,if_versioncompare-and-set after that), and a lost race re-reads. The cap is checked whenever a conversation is created, resumed, sent a new revision, or retried after an ERROR (fix(github): retry errored deliveries and wait on action_required heads #688). Past the cap,deliverreturnsdeferred: nothing starts, no delivery record is written, and a deferred retry does not use up one of its two allowed attempts, so the GitHub request stays outstanding for a later scheduled scan. A failed launch releases its slot, and a just-reserved slot is held for a grace window before a 404 is believed.in_flight()read stays a pre-filter formin(max_new_per_run, max_in_flight - in_flight). The drain countscreatedandretriedagainstmax_new_per_runand stops at the firstdeferred. Bundles: github-pr-reviewer 1.10.0 (adds themaxInFlightsetup field), github-issue-to-pr 1.2.3, github-issue-triage 1.3.2.Issue Number
Closes #689
How to Test
The focused tests drive the shipped dispatcher and reviewer entrypoint against a fake KV store that models the real store's versioned document,
if_version, andnx:The KV conditional-write contract was checked against
openhands/automation/kv_router.pyin OpenHands/automation:meta=truereturns the version, anif_version/nxPUT answers 409, and lock timeouts also answer 409.Live validation was not performed. The OSS Agent Canvas deployment where the OOM occurred was not reachable from this environment, and no Cloud run was made. This still needs either a run of the scheduled and event-triggered reviewer under concurrent delivery on a real deployment, or maintainer confirmation that the automated coverage is acceptable.
Video/Screenshots
None; there is no UI change. Runtime logs from a deployment are the evidence still outstanding (see How to Test).
Notes
max_in_flight/MAX_IN_FLIGHTdefaults to 4 and is read from the renderedconfig.json; a non-positive or boolean value is rejected at load. LikeMAX_NEW_PER_RUN, it is honored by the catalogworker.py, not by the standalone agent-drivenmain.pyflow.github-issue-to-prandgithub-issue-triageship the updatedagent_conversation.py(bumped to 1.2.3 and 1.3.2) but don't opt into tracking, so their behavior is unchanged.Generated by OpenHands AI on behalf of the user; merged with current
main(#647, #688) and extended to the Cloud path by an AI agent (Claude Code, Opus 5.5) helping Engel Nyst (@enyst).