Skip to content

feat(code-editing): Step 4 — save→redeploy orchestration (flag-gated, inert) - #67

Merged
WayforthOfficial merged 1 commit into
mainfrom
feat/save-redeploy-step4
Jun 27, 2026
Merged

feat(code-editing): Step 4 — save→redeploy orchestration (flag-gated, inert)#67
WayforthOfficial merged 1 commit into
mainfrom
feat/save-redeploy-step4

Conversation

@WayforthOfficial

Copy link
Copy Markdown
Owner

The integration step, per the signed-off design. Wires building + versioning + activation onto the dispatch path — flag-gated and inert (AGENT_VERSIONED_DISPATCH_ENABLED, default OFF). No merge — for review.

The flow

create version → validate (PARAMS extraction + requirements vs allowlist) → build image (injectable build_fn) → forward-only atomic activate. Fail-closed: any failure before activation leaves the prior active version serving.

The two tests you said you'd review hardest — proven against real Postgres

scripts/agent_redeploy_proof.py runs the real orchestrator + dispatch resolver against a live DB with an injected fake build_fn (no E2B/mirror needed):

A. build failure leaves prior active version serving
  PASS  redeploy raises RedeployError(build)
  PASS  active pointer still v1 (agent not broken)
  PASS  failed build recorded as a 'failed' version
B. in-flight isolation (run before edit stays on old version)
  PASS  edit activated v2
  PASS  run R1 (pre-edit) still bound to v1      ← version_id binding
  PASS  run R2 (post-edit) bound to v2
  PASS  the two runs resolved DIFFERENT versions
C. forward-only activate
  PASS  older build (v8) did NOT clobber newer active (v9)
ALL PROPERTIES PROVEN ✓ (8/8)
  • (1) Build failure → pointer unmoved: redeploy raises RedeployError(build), the new version is marked failed, and active_version_id is still v1 — a bad edit can't take an agent down.
  • (2) In-flight isolation: a run dispatched before the edit is bound (agent_runs.version_id) to v1; a run dispatched after gets v2. The binding is set once at dispatch and never re-read, so the activate can't touch a running run.

One refinement over the signed-off design (flagging it)

The design said UNIQUE + advisory lock for the double-save race. I implemented UNIQUE + forward-only activate instead — activation only advances version_no, so a slow older build can't clobber a newer already-active version (proof C). This gives the same last-submitted-wins without holding a DB transaction/lock across the multi-second E2B build (which the advisory-lock approach would have required). Same guarantee, no long-held connection.

Flag-gating + backward-compat

  • OFF (default) → save + dispatch are the legacy single-file .code paths, byte-identical. Proven by the unchanged suite + explicit _resolve_dispatch flag-off unit test.
  • ON → save redeploys (create→build→activate); dispatch resolves the active version, boots its image, writes its files. A version with image_ref IS NULL (backfilled v1) falls back to the .code path but still binds version_id for audit — gradual cutover, agents move to the image path only as rebuilt.

Migration 070 (agent_runs.version_id)

Additive/nullable; idempotent check_db mirror; ephemeral-pg proof: additive, idempotent, reversible (existing runs intact with version_id NULL, down-migration clean).

Mirror — option (B), as agreed

build_fn is injectable (real build_agent_image in prod, fake in tests — same pattern as Step 1's sandbox_factory). The prod build_fn reads AGENT_MIRROR_URL (empty until the mirror stands up). The mirror is a flip-on gate, not a merge gate — Step 4b proves the real end-to-end build before the flag ever flips.

Tests

9 new unit tests (fail-closed validation, build-never-activates, flag-off dispatch identical, NULL-image fallback, versioned path). Full suite 616 passed.

Not in this PR (next)

  • Step 4b: stand up the minimal real mirror + prove the real egress-locked build E2E, then the flip.
  • Step 5: multi-file save endpoint (the orchestrator already takes a files map) + frontend.

🤖 Generated with Claude Code

… inert)

The integration step: create version → validate (PARAMS + requirements vs allowlist) →
build image (injectable build_fn = Step-1 egress-locked pipeline in prod) → forward-only
atomic activate. Fail-closed; flag-gated (AGENT_VERSIONED_DISPATCH_ENABLED, default OFF).

- services/agent_redeploy.py: the orchestrator. Validation is fail-closed BEFORE a build
  is spent; build failure marks the version 'failed' and NEVER reaches activate (prior
  active version keeps serving). Activation is FORWARD-ONLY (advances version_no only) —
  a refinement over the signed-off advisory-lock: same last-submitted-wins WITHOUT holding
  a txn/lock across the multi-second E2B build.
- migration 070: agent_runs.version_id — the in-flight isolation anchor. Dispatch resolves
  the active version ONCE and stamps version_id, so a concurrent edit can't change a
  running run's binding. Mirrored in check_db (idempotent), additive/reversible.
- cloud.py: flag helper + _resolve_dispatch (resolve-once, NULL-image fallback) + version_id
  binding + prod build_fn (mirror config-driven, empty until Step 4b). Save path redeploys
  when the flag is on; the .code write stays in sync as the fallback.
- sandbox.py: provider accepts files + image_ref (per-version image boot + multi-file),
  backward-compatible (None → current single-file .code path).

PROVEN:
- Real-Postgres proof (scripts/agent_redeploy_proof.py, 8/8): (A) build failure → active
  pointer UNMOVED + failed version recorded; (B) in-flight isolation — run dispatched
  before the edit stays bound to v1, run after gets v2 (version_id binding); (C) forward-only
  activate — older build can't clobber a newer active version.
- 9 unit tests (fail-closed validation, build-never-activates, flag-off dispatch identical).
- Migration 070 ephemeral-pg proof: additive, idempotent, reversible.
- Full suite 616 passed; flag OFF → dispatch byte-identical (ininert).

Mirror = option (B): build_fn injectable (fake in tests). Real mirror is a FLIP-ON gate,
not a merge gate — Step 4b proves the real end-to-end build before the flag flips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to wayforth / wayforth-pr-67 June 27, 2026 20:08 Destroyed
@WayforthOfficial
WayforthOfficial merged commit 5b1f148 into main Jun 27, 2026
2 of 3 checks passed
@WayforthOfficial
WayforthOfficial deleted the feat/save-redeploy-step4 branch June 27, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants