Skip to content

feat(registry): add a starting state with a single activation path - #93

Open
maxlamagna wants to merge 1 commit into
bcurts:mainfrom
maxlamagna:feat/ready-gate-registry-state
Open

feat(registry): add a starting state with a single activation path#93
maxlamagna wants to merge 1 commit into
bcurts:mainfrom
maxlamagna:feat/ready-gate-registry-state

Conversation

@maxlamagna

Copy link
Copy Markdown
Contributor

First of three for #90. Relates to #90.

Groundwork for the opt-in readiness gate described in #90. Inert without ready_gate=True, which is why it can land ahead of the server, UI and wrapper pieces that use it.

  • register(ready_gate=True) enters starting, and mark_ready is the only path to active, so an instance cannot drift into active by another route.
  • mark_starting re-gates an existing starting or active instance, for restarts.
  • claim refuses a starting instance, so a second process cannot take an identity that is still coming up.
  • cancel_starting removes the instance with no reservation and no reclaimable entry, through the same removal primitive as deregister, so family rename-back bookkeeping runs on both paths. It returns that removal record so a caller can migrate chat identity on a rename-back.
  • expire_crashed is the crash sweep's removal. A starting instance is cancelled; anything else stays reclaimable so a sleeping machine recovers its identity. Because that choice is state-dependent it is made under the same lock as the removal — deciding outside it lets a wrapper re-gate in the window and take the wrong path, which strands its name and leaves a revivable token.
  • Token recovery preserves starting, so restarting the server cannot activate an agent whose CLI was never proven ready.

The existing removals are untouched: deregister and cancel_starting still return exactly {'ok': True} plus an optional _renamed_back. The cancelled key belongs to the state-dependent expiry alone.

Tests: 21 unit tests, written red-first, each guard verified by a mutation that removes it — including a barrier proving a concurrent re-gate cannot complete inside the removal, one that fails any implementation deciding from a state read before the removal, and a set pinning the exact return shape of every removal.


Ordering. Two further PRs complete #90 and will be opened as each parent merges, so that each shows only its own work rather than repeating this one:

  1. this PR — registry starting state
  2. server, API and UI — 6 files, +587, opens once this merges
  3. wrapper probe and gate with its test harness — 8 files, +1428, opens once 2 merges

#91 also fixes an unrelated intermittent 502 in ProxyHeaderForwardingTests that can make a full-suite run fail at random; merging it first makes this stack read cleanly.

Groundwork for an opt-in readiness gate. An instance may now be registered as
`starting`: it receives the identity details a wrapper needs in order to
launch, but it is not yet active.

- `register(ready_gate=True)` enters `starting`, and `mark_ready` is the only
  path to active, so an instance cannot drift into active by another route.
- `mark_starting` re-gates an existing starting or active instance, for
  restarts.
- `claim` refuses a starting instance, so a second process cannot take an
  identity that is still coming up.
- `cancel_starting` removes the instance with no reservation and no reclaimable
  entry, via a shared `_remove_instance` primitive, so the family rename-back
  bookkeeping runs on both removal paths. It returns that removal record rather
  than a bare bool, so a caller can migrate chat identity and history on a
  rename-back exactly as it already does for deregister.

- `expire_crashed` is the crash sweep's removal. The two cases need opposite
  treatment — a starting instance is cancelled so a relaunch reacquires the bare
  name and its token dies, anything else stays reclaimable so a sleeping machine
  recovers — and that choice is state-dependent, so it is made under the same
  lock that performs the removal. Choosing outside it is a race: a wrapper
  re-gating from active to starting in the window would still be deregistered
  reclaimably, reserving its name and leaving a revivable token.

Token recovery preserves `starting`. On startup every persisted instance is
reloaded as reclaimable and a live wrapper transparently recovers its identity
through resolve_token; that path previously forced every recovered instance to
active, so restarting the server would have opened the gate for an agent whose
CLI was never proven ready. Recovery of an active instance is unchanged.

Inert without `ready_gate=True`: existing registration behaviour is unchanged,
which is why this can land ahead of the server, UI and wrapper pieces that
use it.

The existing removals are untouched: `deregister` and `cancel_starting` still
return exactly `{'ok': True}` plus an optional `_renamed_back`. The `cancelled`
key belongs to the state-dependent expiry alone, and the by-state mode chooses
both branches itself rather than inheriting them from the caller.

21 unit tests, written red-first, each guard verified by a mutation that
removes it. Two cover the expiry race specifically: a barrier proving a
concurrent re-gate cannot complete while the removal holds the lock, and one
that fails any implementation which decides from a state read before the
removal rather than during it. A further set pins the exact return shape of
every removal, so the gate cannot quietly widen an existing contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant