Skip to content

Run up to STRICT_MAX_WORKERS runs concurrently in the run service - #264

Merged
tzhouam merged 1 commit into
mainfrom
feat/strict-concurrent-workers
Sep 30, 2026
Merged

tzhouam merged 1 commit into
mainfrom
feat/strict-concurrent-workers

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Why

RunService drained its queue with one worker thread (MAX_STRICT_WORKERS = 1), so an embedded Strict host could never run two reviews at once. The reviewbot routing work (per-backend concurrency caps, e.g. cursor: 10) needs a host that can. This is PR 4a of that plan; the reviewbot side (PR 4b) consumes it.

What

  • STRICT_MAX_WORKERS (Settings.strict_max_workers, 1–32, default 1, so behaviour is unchanged unless set) and StrictRuntimeConfig.max_workers start N worker threads. capabilities() reports the configured number as max_strict_workers. Strict API 1.3.0 → 1.4.0, so a consumer can gate on it.
  • RunQueue never hands out two runs with the same conflict key: the resolved checkout (via RepositoryContextResolver: frozen path → REPO_PATHS → adapter manifest) plus the PR (None for issue tasks).
    • A busy key keeps its run queued, in order, without holding a worker, so one busy PR never stalls other PRs.
    • A malformed or unkeyable request still enqueues under a shared UNKEYED key and cannot kill a worker.
  • Cursor sessions in a shared checkout are serialized. Cursor writes its bridge config to a fixed path in the session cwd (.cursor/mcp.json).
    • A PR run whose worktree can't be materialized degrades to the live checkout at runtime, which issue tasks also use.
    • Bridge-carrying cursor sessions whose cwd is a shared checkout take an flock in that checkout's git dir for the session. This works across threads, processes, services and the CLI.
    • A timeout returns a loud refusal plus a capability_gap.
    • Managed PR worktrees and run dirs aren't locked.
  • Reaper vs. live run:
    • A reused worktree is touched under its materialize lock, and the reaper re-reads mtime under its own exclusive lock. This closes the gap between materialize releasing the tree and the run taking its shared hold.
    • _reap() is single-flight per service.

Shared-state investigation (two concurrent run subprocesses of one host)

Shared thing Same PR+head Different PRs Status
Duplicate execution of one key idempotency key lock + created gate: one run, one execution n/a safe (pre-existing)
run dir / run_status.json / reservation per-run dir, single writer, CAS claim per-run safe
run_id generation timestamp + uuid6 safe
git fetch into the shared checkout forced run-scoped refs refs/imx/<run_id>/{base,head}; no reader of the shared FETCH_HEAD is reachable from the run service (pr_rebase is not a read-only kind) same safe
PR-time worktree (<repo>-<owner8>-pr<N>-<sha12>) one tree shared, materialize serialized by LOCK_EX, reuse verified distinct trees; git worktree add into distinct names different PRs safe; same PR serialized by the new queue key
cursor .cursor/mcp.json in session cwd would cross-bind tool scope/trace distinct trees → distinct files fixed: queue key (same PR) + cwd flock (shared live checkout)
claude-code / codex / zcode / deepseek bridge config per-spec file / -c overrides / per-session scratch dir / run-dir composition safe (nothing in the shared cwd)
degraded PR run or issue task in the live checkout shares the live checkout fixed: cursor cwd flock
reaper vs. a reused worktree older than retention gap between materialize unlock and hold fixed: touch on reuse + mtime re-check under the reaper's lock
_reap() from several workers fixed: single-flight; update-ref -d races on packed-refs.lock only delay a ref to the next sweep
refs/imx reaping scoped to runs whose dir is gone; run dirs are never deleted safe
knowledge pin / snapshot prune pinned at reservation; a pruned pin fails the run safe (independent of N)
engine RunLock (exit-3 lock-loser) per-run <run_dir>/.lock never contended across runs safe
vendor HOME state (~/.cursor, ~/.codex, ~/.zcode) concurrent CLI sessions already happen within one run (strict_backend_concurrency=2) accepted (vendor CLIs handle concurrent sessions; the live smoke below ran two cursor runs at once)
not covered: parallel sessions inside one run in its own worktree (ensemble / verify fan-out) pre-existing, independent of N documented in the SPEC

Verification

  • Full suite: 2018 passed, 17 skipped. Baseline on origin/main fd79058 was 1992 passed, 17 skipped, so +26 new tests and no regressions. Run with PYTHONPATH=$PWD/src.
  • One flaky run: the first full run executed while the live smoke loaded the machine, and 6 Direct/SDK recheck tests failed. They passed in isolation and in three later full runs; they don't touch the run service.
  • New tests use real threads (test/test_run_service_concurrency.py):
    • N-way parallelism, the worker cap, same-PR serialization with skip-ahead, and issue-task serialization
    • failed-launch isolation, and a malformed request that can't kill a worker
    • adapter-backed implicit/explicit keys, the SDK max_workers → handshake path, and config bounds
    • three cursor sessions in one checkout run strictly one at a time, each seeing its own spec; a timeout is loud
    • reaper behaviour on a reused tree
  • The race tests fail without their fixes. I reverted the reaper and cursor fixes and the tests went red, then restored them.
  • check_spec_freshness --strict, check_doc_links, check_doc_citations: all OK. SPEC pages updated: app/run_service, config, contract, sdk, engine/worktrees, idempotency, providers/cursor.
  • Live smoke (StrictRuntime, max_workers=2, cursor backend, a blob-less scratch clone of vllm-project/vllm-omni, review_depth=light):
    • Two real Strict reviews ran concurrently: vllm-omni #8290 (done in 568 s, APPROVE at 8274b38c7b7f) and #8284 (done in 782 s, REQUEST CHANGES with 2 comments, at 32620f974304).
    • Both child processes were alive together from t≈20 s to t≈568 s.
    • The two PR worktrees were materialized side by side from one clone.
    • Each run's bridge_trace.jsonl referenced only its own PR worktree: 80/0 and 21/0 lines for pr8290/pr8284.
  • Review gate:
    • Iteration 1 was REVISE with two P1s. One was the degraded-live-checkout overlap; the other was that the key missed the adapter-manifest checkout resolution. Both are fixed here, with regression tests.
    • Iteration 2 was APPROVE.
  • CI: GitHub Actions suite passed on both runs for d39ff4ccd: 36738016783 and 36738091058.

For the reviewbot (PR 4b)

  • Construct each StrictRuntime with StrictRuntimeConfig(max_workers=<cap for that backend>).
  • Check the handshake: capabilities().strict_api_version >= 1.4.0 and max_strict_workers == requested. Refuse to start otherwise; an older pinned copilot silently runs 1.
  • Keep admission in the reviewbot ledger at ≤ max_strict_workers per host. The copilot queues any excess, and it serializes same-PR runs regardless.
  • Bump deploy/infermatrix-release.sha to a commit containing this.

🤖 Generated with Claude Code

RunService drained its queue with one worker thread, so a host that
reported max_strict_workers=1 could never run two Strict reviews at once,
and the reviewbot's per-backend concurrency caps could not take effect.

- Settings.strict_max_workers (STRICT_MAX_WORKERS, 1-32, default 1) and
  StrictRuntimeConfig.max_workers start that many workers; the handshake
  reports the configured number. Strict API 1.3.0 -> 1.4.0.
- RunQueue never hands out two runs with the same conflict key (resolved
  checkout + PR; issue tasks key on the checkout alone). Runs on one PR
  head share one PR-time worktree and cursor writes its tool-bridge config
  to a fixed path inside it. A busy key keeps its run queued in order
  without holding a worker. A malformed request cannot kill a worker. The
  checkout is resolved through RepositoryContextResolver (frozen path,
  REPO_PATHS, adapter manifest), the same resolver execution uses.
- A PR run whose worktree cannot be materialized degrades to the live
  checkout at runtime, which the queue key cannot see. The cursor
  transport, the only one that writes its bridge config into the session
  cwd, now serializes bridge-carrying sessions in a shared checkout with
  an flock in that checkout's git dir (across services and the CLI too);
  a timeout returns a loud refusal + capability_gap.
- The reaper is single-flight per service, and a reused worktree is
  touched under its materialize lock while the reaper re-reads mtime
  under its own lock, closing the gap between materialize releasing the
  tree and the run taking its shared hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 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-30T15:43:07.876079Z d39ff4c 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.

@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: d39ff4ccde

ℹ️ 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 on lines +326 to +328
if (isinstance(self.max_workers, bool)
or not isinstance(self.max_workers, int)
or self.max_workers < 1):

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 Reject worker counts above the service limit

When an SDK caller supplies max_workers > 32, this public configuration object accepts it, but StrictRuntime(config=...) immediately fails later when Settings.strict_max_workers enforces the 32-worker limit. This exposes an accepted-but-unusable SDK value and raises a raw Pydantic ValidationError instead of the SDK's stable InvalidRequestError; validate the same upper bound here.

Useful? React with 👍 / 👎.

@tzhouam
tzhouam merged commit 30f351f into main Sep 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant