Skip to content

feat: durable state milestone 1 — version-2 state, lease, and state commands - #42

Merged
jonatns merged 1 commit into
mainfrom
claude/progress-next-steps-y61lc7
Aug 19, 2026
Merged

feat: durable state milestone 1 — version-2 state, lease, and state commands#42
jonatns merged 1 commit into
mainfrom
claude/progress-next-steps-y61lc7

Conversation

@jonatns

@jonatns jonatns commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Implements the durable-state foundation from docs/DURABLE-STATE-PLAN.md Milestone 1. This is the local regtest foundation only — the planner, apply engine, and every later milestone remain proposed.

What's here

Version-2 durable state (labcoat-core/src/state.rs)

  • Per-environment state at .labcoat/state/<environment>/state.json: lineage UUID, monotonic serial, explicit chain identity, append-only instance history per resource, and a schema-only operations journal for the future apply engine.
  • A redeploy appends instance-2 and moves the active pointer instead of erasing history.
  • Deviation from the design doc, now recorded there: a per-environment directory (holding state.lock and backups/) instead of the flat .labcoat/state/<environment>.json, because that namespace already belongs to the apply call journal (apply.rs), which is untouched.

Locked, atomic, fail-closed backend (labcoat-core/src/state_backend.rs)

  • Exclusive OS lease on state.lock via fd-lock (kernel-released on crash, STATE_LOCKED on contention).
  • Compare-and-swap commits on the serial: temp file + fsync + atomic rename + parent-directory fsync, previous state kept as backups/state.json.prev.
  • Corrupt or truncated state is STATE_INVALID, never an empty ledger; unknown schema versions are STATE_UNSUPPORTED.
  • The design doc's StateBackend trait is deferred to the first remote backend (noted in the doc); the lease + serial-CAS semantics it needs are what StateLease::commit implements.

CLI: labcoat state migrate / state list / state show <resource> [--history]

  • migrate imports the resolved network's labcoat.lock records as imported instances, takes a timestamped backup into the environment's backups/ first, refuses to run onto existing state, and regenerates labcoat.lock byte-identically as the active-address book. labcoat.lock stays canonical for name resolution.
  • New environment setting (labcoat.toml key, LABCOAT_ENVIRONMENT, global --environment), default default, validated as a path component.

Reset detection

  • isomer-core keeps a persistent instance UUID inside the qubitcoin data directory, so labcoat reset (which deletes that directory) regenerates it for free; labcoat status shows it.
  • Deploys and apply reserve adoptions dual-write instances into v2 state under the lease when it exists, and refuse a reset or foreign chain with STATE_CHAIN_MISMATCH before broadcasting. Projects that never run state migrate are unaffected — the recording is dormant without state.

New error codes (STATE_MISSING, STATE_UNSUPPORTED, STATE_LOCKED, STATE_CHAIN_MISMATCH, STATE_CONFLICT) registered in the docs error table; STATE_INVALID wording broadened to cover both .labcoat/state stores.

Acceptance criteria → tests

Criterion (from the design doc) Coverage
Killed write leaves old-or-new valid file, never partial-read-as-empty hand-constructed post-crash filesystems, corrupt/truncated loads, commit-protocol and state.json.prev tests (state_backend.rs)
Concurrent mutating processes cannot share the lease same-process two-handle contention test plus a two-process test re-executing the test binary (lease_blocks_a_second_process)
v1 migration preserves every field, invents nothing from_v1/to_v1 verbatim round-trips (incl. the adopted sentinel and revert records), golden byte-compare, byte-identical lockfile round-trip incl. an untouched foreign-network subtree (tests/state_migrate.rs, bless with LABCOAT_BLESS=1)
Reset rejected as chain mismatch before mutation validate_chain matrix, instance-UUID regeneration test, deploy_guard dormant/mismatch/lease-release tests

Validation

  • cargo fmt --check, cargo check/test/clippy --workspace --locked -- -D warnings: 193 tests passing.
  • sync-reference.mjs --check green; apps/web reference regenerated (never hand-edited); pnpm --filter @labcoat/web check and build green; brand/release-notes/runtime-manifest guards pass.
  • Cargo.lock diff is exactly the two new labcoat-core dependency edges (fd-lock 4.0.4 and rand 0.8.5, both already resolved transitively — no version changes, no new packages).
  • End-to-end smoke with the built binary: state listSTATE_MISSING; migrate --json (offline: warns and defers chain identity); list/show --history; second migrateSTATE_CONFLICT; labcoat.lock byte-identical to its backup.

Manual follow-up (needs a live stack; not run in CI)

The against-live check documented in docs/STATE.md: in a project with deploys, state migrate → deploy again and see instance-2 in state show --historylabcoat reset -y + labcoat up → confirm the next deploy fails with STATE_CHAIN_MISMATCH before broadcasting.

🤖 Generated with Claude Code

https://claude.ai/code/session_017KCX1ovEzPP5fsAYQBA2LW


Generated by Claude Code

…te commands

Implements the durable-state foundation from docs/DURABLE-STATE-PLAN.md
Milestone 1:

- version-2 per-environment state at .labcoat/state/<environment>/state.json:
  lineage UUID, monotonic serial, chain identity, append-only instance
  history, and a schema-only operations journal (labcoat-core state.rs)
- locked, atomic, fail-closed backend (state_backend.rs): exclusive OS
  lease on state.lock via fd-lock, compare-and-swap serial commits with
  temp+fsync+rename, parent-directory fsync, and a state.json.prev backup;
  corruption is never read as empty state
- labcoat state migrate: imports the resolved network's labcoat.lock
  records as imported instances, backs the ledger up first with a
  timestamp, refuses existing state, and regenerates labcoat.lock
  byte-identically as the active-address book; state list and
  state show [--history] inspect resources and history
- environment setting (labcoat.toml, LABCOAT_ENVIRONMENT, global
  --environment), defaulting to "default"
- persistent Labcoat Network instance UUID in the qubitcoin data dir,
  regenerated by labcoat reset and shown by labcoat status
- deploy-time recording: deploys and reserve adoptions append instances
  under the lease when state exists, and refuse a reset or foreign chain
  with STATE_CHAIN_MISMATCH before broadcasting; labcoat.lock remains
  canonical for name resolution

Covered by unit and crash-simulation tests, a two-process lease-contention
test, and a golden migration fixture (bless with LABCOAT_BLESS=1). The web
CLI reference is regenerated and docs/STATE.md documents the shipped scope
and its deviations from the design doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KCX1ovEzPP5fsAYQBA2LW
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
labcoat-web Ready Ready Preview Aug 19, 2026 2:44am

@jonatns
jonatns merged commit 05d3764 into main Aug 19, 2026
6 checks passed
@jonatns
jonatns deleted the claude/progress-next-steps-y61lc7 branch August 19, 2026 02:45
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