fix(agents): make SSH-port allocation atomic so concurrent creates cannot collide (#2215) - #2235
Conversation
…on (#2215) get_next_available_port() was an unlocked check-then-act: two concurrent creators (the fresh-install seeders under --workers 2) computed the same max+1 and the second containers.run failed 'port is already allocated'. - _existing_agent_ports_strict(): the allocator's label scan now RAISES on a Docker listing fault instead of degrading to the empty set (which would allocate 2222 over an existing fleet); demo mode (no client) keeps the empty set. - exclude= parameter, merged before BOTH scan loops (forward + fallback). - per-candidate SETNX reservation port_alloc:{port} EX 600 as the LAST gate: contention => next candidate; a raised Redis error fails open (returns the candidate unreserved — crud's D2 bind-retry converges the collision). TTL-only self-heal, no release: the container label becomes the durable truth (Invariant #11). - reserve_port_for_recreate(): SET no-NX, fail-open — re-asserts a recreating agent's own port across the remove->create gap. - is_port_available(): netns-blindness documented (weak filter, kept). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2215) The D1 port reservation is fail-open (Redis down => today's racy allocation), so a same-port assignment must converge at bind time instead of failing the create — which, during first-run seeding, permanently latches a 'partial' default fleet (the observed #2215 incident shape). - auto_allocated_port captured BEFORE the config.port mutation (captured after, the retry gate is always-False and ships dead). - _is_port_bind_conflict: narrow two-phrase duck-typed classifier ('port is already allocated' / 'address already in use'; never the generic 'conflict'), cycle-guarded cause/context walk. - _run_agent_container_with_port_retry: <=3 attempts, auto-allocated ports only; per-attempt order = record failed port -> cleanup husk -> reallocate with exclude= -> retry; WARNING per retry. - _cleanup_bind_failed_container: runs on EVERY bind-classified attempt (the final one included, so no husk reaches the generic reclaim); ownership + provenance gated; ANY doubt aborts retries and re-raises the ORIGINAL bind error (never the cleanup error). Direct containers.get with duck-typed 404 so a lookup FAILURE aborts instead of reading as 'absent'. - D2b: the ent#313 reclaim's no-handle branch classifies bind-conflict BEFORE the name-conflict decline — Docker Desktop's bind phrasing contains the name-conflict substring, and the old order stranded the Created husk (and let Cornelius's 409-convergence burn the durable seed flag on a dead container). All three fail-closed gates preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recreate_container_with_updated_config reuses the old container's labeled
SSH port, but between container_remove and containers_run that port is
invisible to the allocator's label scan AND unreserved — a concurrent
creation computing max+1 lands exactly there when the recreating agent
holds the fleet's max port. reserve_port_for_recreate (SET no-NX, EX 600,
fail-open) closes the gap with the same port_alloc:{port} keyspace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilure (#2215) D3: the two seeders held DIFFERENT SETNX locks, so worker A's slow Cornelius clone ran concurrently with worker B's default-system deploy — the concurrent burst that exposes the port race and any other boot transient (SQLite BUSY, 60s Docker read timeout), each of which latches 'partial' permanently. ensure_first_run_seeded() now takes ONE pass-level lock (first_run_seed:provision, SETNX, TTL 900s, #1919 token hygiene: uuid token + lock_token_matches compare-and-delete in finally). The loser skips the WHOLE pass and writes no flags; fail-open on Redis down; inner seeder locks kept unchanged as belts; the pre-setup deferral flow releases promptly. D4 (AC#3): a failed Cornelius creation was log-only. The orchestrator now captures the seeder result and raises a system-seed-cornelius-failed operator-queue alert on create_failed/create_blocked AND from the belt-except (a raise despite never-raises). Reserved-prefix deterministic id; context = sanitized message + logs pointer; flag stays unset so next boot retries (#1790 asymmetry untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- test_2215_port_allocation.py (16): deterministic same-snapshot regression (two sequential calls -> distinct ports), threaded race belt (8 workers, bounded barrier, fakeredis), reservation-as-last-gate, SETNX contention, exclude in both scan loops, EX 600 on every reservation, Redis-exception fail-open (single attempt), client resolved once per call, Docker-fault raises, demo mode, Redis-down convergence-via-exclude, reserve_port_for_recreate (no-NX overwrite + fail-open). - test_2215_create_port_retry.py (25): classifier vocabulary + the documented Desktop-phrasing overlap pin, per-attempt cleanup->realloc order, final- attempt cleanup, abort-and-reraise-ORIGINAL on any cleanup doubt (ownership row / lookup failure / provenance / removal failure), lookup-404 = clear, non-bind + pinned-port never retry, <=3 attempts with accumulating exclude, D2b reclaim path (bind reclaims under gates; pure name-conflict still declines; gates stay fail-closed), AST wiring pins (wrapper awaited, raw create not; auto_allocated_port captured BEFORE the mutation). - test_ent124_default_system_seed.py (+11): pass-lock loser skips the WHOLE pass with no flag writes and leaves a foreign lease; winner runs both seeders and compare-and-deletes only its own token; Redis-error fail-open; cornelius create_failed/create_blocked/belt-except -> system-seed-cornelius-failed row; success/skip actions -> no row. - test_1560_breaker_cleared_on_lifecycle.py: source-shape guard retargeted at the #2215 retry wrapper (the orchestrator's container-create phase call). Also: _is_port_bind_conflict str(exc) made defensive — APIError.__str__ can itself raise on a partial response object, inside the never-raises reclaim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- architecture.md: Redis-keyspace list gains port_alloc:{port} (transient
SSH-port reservation, SETNX + TTL 600s, no release — the container label
becomes the durable truth; SET no-NX by recreate; deliberately not
agent:*) and first_run_seed:provision (pass-level seed lock, token +
compare-and-delete, TTL 900s, fail-open); docker_service /
system_seed_service / cornelius_agent_service service entries gain the
matching clauses incl. the precise Redis-down guarantee boundary.
- requirements/roadmap.md 16.5.1: the pass-level lock over both seeders;
honest-status now covers a failed Cornelius seed
(system-seed-cornelius-failed).
- feature-flows/cornelius-default-agent.md: the operator alert + the
pass-level lock in Error Handling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#2215) Review finding on the #2215 F4 "fail-loud allocator" decision: making `get_next_available_port()` raise on a Docker listing fault (instead of degrading to the empty set and handing out 2222) moved the failure from INSIDE the docker try-block to BEFORE it. `_resolve_template` has already written the `agent_git_config` working-branch reservation for a `github:` template by then, and only `_rollback_failed_creation` (inside the try's except) deletes it — so a listing fault at allocation stranded the row, after which every later create of that name fails `reserve_and_generate_instance_id: agent_git_config already exists` (Cornelius's next-boot retry included: a permanent seed failure by a new route). Pinned by a probe test that failed on the previous commit (raw RuntimeError, no rollback). Move the `auto_allocated_port` capture + allocation into the try, immediately before `_run_agent_container_with_port_retry`. `config.port` is consumed only by `_create_agent_container` (label + ports map), so nothing between the old and new sites needs it; the move also shrinks the reserved window (the ent#15 snapshot prepopulate and the volume builds now run BEFORE the port is reserved), which the docker_service TTL comment now states accurately. - crud.py: allocation inside the docker try; comment explains both reasons - docker_service.py: TTL-adequacy comment updated for the new window - test_1484 case 22: allocator raise -> git-config + MCP-key rollback, no run - feature-flows/agent-lifecycle.md: allocator step/table lines refreshed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2215) /cso finding on the new `system-seed-cornelius-failed` operator alert: its `context.message` carried the seeder's `create_failed` text verbatim, which is `str(exc)` from a `github:` create — one that resolves the platform PAT when an admin has configured it — and git/GitHub errors can embed PAT-bearing remote URLs (learnings 2026-07-14). The deploy report already redacts at its exit point (`system_service._failure_reason`); the operator queue is the same durable, UI-rendered class. Apply `sanitize_text(redact_url_userinfo(...))` at the alert's exit point; the URL itself survives, only userinfo/secrets go. Pinned by a new ent124 test feeding a PAT-bearing clone URL through the alert path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e lifecycle stub `test_agent_readiness_probe.py` loads `lifecycle.py` in isolation with a hand-enumerated `services.docker_service` stub; #2215 added `reserve_port_for_recreate` to lifecycle's from-import, so every test in the file errored at setup (`ImportError: cannot import name 'reserve_port_for_recreate'`) — the same shape as the #1559/#1809/#1854 entries already in that stub. Add the name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
House convention (#2226 registered its own in the same commit that added them). Both entries sit beside test_ent313_failed_creation_container_reclaim.py — the create-path neighbourhood they extend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two textual conflicts, both resolved as a union — neither side's behaviour changed: * docker_service.py — the `typing` import line only. #2196 added `Dict` (its tri-state readers), #2215 added `Set` (the allocator's port set); the merged line carries both. Everything else auto-merged: #2196's agent_container_states/agent_container_state sit above #2215's _existing_agent_ports_strict/_try_reserve_port/reserve_port_for_recreate with no overlap. * architecture.md — both sides appended a clause to the same `docker_service.py` catalog bullet. Kept #2196's tri-state clause first (it landed first), then #2215's allocator clause, plus one sentence naming why the two resolve a Docker fault in OPPOSITE directions: a roster read must degrade to "unchanged", an allocator must never degrade to "allocate over the fleet". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vybe
left a comment
There was a problem hiding this comment.
Validated per /validate-pr. Approved with the deviations read, not waved through.
The defect and the defence-in-depth
The root cause is stated correctly: get_next_available_port() was an unlocked check-then-act whose only real filter — is_port_available — binds inside the backend container's own netns, so host-published agent ports are invisible to it in production. The two first-run seeders hold different SETNX locks, so they were never serialised against each other, and the resulting partial then permanently latches default_system_seeded. Ports were the symptom; the missing pass-level lock was the disease, and D3 is the right place to fix it.
The guarantee boundary is stated honestly and belongs in the docstring. With Redis up the SETNX is the atomic arbiter; with Redis down a same-port assignment is convergent-under-retry, not prevented. Since every lock here fails open on one shared client, D1/D3 and both inner locks degrade together and D2 alone stands in that mode — which is exactly why D2 must not later be deleted as redundant. Good that this is in the source and not only the PR body.
Reviewed in detail
- D1 ordering is right: the reservation is the last gate, after the label scan and
is_port_available, so a host-bound port never holds a 600s reservation.excludemerged once before both loops binds the 2222–2500 fallback too. SETNX contention → keep scanning; only a raised Redis error fails open. _existing_agent_ports_strictraising instead of reusinglist_all_agents_fastis the correct inversion: a[]degrade computesstart_port = 2222and confidently reserves over the live fleet, which no bounded retry can outrun. Note this makesrecreate_missing_containerand the system-agent bootstrap fail loud too — intended, and consistent with #2196's stated rule that a roster read degrades to unchanged while an allocator must never degrade to allocate over the fleet._cleanup_bind_failed_containerfails closed on every doubt (ownership row, lookup failure, unprovable provenance, removal failure) and re-raises the original bind error, not the cleanup error — correct, since proceeding after a failed removal makes attempt N+1 409 against our own husk which the reclaim then reads as "not ours". The duck-typedcontainers.getoverget_agent_containeris justified: that helper flattens failure into absent, and here absent means safe-to-retry.- Husk cleanup on the final attempt too — the detail that keeps a bind husk from reaching the generic ent#313 reclaim still on the name.
auto_allocated_portcaptured before the mutation — captured after, the gate is always-False and the whole retry ships dead. Worth the comment it got.- D3 release is compare-and-delete with a unique token per #1919, in a
finally, fail-open on acquire._notify_cornelius_failuresanitizes + redacts URL userinfo at the exit point, which is right for a durable UI-rendered surface fed bystr(exc)from a PAT-resolvinggithub:create.
Flagged deviations — reviewed, both accepted
- D2b touching ent#313 reclaim. Accepted.
_is_container_name_conflict's text fallback matches the bare substring"already in use", which Docker Desktop's bind failure contains — so a bind failure withcontainer=Nonewas misclassified, the reclaim declined, and for Cornelius_agent_is_presentcounts the husk, 409-convergingcornelius_seededonto a dead container. That is a live pre-existing bug squarely in this issue's blast radius, and the change is a condition amend only: all three fail-closed gates (409, ownership row, floor-ts provenance) are byte-identical and_rollback_failed_creationis untouched. Tightening the substring instead would have been riskier and non-additive — agreed. - Allocation moved inside the docker try/rollback fence. Accepted, and it is a correction rather than a liberty: with D1 now raising, a pre-
tryraise strands theagent_git_configworking-branch reservation_resolve_templatealready wrote, after which every later create of that name fails permanently — Cornelius's own next-boot retry included. Verified the claim that nothing between the old and new call sites readsconfig.port. - The two defensive shapes (
str(exc)in its own try; duck-typed lookup) are not over-caution — the first sits inside a never-raises reclaim where a raise would replace the error being reported.
Verification
All 24 non-skipped checks green, including all six pytest legs and regression diff. The seed-99999 pair that initially showed red were cancelled at an identical timestamp, not failed; I re-ran them and both pass. prod-image-smoke, verify-non-root, gitleaks, both CodeQL analyses green.
Scan: the one ghp_-shaped string is ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ab, a synthetic fixture in the test that asserts it is redacted — not a finding. No host paths, no mode/symlink changes, no new env var, no new top-level src/backend/ package, no schema change (so schema-parity/Alembic correctly N/A). Merges clean into current dev after #2234/#2237/#2233.
Honest gaps, noted not blocking
The agent_git_config stranding scenario and the "exactly one #2069 gitignore-merge task" claim rest on unit coverage because the dev .env GITHUB_PAT is expired; D2b's "address already in use" branch is unit-only on this daemon. Both are disclosed rather than papered over, and the empirical port-squat harness results (2233 → 2234, the 2/3 → 3/3 retry ladder with no 4th attempt, husk removed every attempt, recreate holding 2232 with TTL 309 → 590) cover the load-bearing paths on real Docker.
Fixes #2215
What changed and why
get_next_available_port()was an unlocked check-then-act. It read existing agent SSH ports from container labels, filtered them throughis_port_available()— which binds inside the backend container's own network namespace, so host-published agent ports are invisible to it in production — and returned. The caller then rancontainers.runup to ~100 lines and several seconds later.At first boot with
--workers 2the two first-run seeders hold different SETNX locks (cornelius:provision/system_seed:provision), so worker A's Cornelius create (slow anonymous GitHub clone) runs concurrently with worker B's default-system deploy. Both compute the samemax(existing_ports) + 1; the secondcontainers.runfailsBind for 0.0.0.0:2227 failed: port is already allocated. The ent#313 reclaim removes the leaked container, but the agent is never created — and the system-seed flag policy then permanently latchesdefault_system_seededon the resultingpartial, freezing the default fleet incomplete.Four changes, in the order they defend:
D1 — atomic allocation (
services/docker_service.py). A per-port Redis reservation,SET port_alloc:{port} "1" NX EX 600, is the last gate — after the label scan and afteris_port_available, so reservations never leak onto candidates the scan walks past. SETNX contention means a concurrent allocator holds that candidate, so the scan continues; only a raised Redis error fails open (warn, return the candidate unreserved, stop attempting reservations). No release and no refresh: once the container exists itstrinity.ssh-portlabel is the durable truth (Invariant #11), so the reservation is transient state bridging check→run, never a port registry — which also means there is no lease anything could release, so the #1919 compare-and-delete bar is N/A by construction. The label scan is now strict (_existing_agent_ports_strict): a Docker listing fault raises instead of degrading tolist_all_agents_fast's[], which would computestart_port = 2222and confidently reserve an existing agent's port.exclude=is merged once, before both scan loops, so it binds the forward scan and the 2222–2500 fallback.D2 — bounded bind-conflict retry (
services/agent_service/crud.py). D1 is fail-open, so a collision can still surface atcontainers.run— Redis down or restarted, an expired reservation, a foreign host process._run_agent_container_with_port_retrywraps_create_agent_containerfor ≤3 attempts. On a bind-classified failure, in strict order: record the failed port → clean up the leaked Created husk (on every bind-classified attempt, the final one included) → if attempts remain,get_next_available_port(exclude=attempted_ports)and retry. Any cleanup doubt — ownership row present, lookup failure, unprovable provenance, removal failure — aborts and re-raises the original bind error, never the cleanup error: proceeding after a failed removal makes attempt N+1 409 against our own husk, which the reclaim then reads as "not ours" and strands. Gated on the port having been auto-allocated, captured before the mutation, so a caller-pinned port never silently moves.D2b — reclaim classifier precedence (
crud.py, 3 additive lines). See "Flagged deviations" below.D3/D4 — pass-level seed lock + Cornelius alert (
services/system_seed_service.py). Onefirst_run_seed:provisionlock (SETNX, uuid token, TTL 900s, compare-and-delete release in afinally, fail-open) over the whole first-run pass; the loser skips the entire pass without touching any flag. Ports were never the only cross-seeder hazard — SQLite BUSY under concurrent boot writes is anticipated in this very file, the 60s Docker read timeout is in ent#313's own docstring, and any transient during the burst produces the same permanently-latchedpartial. AC#3's genuine gap was that a failed Cornelius create was log-only; the orchestrator now captures the result and emitssystem-seed-cornelius-failedoncreate_failed/create_blockedand from the belt-except, credential-sanitized and URL-userinfo-redacted at the exit point.Recreate-gap pre-reservation (
services/agent_service/lifecycle.py, 2 lines).recreate_container_with_updated_configreuses the labeled port across its remove→create gap, during which the port is invisible to the allocator and unreserved — and the most-recreated agent tends to hold the fleet's max port, exactly what a concurrent allocation computes asmax+1.reserve_port_for_recreate(port)(SET without NX — it is that agent's own port — fail-open) re-asserts it before the old container's removal.No schema change, no migration on either track, no new env var, no compose change, no new endpoint or MCP tool.
AC#1 guarantee boundary — read this before building on it
With Redis up, two concurrent creations structurally cannot be assigned the same port: the SETNX is the atomic arbiter.
With Redis down, a same-port assignment is convergent-under-retry, not prevented: it is detected at bind time and resolved within the same create call by D2. Every lock in this codebase deliberately fails open and they share one client, so D1, both inner seeder locks and the D3 pass lock all degrade together — D2 alone stands in that mode.
This boundary is in the allocator's docstring verbatim so nobody later reads "atomic" as unconditional and deletes D2 as redundant.
Flagged deviations — please do not re-litigate these blind
The D2b guard edits ent#313 reclaim code. The plan's dossier said "reclaim untouched"; this is a deliberate, pre-authorized 3-line deviation (plan §8 audit row 10, demanded independently by both
/autoplanvoices at HIGH)._is_container_name_conflict's text fallback matches the bare substring"already in use"— and Docker's userland-proxy bind failure,"… listen tcp 0.0.0.0:X: bind: address already in use"(the standard phrasing on Docker Desktop, this repo's primary dev platform), contains it. So a bind failure reaching_reclaim_failed_creation_containerwithcontainer=Nonewas misclassified as a name conflict, the reclaim declined, the Created husk squatted the name, every retry 409'd — and for Cornelius_agent_is_presentcounts the husk, so the next boot 409-converges the durablecornelius_seededflag onto a dead container: the exact unrecoverable latch Cornelius seeder marks itself seeded on a volume-conflict 409 — Cornelius never created, never retried #1790 exists to prevent. A live pre-existing bug, squarely in this issue's blast radius. The change is a condition amend only:if _is_container_name_conflict(exc) and not _is_port_bind_conflict(exc). All three fail-closed gates (409, ownership row, floor-ts provenance) are preserved byte-for-byte, and_rollback_failed_creationis untouched. Tightening_is_container_name_conflict's substring instead was considered and rejected as riskier and non-additive.Port allocation MOVED inside the docker try/rollback fence during review. The plan had the allocation staying where it was (pre-try). It could not: D1's allocator now fails loud on a Docker listing fault, and a raise before that
trystrands theagent_git_configworking-branch reservation_resolve_templatealready wrote — after which every later create of that name failsagent_git_config already exists, permanently, Cornelius's next-boot retry included. So the allocation sits immediately beforecontainers.run, inside the fence. Second benefit: the 600s reservation TTL no longer has to cover config staging, the MCP-key mint, env build, the ent#15 snapshot prepopulate and the volume builds. Verified that nothing between the old and new call sites readsconfig.port— it is consumed only inside_create_agent_container(label + ports map, rebuilt per call), and there is no port column anywhere inagent_ownership.Two defensive shapes that look like over-caution and are not. (a)
_is_port_bind_conflictwrapsstr(exc)in its own try —docker.errors.APIError.__str__dereferences response attributes and can itself raise on a partial or stubbed response object, and this classifier runs inside the never-raises reclaim, where a raise would replace the creation error being reported (the ent#313isinstanceregression, one door over). (b) Cleanup uses a duck-typeddocker_client.containers.getrather than the namedget_agent_containerhelper: that helper flattens a lookup failure into "absent", and here absent means "safe to retry" while failure must abort.Verification
Empirically proven on real Docker (dev stack, port-squat harness):
/healthhealthy.port_alloc:*keys carry TTL ≤ 600 and decay as expected.reserve_port_for_recreate's no-NXSET … EX 600can do.WAVE-4 FULL
/verify-local: branch PASS. The harnessresult.jsonreported fail atagent-exercise, root-caused across 4 reproductions including a fresh VM to a Docker Desktop 4.36.0 / Engine 27.3.1 bug:docker network connectagainst a running container with an ephemeralHostPort: "0"silently drops the host binding (gvisorwrite unixgram: destination address required). The app was healthy in-container throughout; the equivalent checks were driven manually and passed. Not branch-related.Honest gaps — two claims rest on unit coverage, not end-to-end:
agent_git_configstranding scenario (deviation 2) and "exactly one bug: the fleet-wide .gitignore merge never runs at agent creation — in-container auto-sync commits .trinity/ runtime state before any Push can migrate #2069 gitignore-merge task fires against the final container" were not driven end-to-end, because the dev.envGITHUB_PATis expired — template metadata reads 401, sogithub:creates are refused before reaching either path."address already in use"branch is unit-only: this daemon emits"port is already allocated". The"address already in use"phrasing is covered by the classifier disjointness pin, not by a live daemon.Tests (
-p no:randomly, affected-file neighbourhood):tests/lint_sys_modules.py→ exit 0, no new violations (both new files usemonkeypatch.setitem).origin/devmerged in (2 conflicts, both resolved as a union)devmoved 5 commits after this branch was cut, soorigin/devis merged in rather than left conflicting. Neither side's behaviour changed:docker_service.py— thetypingimport line only. bug: Workspace roster lists agents with no container (DB-sourced roster vs Docker-as-truth) #2196 addedDict(its tri-state readers), this branch addedSet(the allocator's port set); the merged line carries both. Everything else auto-merged — bug: Workspace roster lists agents with no container (DB-sourced roster vs Docker-as-truth) #2196'sagent_container_states/agent_container_statesit above_existing_agent_ports_strict/_try_reserve_port/reserve_port_for_recreatewith no overlap (verified by AST: no duplicate top-level definitions in either merged file).architecture.md— both sides appended a clause to the samedocker_service.pycatalog bullet. bug: Workspace roster lists agents with no container (DB-sourced roster vs Docker-as-truth) #2196's tri-state clause is kept first (it landed first), then bug: fresh-install seeding races itself across workers — two seeders allocate the same agent SSH port #2215's, plus one added sentence naming why the two resolve a Docker fault in opposite directions: a roster read must degrade to unchanged, an allocator must never degrade to allocate over the fleet. Worth a reviewer's eye — it is the one place the two changes make a claim about each other.Post-merge re-run of the same six files: 155 passed. The now-shared
docker_service.pyneighbours (test_2196_roster_availability.py,test_docker_service_list_fast.py,test_1560_agent_redis_key_parity.py,test_1560_breaker_cleared_on_lifecycle.py,test_ent107_cornelius_seed.py): 77 passed.Pre-existing reds on
dev— not from this PRThe full unit suite under
-p no:randomlyis 8 failed / 10466 passed, and all 8 reproduce onorigin/devitself:tests/unit/test_ent96_timeline_split.py— hardcoded2026-08-14bucket keys, now in the past.tests/unit/test_pat_propagation_properties.py::TestKnownGaps— stale strict-xfail; bug(github-pat): a backslash in the token raises re.error mid-rotation; .env quote-escaping is write-only #2017 fixed the bug the xfails describe, so they now XPASS and fail strictly.pytest-randomlyis installed in this venv, so always compare with-p no:randomly.backend-unit-testshould still go green on this PR, and that is not the check being lax: it is a base-vs-head differential (the suite under three seeds on both the base tip and the head merge commit, failing only on a test ID that is newly failing on head). All 8 fail identically on both sides, so they diff away. Worth knowing while reading dev's own history: on apushthose diff jobs are skipped by design (if: github.event_name != 'push'), so a greenbackend-unit-teston a dev push means the invariant lint ran — not that the suite did.Follow-ups (listed, deliberately not filed)
TestKnownGapsstrict-xfails need retiring.agent-<name>-workspace. The volume outlives the reclaimed container, so an immediate same-name retry 409s until the bug: Docker volumes are never deleted — volume_remove has zero callers; agent purge leaks workspace/public/shared volumes forever #1581 orphan-volume sweep reclaims it (~15 min, 3 unattached strikes). This partially blunts the "Cornelius retries on next boot" benefit when the failure lands late in creation.cornelius:provisionandsystem_seed:provisionstillDELETEunconditionally, so a TTL lapse can release a sibling's live lease. Out of scope here, kept as belts._release_pass_lock's GET→DEL is non-atomic. Correct compare-and-delete needs a Lua CAS (theredis_breaker_utilscript-cache pattern); the current form narrows the window rather than closing it.agent-exerciseblocker makes/verify-localunable to complete its final phase on macOS Desktop until 4.36.0/27.3.1 is worked around or upgraded past.Review pipeline
/autoplan(strategy + engineering voices, both approve-with-changes) +gemini-critic(approve) on the plan;/reviewon the branch (deviation 2 originated there); WAVE-4 FULL/verify-local;/validate-prpre-push. No new endpoints, WS channels, MCP tools, auth-boundary changes, or credential paths —/cso --diffskipped per the pipeline table for a non-auth bug fix.🤖 Generated with Claude Code