Fix #2399: push pipeline tip to <branch>/work so slice refs coexist - #2402
Conversation
Git rejects coexisting refs at <branch> (leaf) and <branch>/slice-N (child) on origin with `directory file conflict`. The orchestrator was creating both — the pipeline branch as a leaf at egg/<id> and slice integration branches under egg/<id>/slice-N — so every slice DAG submission failed at slice creation time. Push the pipeline tip to egg/<id>/work instead. The <id>/ namespace becomes a directory holding /work and /slice-N as siblings, mirroring the existing per-container worktree convention (egg/<container_id>/work). This also resolves #2371's latent fallback shape — egg/<id>/work becomes the canonical pipeline ref. Implementation: - _ensure_pipeline_work_ref normalises egg/-prefixed branches at pipeline submission. Idempotent; skips BABYSIT (PR head refs are not orchestrator-owned) and non-egg branches. - _slice_namespace_root strips the /work suffix to derive the prefix slice paths build from. Used in the slice loop and the reconciler. - concurrent_executor's slice-branch helpers strip /work before embedding in egg/<id>/slice-M. - stacked_pr_reconciler splits issue_branch into slice_namespace_root (slice path prefix) and pipeline_branch (cascade fallback). - New regression suite test_pipeline_branch_namespace.py pins the coexistence property: pipeline tip and slice paths must not be prefixes of each other. Operational note: pipelines whose egg/<id> leaf was already pushed to origin under the old shape will need that ref deleted before retrying under the same pipeline_id, or they should retry under a fresh id.
This comment has been minimized.
This comment has been minimized.
PR #2399 normalises the pipeline tip to <branch>/work via _ensure_pipeline_work_ref so slice integration branches at <branch>/slice-N can coexist as siblings. Six test assertions still expected the bare egg/<id> shape: - test_contracts_routes.py: branch-fallback git-show path now uses origin/egg/<pipeline_id>/work - test_pipelines_api.py: 5 create-pipeline tests assert the normalised branch flows through to create_pipeline / 409 error details
Autofix tracking{"Test/Unit Tests": 1, "Lint/Python": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review of #2402 — push pipeline tip to <branch>/work
The core fix is correct: normalising egg/<id> → egg/<id>/work at the HTTP-route level keeps the slice integration branches egg/<id>/slice-N as siblings rather than children, which is exactly what's needed to avoid git's directory/file-conflict rejection. The two helpers (_ensure_pipeline_work_ref, _slice_namespace_root) are tightly scoped and idempotent. The reconciler split into slice_namespace_root + pipeline_branch is a clean way to preserve the qualifier through contract_key while still returning the actual remote ref for the cascade-fallback. Unit coverage in test_pipeline_branch_namespace.py pins the design property well.
That said, there are a handful of stale-doc and parallel-entry-point gaps the PR should clean up while it's already in this area. None are blocking on functionality, but several will mislead future maintainers or silently break alternate creation paths.
Non-blocking issues
1. orchestrator/cli.py:432 bypasses _ensure_pipeline_work_ref.
branch=args.branch or f"egg/issue-{args.issue}",cmd_pipelines_create calls store.create_pipeline(...) directly with the legacy egg/issue-N shape, completely bypassing the HTTP route's normalisation. Anyone who uses egg-orch pipelines create to provision a pipeline will end up with pipeline.branch == "egg/issue-N" persisted in state. Once the orchestrator picks it up, _run_implement_phase_slices will compute _slice_namespace_root("egg/issue-N") == "egg/issue-N" (no /work to strip) and slice creation will hit the exact directory file conflict this PR is meant to fix. Either:
- update the default to
f"egg/issue-{args.issue}/work", or - have
cmd_pipelines_createroute through_ensure_pipeline_work_ref.
The same goes for the --branch help text on line 959 ("default: egg/issue-N").
2. Stale docstring on _sync_worktree_with_remote (routes/pipelines.py:5304-5314).
pipeline_branchis the remote branch name to reconcile against (e.g.egg/<pid>). The orchestrator-side worktree is checked out on a/work-suffixed local branch (egg/<pid>/work) that does not exist on origin — agents push toegg/<pid>.
After this PR, agents push to egg/<pid>/work and that is what's on origin. The local-vs-remote split this docstring describes is exactly the shape the PR is removing. Callers still pass pipeline.branch and the function still does the right thing, but a maintainer reading this docstring will end up with the wrong mental model.
3. Stale docstring on _cleanup_remote_branches (routes/pipelines.py:2111-2113).
Deletes the pipeline's shared branch (
pipeline.branch, typicallyegg/{pipeline_id}) ...
Should now say egg/{pipeline_id}/work. Cosmetic, but in the file the PR is editing.
4. Cleanup leaves slice integration branches behind (pre-existing).
_cleanup_remote_branches deletes only pipeline.branch and per-container egg/<container_id>/work. After this PR, pipeline.branch is egg/<id>/work and slice integration branches egg/<id>/slice-N are siblings — they're orphans of pipeline cleanup. This was true before too, but the new namespace structure makes it more visible: egg/<id>/ will accumulate orphan slice paths after each cancellation. Worth a follow-up to enumerate egg/<id>/slice-* and best-effort delete them.
5. Branch-existence check at submission time does not detect the legacy-egg/<id> collision.
The operational note correctly states that pipelines whose old egg/<id> leaf is still on origin will need that ref deleted before resubmission under the same pipeline_id. But the orchestrator could surface this proactively: after _ensure_pipeline_work_ref, the existence check at line 1473 looks up egg/<id>/work only. If that doesn't exist but the legacy egg/<id> does, the check passes silently and the pipeline starts — then the first push fails with a confusing directory file conflict from git rather than a clean 409 from the orchestrator. Adding a parallel check for the legacy leaf and returning a targeted error ("legacy <branch> still on origin — delete and retry") would dramatically improve the operator experience for the migration window.
6. _ensure_pipeline_work_ref endswith("/work") heuristic is fragile at the edges.
_ensure_pipeline_work_ref("egg/work")returns"egg/work"unchanged because"egg/work".endswith("/work")is true. If a CUSTOM pipeline ever lands on a literalegg/workbranch, the slice DAG would push toegg/work/slice-Nunderegg/work— back to the directory/file conflict. Consider keying on a deeper structural check (e.g.branch.count("/") >= 2 and branch.rsplit("/", 1)[1] == "work")._ensure_pipeline_work_ref("egg/")returns"egg//work". The branch validation regex atpipelines.py:1387allows trailing slashes; consider stripping or rejecting upstream.
These are degenerate inputs and unlikely in practice, but the helper is a security/correctness boundary and worth tightening.
7. _ensure_pipeline_work_ref skip for non-egg/ includes CUSTOM mode (not just BABYSIT).
The docstring frames the non-egg/ passthrough as the BABYSIT case ("e.g. babysit PR head refs"), and the test comment at test_passthrough_for_non_egg_branch says the same. But the route-level call only skips BABYSIT (if mode != PipelineMode.BABYSIT), so a CUSTOM-mode pipeline submitted with a non-egg/ branch (e.g. feature/foo) reaches the helper, gets passed through unchanged, and would hit the same directory/file conflict if its contract has slices. CUSTOM-with-slices is rare today, but the helper's claim of being "safe across modes" is wider than its actual guarantee. Worth either narrowing the docstring or rejecting non-egg/ branches earlier in the CUSTOM-mode path.
8. Reconciler pipeline_branch is hardcoded rather than read from pipeline.branch.
slice_namespace_root = f"egg/{contract.contract_key}"
pipeline_branch = f"{slice_namespace_root}/work"For all pipelines that went through _ensure_pipeline_work_ref, this produces the right ref. But it's a separate source of truth from pipeline.branch. If a future change makes pipeline.branch diverge from egg/<contract_key>/work (e.g., a pipeline whose branch wasn't normalised — see issue 1 above), the reconciler would silently rebase orphans onto a non-existent ref. Long-term, threading pipeline.branch into find_orphaned_child_prs (or asserting they match at the call site) would be more defensible than the parallel construction.
Test plan gap
The PR description's checklist shows the end-to-end test (issue-2261-v7) is unchecked. The unit tests cover the helper invariants but don't exercise the actual push path against GitHub's ref storage — that's where the original failure manifested. Worth running the e2e before merge so you don't ship the third revision in the #2367 → #2399 chain on unit tests alone.
What's good
- The split of
_resolve_extant_new_base'sissue_branchintoslice_namespace_root+pipeline_branchis the right shape — keeps the lookup-key construction (/<slice_id>is appended to the root) separate from the cascade-fallback target (the actual remote ref). test_strips_only_trailing_workcorrectly catches the/workmid-path case.TestNamespaceCoexistenceis a good design-property test — pinning that neither ref is a prefix of the other is exactly the invariant that breaks if someone accidentally re-introduces the old shape.- Idempotency of
_ensure_pipeline_work_ref(already-normalised inputs return unchanged) means resubmission paths and internal callers can both invoke it without coordination.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…NE_ID
The slice spawn path was overwriting EGG_PIPELINE_ID with
"{pipeline_id}/{slice_id}" so the orchestrator's _tracker_key would
route CONSENSUS_* to the per-slice tracker without an extra signal-
level field. That broke every agent → orchestrator round-trip:
- state_store.PIPELINE_ID_PATTERN and the agent handler validator
([a-zA-Z0-9_-]+) both reject the slash;
- Flask's default URL converter doesn't allow /, so every
POST /api/v1/pipelines/{pid}/... route 404s — i.e. all of
progress, BRC, heartbeat, message, phase, decision endpoints.
Slice routing is now plumbed explicitly: spawn sets EGG_SLICE_ID and
leaves EGG_PIPELINE_ID as the canonical pipeline id. Sandbox BRC
handlers (propose/ack/nack/confirm/resolve_obligation) and the
egg-orch consensus withdraw CLI forward slice_id on the signal body;
the orchestrator's signal handlers feed it into
get_peer_consensus_tracker(pipeline_id, slice_id) so per-slice
CONSENSUS_* still lands on the slice's tracker. HEARTBEAT /
OVERSEER_ALERT keep flowing through the pipeline-scoped tracker.
Tests pin the new wire shape on both sides:
- orchestrator/tests/test_slice_signal_routing.py asserts the spawn
env shape and tracker-lookup forwarding;
- sandbox/tests/test_brc_slice_routing.py asserts each BRC handler
attaches slice_id from EGG_SLICE_ID, including override/validation
semantics.
This comment has been minimized.
This comment has been minimized.
- cli.py cmd_pipelines_create routes through _ensure_pipeline_work_ref so CLI-provisioned pipelines get the same /work normalisation as the HTTP route (review issue 1). - _sync_worktree_with_remote and _cleanup_remote_branches docstrings reflect the post-#2399 namespace shape (issues 2, 3). - _ensure_pipeline_work_ref + _slice_namespace_root use a structural /work check (>=2 slashes, last segment == 'work') so degenerate inputs like 'egg/work' are not mistaken for already-normalised pipeline branches; rstrip trailing slashes to avoid 'egg//work' (issue 6). Mirrored into concurrent_executor.py for consistency. - Docstring narrowed to acknowledge that CUSTOM-mode with non-egg/ branches is also passed through (issue 7). - Added unit tests for the new edge cases.
…c-20260505-211943-3096/work
Add type annotations for _captured_data: annotate mock_request as Any, use dict[str, Any] return type, and wrap return value in dict() to avoid no-any-return.
…c-20260505-211943-3096/work
This comment has been minimized.
This comment has been minimized.
|
Thanks for the thorough review. Disposition per item below; fixes pushed in Per-item disposition
Test plan gapAcknowledged. The fixes here are all unit-testable; the e2e against GitHub's ref storage remains the operator's call before merge. New tests added to Test results
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
When two slices in the same pipeline run concurrently (or back-to-back),
they used to construct the same Job name and the same agent_worktree_id
because both were "{pipeline_id}-{role}". Two consequences:
- spawn_agent_job's pre-spawn cleanup deletes any existing Job with
the matching name, so slice-N's coder spawn would terminate
slice-(N-1)'s still-running coder.
- The gateway worktree key collides too — slice-N reuses (or steps
on) slice-(N-1)'s worktree contents, leaving the agent staring at
an empty mount.
The Job-name builder, agent_worktree_id, and the cleanup_pipeline
filesystem scan now all understand the slice-scoped shape
"{pipeline_id}-slice-{N}-{role}". slice_id flows from
_run_concurrent_phase → create_concurrent_spawn_fn → spawn_agent_job
so retries via spawn_specific_roles inherit it automatically.
Pipeline-level (non-slice) spawns are unchanged.
Out of scope:
- restart_agent_job and detect_uncommitted_changes don't yet take
slice_id; the operator-triggered restart route doesn't carry slice
scope today and that is a separate plumbing change.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
#2199 covers per-slice MCP control verbs (restart_slice, get_slice_status, list_slices, restart_agent slice_id extension); the reconstruction-from- message-store gap for slice-scoped trackers is a separate concern (needs a slice_id field on Message and a filtered replay) and now has its own ticket.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of #2402 — <branch>/work namespace + #2403 slice routing/spawn fixes
Status of v1 review feedback
All eight items from the previous review are properly addressed:
| # | v1 item | Disposition |
|---|---|---|
| 1 | cli.py:432 bypassed _ensure_pipeline_work_ref |
Fixed in b34ba31 — cmd_pipelines_create now imports the helper and the --branch help text is updated. |
| 2 | Stale _sync_worktree_with_remote docstring |
Fixed in b34ba31 — the local-vs-remote split paragraph is rewritten and now matches the post-#2399 shape. |
| 3 | Stale _cleanup_remote_branches docstring |
Fixed in b34ba31 — also calls out that slice integration branches are not swept (links to follow-up). |
| 4 | Cleanup leaves slice integration branches behind | Deferred to #2406 (cross-cuts the gateway client; out-of-scope here). |
| 5 | Branch-existence check doesn't detect legacy egg/<id> collision |
Deferred to #2407 (migration-window UX, not a correctness fix). |
| 6 | _ensure_pipeline_work_ref endswith heuristic fragile |
Fixed in b34ba31 — switched to structural check (count("/") >= 2 and rsplit[1] == "work"), rstrip("/") upstream, mirrored into _slice_namespace_root and the two concurrent_executor.py strip sites. New tests pin both edge cases (egg/work → egg/work/work, egg/issue-1/ → egg/issue-1/work, egg/ → egg). |
| 7 | Non-egg/ passthrough scoping vs. CUSTOM mode |
Fixed in b34ba31 — docstring narrowed to acknowledge CUSTOM-with-non-egg/ is intentionally not normalised here. |
| 8 | Reconciler pipeline_branch hardcoded |
Deferred to #2408 (single-source-of-truth refactor). |
The structural-check cleanup is exactly the right shape. The mirrored helper at concurrent_executor.py:285 and :331 keeps the two source files honest about what "already-normalised" means.
Scope creep — large but justified
The PR has expanded from the namespace fix into two #2403 fixes that are essentially blockers for #2399 working at all:
ad68f8f— slice signal routing via explicitEGG_SLICE_IDinstead of the slashedEGG_PIPELINE_IDshape. Without this, every per-slice agent → orchestrator round-trip 404s (Flask's URL converter and the orchestrator'sPIPELINE_ID_PATTERNboth reject/). The previous shape was non-functional in production.b65c7a3— slice-namespacedJOB_NAME_FORMAT_SLICEand_build_agent_worktree_id. Without this,spawn_agent_job's pre-spawn cleanup deletes a sibling slice's still-running Job (and the gateway worktree key collides too).
Both are correctly fixed and explicitly tested. Bundling them with #2399 is the right call — shipping #2399 alone would have left the slice-DAG submission still broken at the next layer down.
New code review
The orchestrator-side _extract_slice_id (validates ^slice-[0-9]+$), the sandbox-side _maybe_attach_slice_id (env- or req-driven, same regex), and the eight consensus signal handlers (propose, ack, nack, withdraw, confirmed, excuse_producer, resolve_obligation, producer_push) all consistently route through get_peer_consensus_tracker(pipeline_id, slice_id). The sandbox CLI verbs (cmd_consensus_propose / _ack / _nack / _confirmed) delegate to the brc_* handlers, so the env-driven slice_id flows through automatically — only cmd_consensus_withdraw (which doesn't go through a handler) needed the explicit fix, and it got it.
KubernetesSpawner._build_k8s_job_names and ._build_agent_worktree_id correctly embed the slice segment when present, and the cleanup filesystem scan's slice-segment regex preserves the _ROLES_WITHOUT_WORKTREE allowlist guard that prevents the #1865 sibling-pipeline regression.
Non-blocking observations
1. restart_agent_job and detect_uncommitted_changes don't carry slice_id. Acknowledged in the b65c7a3 commit message as out-of-scope. Today the operator-triggered restart route at routes/pipelines.py:2412 doesn't expose slice scope, so the gap is dormant — but if a slice agent ever needs restarting under operator control, the restart will (a) issue a no-op delete_job against the wrong (non-slice) Job name, (b) spawn a fresh non-slice Job that mounts the wrong worktree, and (c) be unable to participate in slice consensus (no EGG_SLICE_ID). Worth a follow-up ticket so the next operator-route extension doesn't silently break.
2. Orchestrator-side slice routing only has unit coverage for propose. test_slice_signal_routing.py has test_propose_routes_to_slice_tracker, test_propose_without_slice_falls_back_to_pipeline_tracker, and test_propose_rejects_malformed_slice_id — but all eight handlers got the same paste-and-modify treatment, and the other seven (ack/nack/withdraw/confirmed/excuse_producer/resolve_obligation/producer_push) aren't pinned by an analogous test. The sandbox-side suite covers the agent-emitter side for five verbs. Sufficient coverage for a refactor, but a future caller adding a ninth signal type would lack a template-by-pattern.
3. Pattern asymmetry: contract Slice.id accepts ^(?:slice|phase)-[0-9]+$; signal handlers accept ^slice-[0-9]+$. The _migrate_phases_to_slices model validator normalises phase-N → slice-N on contract load, so the asymmetry is dormant — every in-memory slice_.id reaching the spawn path is already canonical. But the model-vs-handler regex divergence is worth noting; if anyone constructs a Slice from raw legacy JSON without going through the loader (e.g. a future migration tool), the slice's BRC handlers will reject it as "Invalid slice_id 'phase-N'".
4. Tracker reconstruction guard is the right call but worth surfacing. handle_consensus_confirmed_signal correctly disables reconstruct_tracker_from_messages for slice-scoped trackers (if slice_id is None) because the message store keys on bare pipeline_id. The new comment names #2409 as the tracking ticket. Concretely this means: if the orchestrator restarts mid-slice-BRC, every in-flight slice agent loses its tracker and gets a 404 on the next consensus signal — there's no replay path. Pipeline-level agents are unaffected. The deferred ticket is correctly scoped, and the code's behaviour is the right conservative default (silence beats a false-consensus replay), but the operational consequence for live slice pipelines should be on the #2409 description.
5. The new _run_concurrent_phase comment slightly overstates routing. "HEARTBEAT / OVERSEER_ALERT continue to flow through the pipeline-scoped tracker" — but handle_heartbeat_signal is a no-op (just logs and ACKs, doesn't touch any tracker), and OVERSEER_ALERT goes through the message bus (MessageType.OVERSEER_ALERT), also not via the consensus tracker. The intent — those signals don't get slice-scoped — is correct, but the framing makes it sound like there's a tracker lookup happening that isn't.
What's good
- Defense-in-depth on
slice_id: identical regex on both ends, plus theconcurrent_executor.pystrip-sites do their own re-validation againstslice-[0-9]+before embedding into a git ref. - Spawn-env mutation is on a
dict(sandbox_env)shallow copy — caller's env not mutated (preserves the caller's expectation when the spawn loop iterates multiple slices). get_slice_id() returns None on empty string(line 87 of_gateway.py) — avoids theEGG_SLICE_ID=""ambiguity that bitEGG_PIPELINE_IDcallers historically.- The slice-cleanup filesystem regex
r"^-slice-[0-9]+(-.+)$"plusif slice_match.group(1) in valid_role_suffixeskeeps the role allowlist enforced in the slice-scoped branch — aegg-{pid}-slice-2-maliciousdirectory wouldn't be swept. - All four deferred review items from v1 reference specific tracking issue numbers (#2406, #2407, #2408, #2409) rather than vague "follow-up" promises.
Test plan
The end-to-end push-against-GitHub-ref-storage check (the one that surfaced the original #2399 directory-file conflict) is still unchecked in the PR description. The combination of #2399 + #2403-routing + #2403-spawn-id changes touches the slice-DAG cold path end-to-end; running a fresh issue-2261-v7-style submission before merge would catch the next layer down if any remains, especially given how many integration seams are in flight together.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…e handler tests Fixes three review items inline; the other two are deferred to #2410 and to #2409's comment thread. - Routing comment in _run_concurrent_phase clarified: HEARTBEAT and OVERSEER_ALERT are not tracker-scoped at all (handle_heartbeat is a no-op ACK, OVERSEER_ALERT flows through the message bus), so the prior wording "continue to flow through the pipeline-scoped tracker" was misleading. The intent — those signals don't get slice-scoped — is preserved with accurate framing (review item 5). - _SLICE_ID_PATTERN gains a doc comment noting the deliberate asymmetry with the contract-side Slice.id regex (which accepts legacy 'phase-<N>' for backward compat). The model loader's migration shim makes the asymmetry dormant; the comment explains why and what would happen if a future migration tool bypassed the loader (review item 3). - New tests pin slice-tracker routing for the other seven CONSENSUS_* handlers (ack, nack, withdraw, confirmed, excuse_producer, resolve_obligation, producer_push). Each handler gets a pair: one asserting get_peer_consensus_tracker is called with slice_id when supplied, and one asserting a malformed slice_id is rejected at the boundary by _extract_slice_id (review item 2).
|
Thanks for the thorough re-review. Disposition per non-blocking item below; fixes pushed in Per-item disposition
Test plan gapAcknowledged. The fixes here are unit-testable; the e2e against GitHub's ref storage remains the operator's call before merge. The combined #2399 + #2403-routing + #2403-spawn-id changes are all covered by unit tests against the contract-defined invariants, but the directory/file-conflict failure surfaced through GitHub's ref storage specifically — that layer is only exercised by a fresh Test results
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of #2402 — v3 (22770f5 addresses v2 feedback)
Status of v2 review feedback
The single new commit (22770f5) is a clean, surgical follow-up that addresses all three non-blocking items from the v2 review:
| v2 item | Disposition |
|---|---|
#2 — orchestrator-side slice routing only had unit coverage for propose |
Fixed — two new test classes pin the other seven handlers (ack/nack/withdraw/confirmed/excuse_producer/resolve_obligation/producer_push), both for slice-tracker routing and for malformed-slice rejection at the boundary. |
#3 — pattern asymmetry between contract Slice.id (^(?:slice|phase)-[0-9]+$) and signal handlers (^slice-[0-9]+$) |
Fixed — routes/signals.py:77-88 adds a multi-line note explaining the divergence is intentional, names _migrate_phases_to_slices as the canonicaliser, and calls out that a future migration tool constructing a Slice from raw legacy JSON would be rejected at the signal boundary. |
#5 — _run_concurrent_phase comment overstated routing |
Fixed — routes/pipelines.py:11618-11626 rewrites the HEARTBEAT/OVERSEER_ALERT framing to be accurate. handle_heartbeat_signal is correctly described as a no-op ACK with no tracker lookup; OVERSEER_ALERT is correctly attributed to the message bus (MessageType.OVERSEER_ALERT) rather than the consensus tracker. Verified against signals.py:716-745 (heartbeat handler is just a debug log + ACK) and the four MessageType.OVERSEER_ALERT writes in pipelines.py. |
The remaining v2 items (#1 restart_agent_job/detect_uncommitted_changes slice-id gap, #4 tracker-reconstruction operational consequence) were already deferred to follow-up tickets and are not in scope here.
Test coverage review
The two new test classes are correctly structured:
TestAllConsensusHandlersRouteToSliceTracker — verifies each handler forwards slice_id into get_peer_consensus_tracker(pipeline_id, slice_id). The _slice_arg_from_call helper handles both positional and keyword extraction so a future kwarg refactor wouldn't silently break the assertions. Realistic test data (slice-2, issue-2403, role-correct mock returns).
TestAllConsensusHandlersRejectMalformedSliceId — uses three different malformed shapes across the seven handlers (../etc/passwd, phase-2, slice-2/extra, ../etc), which is a nice anti-paste-and-modify hedge. The legacy phase-2 rejection in test_nack_rejects_malformed_slice_id doubles as documentation of the regex divergence.
I traced through validation order for each test to confirm the rejection actually reaches _extract_slice_id:
- ack/nack/withdraw: reason validation comes first; tests pass substantive (>50 char, non-boilerplate) reasons so validation succeeds and the malformed
slice_idtriggers 400. - confirmed: no pre-
_extract_slice_idvalidation gates, reaches the regex check directly. - excuse_producer: HITL gate (
decision_id+ RESOLVED status +failed_role:codercontext) is mocked to short-circuit cleanly so_extract_slice_idis reached. - resolve_obligation: note validation runs against the relaxed
_BRC_CONDITION_MIN_LEN=10; the 47-char note passes, then_extract_slice_idrejects. - producer_push:
commit_shanon-empty check, then_extract_slice_id.
All correct.
Non-blocking observations
1. Tests don't reset message_store state. test_confirmed_routes_to_slice_tracker calls tracker.handle_confirmed whose mocked return ({"status": "confirmed"}) drops the handler into the "Final CONFIRMED" branch, which does a real _existing_confirmed_for_role lookup against the live in-memory message store and a real store.add_message write. The single tracker-call assertion still holds because the tracker call happens before the message-bus side effects, but if these tests run in the same process as another confirmed-handler test that seeds a coder CONSENSUS_CONFIRMED for issue-2403/implement, the dedupe path takes over silently. Today this isn't a problem (no other test seeds that exact message), but a monkeypatch of _existing_confirmed_for_role or get_message_store would make the test more hermetic. Same observation for test_producer_push_routes_to_slice_tracker (writes a CONSENSUS_PROPOSE message on auto re-propose — harmless because the test mocks auto_re_propose: False).
2. call.args[0] pipeline_id assertion is positional-only. The _slice_arg_from_call helper supports both positional and kwarg forms for slice_id, but every test asserts call.args[0] == "issue-2403" for the pipeline_id. If a future refactor moves pipeline_id to a kwarg, every test in both new classes would break in lockstep — a low-cost generalisation in the helper would future-proof this.
3. Regex-divergence comment names egg_contracts.models.Slice but the migration validator is on Contract. The note at signals.py:81-82 says "the model loader's _migrate_phases_to_slices validator rewrites legacy phase-<N> ids to slice-<N> on contract load" — accurate, but the validator is at Contract._migrate_phases_to_slices (shared/egg_contracts/models.py:668), not on Slice. The validator is mode="wrap" on the parent Contract, so it runs before per-Slice field validation kicks in. Worth noting that constructing a Slice directly via Slice(id="phase-2", …) (bypassing Contract loading) is also accepted by pydantic field validation — i.e. the canonicalisation guarantee applies to Contract.model_validate(json_dict) paths, not to all Slice construction. The signal-side regex is doing real work here, not just defense-in-depth, for any code path that constructs Slices outside Contract loading.
What's good
- The new
_run_concurrent_phasecomment names exactly what HEARTBEAT/OVERSEER_ALERT do (no-op ACK, message bus respectively), which prevents a future maintainer from chasing a phantom tracker lookup. - The three different malformed shapes used across the rejection tests (path traversal, slashed segment, legacy
phase-N) cover the practical attack surface. - The
_slice_arg_from_callhelper correctly handles both positional and kwarg forms — a small piece of forward-compatibility that pays off ifget_peer_consensus_tracker's signature ever changes. - The legacy
phase-2rejection test serves as runnable documentation of the divergence.
Test plan
The end-to-end push-against-GitHub-ref-storage check from v2 is still unchecked. The 22770f5 changes don't touch the slice-DAG cold path that the e2e covers — it's strictly comments + tests — so the e2e signal hasn't changed. Worth running issue-2261-v7 once before merge for closure, given the layered fixes (#2399 → #2403-routing → #2403-spawn-id → #2402 v3) all need to compose correctly in production.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Three non-blocking observations from the v3 review: - test_confirmed_routes_to_slice_tracker and test_producer_push_routes_to_slice_tracker now mock message_store.get_message_store and (for confirmed) _existing_confirmed_for_role / _resolve_pipeline_phase / _write_consensus_confirmed_marker so the handler's 'Final CONFIRMED' branch and the producer-push auto-re-propose branch don't read or write the live in-memory message store. Hermetic across repeat runs and multi-test suites that may seed CONSENSUS_CONFIRMED for the same pipeline_id/phase (review item 1). - _pipeline_arg_from_call mirror of _slice_arg_from_call so tests don't break in lockstep if a future refactor moves pipeline_id from positional to kwarg. Both new test classes plus the existing TestConsensusSignalSliceRouting tests now use the helper pair symmetrically. Inline kwarg/positional extraction in the original propose tests collapses to the helpers (review item 2). - _SLICE_ID_PATTERN doc comment rewritten to be precise: the canonicalisation lives on Contract._migrate_phases_to_slices (mode='wrap'), not Slice. The guarantee applies to Contract.model_validate(json_dict) paths only — direct Slice(id='phase-2') construction is accepted by pydantic field validation, so the signal-side regex is doing real work for any code path that constructs Slices outside Contract loading (review item 3).
|
Thanks for the thorough re-review. Disposition per non-blocking item below; fixes pushed in Per-item disposition
I also considered widening Test results
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of #2402 — v4 (923f135 addresses v3 feedback)
Status of v3 review feedback
The single new commit (923f135) is a tight, surgical follow-up that addresses all three non-blocking observations from the v3 review. No production logic changes — diff is routes/signals.py:77-96 (doc comment) + tests/test_slice_signal_routing.py (test hermeticity + helper symmetry).
| v3 item | Disposition |
|---|---|
#1 — test_confirmed_routes_to_slice_tracker and test_producer_push_routes_to_slice_tracker weren't hermetic against repeated runs |
Fixed — test_confirmed_* now patches message_store.get_message_store, routes.signals._existing_confirmed_for_role (returning (False, False)), routes.signals._resolve_pipeline_phase (returning "implement"), and routes.signals._write_consensus_confirmed_marker. The handler now reaches the "Final CONFIRMED" branch (signals.py:1707) and the add_message / _write_consensus_confirmed_marker calls land on mocks. test_producer_push_* patches message_store.get_message_store defensively — today the auto_re_propose: False mock keeps the message-bus write branch dormant, but the patch pins the test against a future regression where the gate moves. |
#2 — call.args[0] pipeline_id assertion was positional-only |
Fixed — added _pipeline_arg_from_call mirror of _slice_arg_from_call (line 297-306). Both helpers do the same positional-or-kwarg fallback. Replaces the nine call.args[0] == "issue-2403" assertions across TestConsensusSignalSliceRouting (lines 223, 260) and the two new test classes — and collapses the inline kwarg/positional extraction in test_propose_routes_to_slice_tracker and test_propose_without_slice_falls_back_to_pipeline_tracker into the helper. A future get_peer_consensus_tracker(*, pipeline_id, slice_id) refactor wouldn't break either dimension. |
#3 — regex-divergence comment named the wrong validator host (Slice instead of Contract) |
Fixed — signals.py:77-96 rewritten. Now correctly attributes the canonicalisation to Contract._migrate_phases_to_slices (verified at shared/egg_contracts/models.py:666-668 — mode="wrap", runs before per-Slice field validation), names the file path so a future maintainer can grep, and explicitly calls out that the canonicalisation guarantee applies only to Contract.model_validate(json_dict) paths — direct Slice(id="phase-2", ...) construction is accepted by pydantic field validation (the Slice.id pattern at models.py:228 is `^(?:slice |
Verification
I traced through the patching wiring to confirm hermeticity:
message_store.get_message_storepatch reaches the handler:signals.py:1604, 1687, 1708all dofrom message_store import ... get_message_storeinside the function body, so each call re-resolves the attribute from the module —mock.patch("message_store.get_message_store")correctly intercepts it.routes.signals._existing_confirmed_for_role/_resolve_pipeline_phase/_write_consensus_confirmed_markerpatches: all three are bare-name calls within the same module (signals.py:1677, 1678, 1726), so module-attribute patching resolves them at call time. ✓- Decorator → parameter ordering: confirmed for both
test_confirmed_*(innermost-to-outermost:mock_get_tracker,mock_get_store,_mock_existing,_mock_phase,_mock_marker) andtest_producer_push_*(mock_get_tracker,mock_get_store).
I ran pytest tests/test_slice_signal_routing.py -v — 19/19 pass in 1.06s.
The doc comment claims also check out:
Slice.idpattern atshared/egg_contracts/models.py:228is exactly^(?:slice|phase)-[0-9]+$✓Contract._migrate_phases_to_slicesatmodels.py:666-668is decorated@model_validator(mode="wrap")✓- The
mode="wrap"runs before per-Slice field validation because the validator transformsdata(rewritingphase-N→slice-Nand copyingphases→slices) before callinghandler(data)✓
Non-blocking observations
1. _pipeline_arg_from_call placement is awkward. The helper sits at tests/test_slice_signal_routing.py:297-306, between TestConsensusSignalSliceRouting (which now uses it) and TestAllConsensusHandlersRouteToSliceTracker (which also uses it). Module-level helpers between two classes that both depend on them is a minor readability nit — colocating both helpers at the top of the file or in a small helpers block above all consumers would be cleaner. Not blocking.
2. Doc comment could pin a line number for the validator. The new comment at signals.py:82-83 names Contract._migrate_phases_to_slices and the file path shared/egg_contracts/models.py, which is more grep-friendly than the v3 wording. A line number reference (e.g. models.py:668) would short-circuit the grep, but line numbers rot — the function name lookup is the right tradeoff. Acknowledging the choice, not requesting a change.
3. The from message_store import ... inline-import pattern is fragile under future refactors. The hermeticity story relies on the fact that signals.py:1604, 1687, 1708 import get_message_store inside the function body each call. If any of these inline imports ever moves to module scope (from message_store import get_message_store at the top of signals.py), the mock.patch("message_store.get_message_store") would no longer reach the handler — routes.signals.get_message_store would be bound at module-load time. The hermeticity tests would silently lose their guarantee. A mock.patch("routes.signals.get_message_store") would be more robust to that refactor, but the current patch shape is correct for the current code shape. Worth a comment in the test if anyone touches the import structure in signals.py.
What's good
- The test hermeticity fix is the right shape: it doesn't replace
tracker.handle_confirmed's return value (which would change what the test pins), it just isolates the side-effect-bearing functions that get called after the tracker assertion. - The
_pipeline_arg_from_call/_slice_arg_from_callhelper pair is symmetric and correctly handles both positional and kwarg forms. - The doc comment rewrite is precise — it names exactly which model owns the validator, the validator's pydantic mode, and the seam where the canonicalisation guarantee breaks down (direct Slice construction). This is the kind of comment that prevents a future contributor from removing
_SLICE_ID_PATTERNthinking it's redundant with theSlice.idpattern.
Test plan
The end-to-end push-against-GitHub-ref-storage check from v2/v3 is still unchecked. The 923f135 changes don't touch the slice-DAG cold path that the e2e covers — it's strictly comments + test mocks — so the e2e signal hasn't changed since v3. Running issue-2261-v7 once before merge remains the operator's call for closure on the layered #2399 + #2403-routing + #2403-spawn-id + #2402-vN fixes.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
* Colocate `_slice_arg_from_call` and `_pipeline_arg_from_call` above
all consumers (was sandwiched between two test classes that both
used them).
* Document the `mock.patch("message_store.get_message_store")` patch
shape's reliance on the inline-import pattern in `signals.py`. If a
future refactor hoists `from message_store import get_message_store`
to module scope, the patch would silently stop intercepting and the
hermeticity guarantee would break — the comment names the fix
(re-target to `routes.signals.get_message_store`) so the next
contributor doesn't have to re-derive it.
No production logic changes; tests still 19/19 pass.
|
Thanks for the v4 review. Disposition per non-blocking item below; fixes pushed in Per-item disposition
Test results
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of #2402 — v5 (5022c94 addresses v4 feedback)
Status of v4 review feedback
The single new commit (5022c94) is a tight, test-only follow-up. The compare API confirms the entire delta is orchestrator/tests/test_slice_signal_routing.py (+38/−23). No production logic touched.
| v4 item | Disposition |
|---|---|
#1 — _pipeline_arg_from_call placement was awkward (sandwiched between two consumer classes) |
Fixed — both helpers now sit at tests/test_slice_signal_routing.py:103-122, above the first consumer class (TestSliceSpawnEnvShape at line 126, TestConsensusSignalSliceRouting at line 215). The relocation is preceded by a section banner explaining the placement rationale ("colocated above all consumers so a future get_peer_consensus_tracker signature change has one place to update"), which closes the readability nit cleanly. All nine _pipeline_arg_from_call / _slice_arg_from_call call sites (lines 252-565) are unchanged — pure relocation, no semantic drift. |
| #2 — doc comment could pin a line number for the validator | Acknowledged, not requested — line numbers rot, function-name lookup is the right tradeoff. No change expected. |
#3 — inline-import pattern in signals.py is fragile under future refactors |
Fixed — tests/test_slice_signal_routing.py:436-445 adds a multi-line note inside test_confirmed_routes_to_slice_tracker explaining that mock.patch("message_store.get_message_store") only intercepts because signals.py imports the symbol inside the function body at the call sites, names the failure mode if anyone hoists the import to module scope, and prescribes the fix (mock.patch("routes.signals.get_message_store")). |
Verification
I verified the inline-import claim against the current signals.py:
$ rg -n "from message_store import.*get_message_store" orchestrator/routes/signals.py
973, 1148, 1285, 1375, 1439, 1496, 1604, 1687, 1708, 1836, 1954, 2046
All 12 call sites are inside function bodies, not at module scope. The comment's claim is accurate; the patch shape does correctly intercept. If anyone hoists the import, the prescribed fix (mock.patch("routes.signals.get_message_store")) is correct because routes.signals would then bind the name at module-load time.
I also verified the helper relocation didn't change behaviour:
_slice_arg_from_callbody at lines 103-112 matches the v4 body byte-for-byte._pipeline_arg_from_callbody at lines 114-122 matches the v4 body byte-for-byte.- Nine usage sites across three test classes (
TestConsensusSignalSliceRoutingx2,TestAllConsensusHandlersRouteToSliceTrackerx7) all unchanged.
Non-blocking observations
1. The inline-import note lives only in test_confirmed_routes_to_slice_tracker. test_producer_push_routes_to_slice_tracker also patches message_store.get_message_store and would silently lose hermeticity under the same refactor. The single-location comment is fine — it's the canonical documentation of the pattern and git grep "routes.signals.get_message_store" would surface it if a future contributor hits the issue. Not requesting a copy. Acknowledging the choice.
2. Section banner is a minor convention nit. The # --- banner above the helpers is a useful visual anchor but isn't a pattern used elsewhere in the test file. Consistency-wise either rip it out or add similar banners to other logical sections (_setup_spawn at line 78 would be a candidate). Not blocking; just an observation about what stays load-bearing as the file grows.
What's good
- The helper relocation is a pure move: byte-identical bodies, identical call sites, just placement changed. Low-risk refactor done right.
- The inline-import comment names exactly what the test depends on (the call-site
from message_store import get_message_storeshape) and the precise fix (routes.signals.get_message_storepatch target). This is the kind of comment that prevents a silent test-hermeticity regression rather than a noisy one — the patched mock would still be passed in, the assertions would still pass, and only the side-effect cleanup would silently start hitting the live message store. - 19/19 tests still pass per the commit message; consistent with my read of the diff (no behavioural changes).
Test plan
The end-to-end push-against-GitHub-ref-storage check (issue-2261-v7-style submission) from v2/v3/v4 is still the recommended pre-merge verification for closure on the layered #2399 + #2403-routing + #2403-spawn-id + #2402-vN fixes. The 5022c94 changes are purely test-code reshuffling — they don't touch the slice-DAG cold path that the e2e covers, so the e2e signal hasn't changed since v4. Operator's call.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Thanks for the v5 sign-off. Per-item disposition below — no new commit since the review is on the current HEAD ( Per-item dispositionStatus of v4 feedback table (all three already fixed in
Non-blocking observations (both explicitly not requesting changes):
Test plan gapAcknowledged across all five review iterations. The Test resultsNo new commit, but I re-confirmed the v4 test results hold:
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
#2419) * Fix #2410: thread slice_id through restart_agent_job and detect_uncommitted_changes Closes the dormant gap called out in #2402's v2 review. After #2403 plumbed slice_id through spawn_agent_job, the restart and uncommitted- change detection paths still built non-slice-scoped identifiers — so the next operator-route extension that wires slice scope into restart would silently: 1. delete_job() the wrong (pipeline-level) Job name, leaving the real slice Job running while a fresh non-scoped Job is spawned. 2. mount the wrong (or absent) worktree, since agent_worktree_id is built without the slice segment. 3. spawn the agent without EGG_SLICE_ID, so its CONSENSUS_* signals route to the pipeline-level tracker that has no record of it. Changes: - Lift the canonical slice_id pattern + extractor from routes/signals.py into a small slice_id_validation module so routes/pipelines.py can validate against the same regex as the signal handlers. signals.py now imports the alias. - restart_agent_job: add slice_id parameter, thread to _build_k8s_job_names() and forward to spawn_agent_job(). Restart budget key becomes (pipeline_id, agent_role, slice_id) so concurrent slices each get an independent budget; reset_restart_counts(pid) still clears all of them via prefix filter. - detect_uncommitted_changes: add slice_id parameter, build the worktree id with the slice segment when supplied, surface slice_id in the result dict + log line. - get_restart_count: optional slice_id parameter so slice-aware callers can read the per-slice budget. - Operator restart route POST /pipelines/<id>/agents/<role>/restart: accept slice_id via query param or JSON body, validate against the canonical shape, forward to the spawner, and target the per-slice consensus tracker on reset. - Tests: TestRestartAgentJobSliceScope and TestDetectUncommittedChangesSliceScope mirror TestSpawnAgentJobSliceScope; route tests cover query/body/None and invalid-shape rejection. Existing 2-tuple restart-key fixtures updated for the new 3-tuple shape. * Fix file-size lint: allowlist orchestrator/kubernetes_spawner.py The file grew to 1540 lines after slice_id plumbing was added in this PR, crossing the 1500-line hard cap. Add it to the allowlist under the existing #2248 tracking issue for follow-up decomposition. * Address v1 review: propagate EGG_SLICE_ID, fix restart_count, consolidate regex Three fixes from the v1 BRC review on PR #2419: 1. Inject EGG_SLICE_ID into the spawned container's environment in spawn_agent_job whenever slice_id is supplied. The PR claimed to close failure mode #3 from #2410 ("the restarted agent re-enters the per-slice consensus tracker") but the slice_id parameter was only consumed by naming + worktree id — the new Job came up with no EGG_SLICE_ID, so its CONSENSUS_* signals routed to the pipeline-level tracker. Setting it in the spawner is one-source- of-truth: every caller (concurrent path, restart route, future slice-aware caller) gets correct env without re-deriving it. Tests assert the env appears on the SpawnedContainer and on the create_container call. 2. Forward slice_id to spawner.get_restart_count in the restart route. The previous lookup queried the pipeline-level bucket (typically 0) after a slice-scoped restart had bumped the per- slice bucket — both the audit log and the JSON response misreported "you've burned N of M restarts" telemetry. Tests assert the call kwargs include slice_id. 3. Switch concurrent_executor's two inline re.fullmatch sites to the canonical SLICE_ID_PATTERN from slice_id_validation. The module docstring claimed concurrent_executor used the shared regex; it didn't. One pattern, three call sites (signals.py, restart route, concurrent_executor) — drift can't reintroduce a fourth shape. * Address v2 review: protect EGG_SLICE_ID, drop redundant sandbox_env setter The v2 review of #2419 flagged that EGG_SLICE_ID was not in _PROTECTED_ENV_KEYS — today every path that sets it derives the value from the same slice_id parameter, but a future caller passing a mismatched value via extra_env would silently end up with the agent's signals tagged for one slice while its Job + worktree belong to another, with no warning. Make the spawner the single source of truth: - Add EGG_SLICE_ID to _PROTECTED_ENV_KEYS in kubernetes_spawner.py. extra_env values for this key are now logged and dropped, so the spawner's value (from the slice_id parameter that already drives Job naming + worktree id) always wins. - Drop the redundant sandbox_env["EGG_SLICE_ID"] = slice_id assignment in routes/pipelines.py:_run_concurrent_phase. The spawner now owns this; leaving the duplicate in place would just trip the protected- key warning every concurrent spawn. - Update the test_slice_signal_routing.py wrapper-side tests to assert slice_id flows through create_concurrent_spawn_fn as a kwarg (not via sandbox_env), with a positive assertion that sandbox_env does NOT carry EGG_SLICE_ID. - Add two test_kubernetes_spawner.py tests pinning the protected-key contract: extra_env cannot override EGG_SLICE_ID when a slice_id parameter is supplied, and cannot inject EGG_SLICE_ID when the spawn is pipeline-level. The spawner-side container-env tests added in the v1 review fix (test_spawn_with_slice_id_sets_egg_slice_id_env, test_restart_propagates_egg_slice_id_to_container_env) still cover the positive flow end-to-end. * Address v3 review nit: refresh stale shallow-copy comment The comment at test_slice_run_loop_integration.py:1186 referenced behavior from before the v2 review fix (c0eec40), where _run_concurrent_phase took a shallow copy of sandbox_env to assign EGG_SLICE_ID. That assignment was dropped — the dict simply isn't mutated because the function no longer touches sandbox_env at all. Updated the comment to match. --------- Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Re-review of #2402 — v6 (delta: 5022c94..00c2b4d is the merged-in #2419 work)
Status
The PR is MERGED. The single new commit since v5 is the squash-merge of PR #2419 (00c2b4d) — the follow-up that closes the dormant gap from v2 review item #1: restart_agent_job and detect_uncommitted_changes did not carry slice_id. That work went through its own review cycle (v1, v2, v3) on #2419 and is now on this branch. Re-reviewing it here for completeness.
Status of v2 review item #1 (the deferred gap)
| Failure mode from v2 review | Disposition |
|---|---|
delete_job targets the wrong (pipeline-level) Job name |
Fixed: restart_agent_job accepts slice_id and forwards to _build_k8s_job_names(pipeline_id, agent_role, slice_id=slice_id) (kubernetes_spawner.py:1153-1155). Test test_delete_targets_slice_scoped_job_name pins it. |
| Worktree id wrong/absent under restart | Fixed: detect_uncommitted_changes accepts slice_id and constructs the worktree id as f"{pipeline_id}-{slice_id}-{agent_role}" (kubernetes_spawner.py:1304-1306). Tests cover slice/pipeline isolation in both directions. |
EGG_SLICE_ID absent on respawned Job |
Fixed: spawn_agent_job injects EGG_SLICE_ID from the slice_id parameter (kubernetes_spawner.py:760-761). The restart_agent_job → spawn_agent_job chain forwards slice_id (:1219). |
Architectural improvements on top of the literal gap-fix
-
Single source of truth for
EGG_SLICE_ID. v2 review of #2419 caught that_run_concurrent_phasewas settingEGG_SLICE_IDinsandbox_envwhile the spawner was also setting it from theslice_idparameter. The fix addedEGG_SLICE_IDto_PROTECTED_ENV_KEYSand dropped the caller-side setter. This is the correct shape:extra_env-based override is logged and dropped, so a future caller that passes a mismatched value viaextra_envcannot silently end up with the agent's signals tagged for one slice while its Job + worktree belong to another. Teststest_extra_env_cannot_override_egg_slice_idandtest_extra_env_cannot_inject_egg_slice_id_when_pipeline_levelpin both directions. -
Regex consolidation. v1 review of #2419 caught that the canonical
slice-<N>regex was duplicated acrossroutes/signals.py, the newconcurrent_executor.pyvalidation sites, and the new restart route. The fix lifted the pattern intoorchestrator/slice_id_validation.pyand threaded the import through three call sites. Drift can't reintroduce a fourth shape on the orchestrator side. -
Per-slice restart budget. The restart key is now
(pipeline_id, agent_role, slice_id)so concurrent slices each get an independent budget and lock.reset_restart_counts(pipeline_id)still sweeps all of them viak[0] == pipeline_idfilter. Testtest_restart_count_is_per_sliceandtest_reset_restart_counts_clears_slice_bucketspin both behaviors. The concurrency-guard testtest_restart_lock_created_per_keyadds the new slice scope to the differentiation matrix. -
Restart-count reporting follows the same key. v1 review of #2419 caught that the restart route was reading
get_restart_count(pipeline_id, agent_role)after a slice-scoped restart had bumped the per-slice bucket — both the audit log and the JSON response would misreport "you've burned N of M restarts" telemetry. Now the route forwardsslice_idtoget_restart_count. Testtest_slice_id_query_param_forwarded_to_spawnerasserts both kwargs includeslice_id.
New code review
orchestrator/slice_id_validation.py:39-61 (SLICE_ID_PATTERN + extract_slice_id):
- Correctly anchored regex (
^slice-[0-9]+$). Noneand""both returnNone(no slice).- Non-string non-empty values raise
ValueError(theisinstance(raw, str)guard beforefullmatchcorrectly rejectsslice_id: 123rather than blowing up in regex). - Module docstring is precise: it names
Contract._migrate_phases_to_slicesas the canonicaliser, calls out that directSlice(id="phase-2", ...)construction bypasses canonicalization, and explains why this regex is doing real work (registry key must be canonical, Job names / worktree ids must be RFC-1123 safe).
orchestrator/routes/pipelines.py:2330-2338 (restart route slice extraction):
- Query-wins-over-body precedence is documented and consistent.
request.args.get("slice_id")for?slice_id=returns""(Flask convention), whichextract_slice_idcorrectly maps toNone— a quirk worth knowing but not a bug.- Validation failures return 400 before any spawn-side state is mutated. Test
test_invalid_slice_id_returns_400pins four malformed shapes (phase-2,slice-2/etc,../slice-2,slice-).
orchestrator/kubernetes_spawner.py:760-761 (EGG_SLICE_ID injection):
- Set before
extra_envis processed, so the protected-key path can detect and reject conflicts. - Wrapped in
if slice_id is not Noneso pipeline-level spawns leave the env clean. Testtest_spawn_without_slice_id_does_not_set_egg_slice_idpins this.
orchestrator/routes/pipelines.py:2476 (consensus reset routing):
get_peer_consensus_tracker(pipeline_id, slice_id)correctly targets the per-slice tracker. The pipeline-level tracker has no record of the slice agent, so without this theremove_agentcall would be a silent no-op against the wrong tracker.
orchestrator/concurrent_executor.py:294-300 and :332-336:
- Inline
import reblocks replaced with the sharedSLICE_ID_PATTERN. Comment correctly attributes the consolidation to the cross-cutting concern.
Non-blocking observations
1. Sandbox-side regex is a fourth copy outside the orchestrator's "single source of truth." sandbox/egg_agent_tools/handlers/brc.py:23 keeps its own _SLICE_ID_PATTERN = re.compile(r"^slice-[0-9]+$"). That's correct — the sandbox is a separate process and can't import from orchestrator/. But the new slice_id_validation.py module docstring says "The orchestrator pins slice ids to the canonical slice-<N> shape at every gateway-facing seam … signal handlers (#2403), the operator-triggered restart route (#2410), and the gateway-bound branch builders in concurrent_executor." A reader could conclude this is the only copy. Worth a one-line note that the agent-side regex is intentionally duplicated across the process boundary.
2. ?slice_id= (empty query value) is silently treated as no-slice and masks any body-level slice_id. With ?slice_id=&body={"slice_id":"slice-2"}, the route extracts None and ignores the body. The "query wins over body" docstring covers it, but no test pins this edge case. A regression where the precedence was swapped wouldn't be caught.
3. Concurrent execution reconstruction in the slice-scoped restart path is implicitly mocked. The new tests in TestRestartAgentEndpointSliceScope mock get_container_spawner directly, so the is_concurrent_execution → ConcurrentPhaseExecutor.get_agent_env reconstruction is never exercised under a slice scope. If a future change broke executor.get_agent_env(role) for slice-scoped pipelines, the route's extra_env would be wrong but the test would still pass. Acknowledging — covering this would require constructing a fuller pipeline fixture.
4. The restart-route docstring documents query string slice_id but example shows it in the body. routes/pipelines.py:2275-2286 lists query-string slice_id first, then shows the body example with slice_id too. Functionally fine (both are accepted), but a reader might miss that one wins over the other unless they read the implementation comment.
What's good
- The
_PROTECTED_ENV_KEYSextension toEGG_SLICE_IDis a tight, defensive choice. It catches both directions (extra_env overriding a slice-scoped spawn's value, and extra_env injecting a value into a pipeline-level spawn). - Test coverage is comprehensive: spawn slice/no-slice, restart slice/no-slice, restart budget per-slice, worktree isolation in both directions, malformed slice rejection at the route boundary, query/body parameter precedence, protected-key contract on
extra_env. - The module docstring on
slice_id_validation.pyis the right level of detail — it names the validator that performs the canonicalisation, the file it lives in, the migration history (#2137 backward compat), and the failure modes (RFC-1123 safety, registry-key uniqueness). - Restart-budget telemetry is now correct under slice scope: the JSON response and audit log report the per-slice count, which is what the operator running
/restart?slice_id=slice-2expects. _get_restart_lockreturning the same lock for the same key (per-key map, not per-call) means a future concurrent-restart bug for slice agents would be caught by the same mutual-exclusion guarantee that already exists for pipeline-level agents.- The decision to keep
_restart_locksentries even afterreset_restart_counts(per the in-code comment) preserves the mutual-exclusion guarantee against in-flight concurrent restarts. The slice-scope extension doesn't weaken this.
Test plan
Same status as v5: the slice-DAG cold path against GitHub's ref storage is the unchecked e2e. The 00c2b4d work is restart-route + spawn-side, not the push path that #2399 originally fixed, so the e2e signal hasn't changed. The combined #2399 + #2403 + #2410 changes need an issue-2261-v7-style submission to confirm they compose correctly under live GitHub ref-storage semantics — same operator-call status as before.
— Authored by egg
|
egg review completed. View run logs 22 previous review(s) hidden. |
…c-updater] (#2425) Update documentation to reflect changes from a505cfa: - slice-dag.md: document that the pipeline tip is now at egg/<id>/work (not egg/<id>) so slice integration branches can coexist as siblings (#2399); update branch table, get_slice_integration_branch note, and get_worktree_branch example accordingly. - slice-dag.md: EGG_SLICE_ID is no longer just an advisory hint — BRC handlers now read it and attach slice_id to every CONSENSUS_* payload so the orchestrator routes to the per-slice tracker (#2403). - slice-dag.md: remove restart_agent with slice_id from "Out of scope" — the REST endpoint now accepts ?slice_id=slice-N (#2410). - orchestrator-cli.md: add slice-scoped curl example to the agent restart note. Triggered by: #2402 Authored-by: egg Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Summary
Fixes #2399. The orchestrator was creating two refs that git's storage rules forbid coexisting on origin: a leaf at
egg/<id>(the pipeline branch) and children ategg/<id>/slice-N(slice integration branches). GitHub rejects withdirectory file conflict, so every slice-DAG submission failed at slice creation time — the fifth distinct failure layer in the chain (#2367 → #2370 → #2372 → #2393 → this).The pipeline tip now pushes to
egg/<id>/workinstead. The<id>/namespace becomes a directory holding/workand/slice-Nas siblings, mirroring the existing per-container worktree convention (egg/<container_id>/work). This also resolves #2371 — its "latent fallback" shape becomes canonical.Changes
_ensure_pipeline_work_refinroutes/pipelines.py— normalisesegg/-prefixed branches at pipeline submission. Idempotent, skips BABYSIT (PR head refs are not orchestrator-owned) and non-egg/branches._slice_namespace_root— strips the/worksuffix to derive the prefix slice paths build from. Used in the slice loop and reconciler.concurrent_executor.py—get_worktree_branchandget_slice_integration_branchstrip/workbefore embedding inegg/<id>/slice-M.stacked_pr_reconciler.py— splits the oldissue_branchparameter intoslice_namespace_root(slice path prefix, no suffix) andpipeline_branch(umbrella ref,/work-suffixed) so the cascade-fallback returns the actual remote ref.routes/contracts.py— branch-fallback updated to the/workshape.test_pipeline_branch_namespace.py— pins the coexistence property: pipeline tip and slice paths must not be prefixes of each other.Operational note
Pipelines whose
egg/<id>leaf was already pushed under the old shape (e.g.issue-2261-v6) will need that ref deleted from origin before retrying under the samepipeline_id, or should retry under a fresh id. New submissions get the correct shape automatically.Test plan
make lint(Python; pre-existing shellcheck failures in other worktrees' venvs are unrelated)orchestrator/tests/test_pipeline_branch_namespace.py— 12 tests, all passissue-2261-v7) and confirm slice integration branches push successfully