Skip to content

Make a stranded agent fleet say so, and give 2-BM's three roles a slot each - #766

Merged
xmap merged 3 commits into
mainfrom
agent-readiness-verdict
Sep 2, 2026
Merged

Make a stranded agent fleet say so, and give 2-BM's three roles a slot each#766
xmap merged 3 commits into
mainfrom
agent-readiness-verdict

Conversation

@xmap

@xmap xmap commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Makes two silent failures loud, and gives 2-BM's three human roles a slot each.

Why

A Defined Agent is registered and inert: the subscribers' lifecycle gate
fires on Versioned only and refuses anything less without saying so. At
2-BM that stranded seventeen agents for three months behind a log that looked
clean the whole time, and it is still true today: sixteen of twenty are
Defined, while /actors reports all twenty active, because that is a
different axis. promote_seeded_fleet has existed since. What did not exist
was anything telling an operator the remedy was needed.

Three commits

Say at boot whether the fleet can actually act. A warning on every boot
of a deployment whose fleet cannot act, naming the members and the remedy.
Four not-ready reasons stay apart, because collapsing them restores the
silence at one remove: Defined is the only fault and the only one nobody
chose; Suspended is a live operator decision, reported and never warned
about; Deprecated is terminal; absent is a partly-seeded deployment rather
than a silent zero. The sort ranges over the FLEET, never the status map it is
handed, so a member the caller failed to look up lands in absent instead of
vanishing from the denominator.

Put the fleet's readiness on the page an operator already has open. The
subtitle carries 16 of 20 agents registered but inert and is the one line
always on screen; the Actors lane's card carries the fraction, the remedy, and
every inert member by name. Silent in the subtitle when healthy. NOT silent on
the card, deliberately: a row that appears only on a bad day teaches its reader
that an absent row means nothing to check, which is the lesson that hid this
for three months. actors joins ALWAYS_ON so the lane exists on exactly the
days no agent has emitted anything. Producer reads it on a 150-tick cadence,
not per tick (one stream load per agent) and not once per process (an operator
who just ran the promotion is looking at this page to see whether it worked).

Name the staff slots by role, and add 2-BM's third human.
2-bm-operator-a/b become 2-bm-admin, 2-bm-group-manager, 2-bm-staff. A
seat letter tells a future reader nothing about why a principal may act, and a
Policy grant is where that gets read. The ids did not move: they are
literals, the id is what every existing grant hangs off, and re-pinning would
leave a second Actor for the same person, which has already happened here once.
Names still come from the deploy host and still land only in the PII vault.

A gap this does not close

A role carries no scope. Policy holds (principal, command) pairs gated by a
Conduit and a Surface, with no beamline dimension, so "manages the imaging
group across several beamlines" and "staffs this one" are the same grant.
Harmless at a single-beamline deployment, real at the second. Documented in
governance.md rather than papered over by giving the roles different
COMMANDS, which would record a difference of scope as a difference of
capability.

Verification

  • 14 new unit tests for the census, each mutation-proved (ranging over the map
    instead of the fleet fails three; a stuck stranded fails two; treating
    Suspended as a fault fails two more)
  • 17 for the staff seed; adding the third slot broke three that hardcoded
    "two", which is the protection working
  • A new browser suite for the page readout plus a mutation harness: every
    mutant caught
  • 37181 architecture tests, ruff, pyright, tach all green

Also removes a hand-copied-list trap: the staff slots were written out in three
places (the tuple, an add_argument call, a dict literal in main), so adding
a person meant remembering all three and forgetting the last would parse a name
and never pass it on. The parser and the slot-to-name map now build from the
tuple.

Deploy note

The env var names change with the slots (BEAMLINE_STAFF_ADMIN_NAME,
..._GROUP_MANAGER_NAME, ..._STAFF_NAME). A host still carrying the old
names gets the existing fail-loud refusal, which names the slot and the
variable and never a value.

🤖 Generated with Claude Code

xmap and others added 3 commits September 1, 2026 17:18
`_seeded_fleet` made the fleet a value so a gesture could be made over it.
Nothing yet asked the question that value exists for.

A `Defined` Agent is registered and inert: the subscribers' lifecycle gate
fires on `Versioned` only and refuses anything less WITHOUT SAYING SO. At 2-BM
that stranded seventeen agents for three months behind a log that looked clean
the whole time, and it is still true right now: sixteen of twenty are
`Defined`, while `/actors` reports all twenty `active`, because that is a
different axis. The remedy has existed since `promote_seeded_fleet` shipped.
What did not exist was anything telling an operator the remedy was needed.

So this is not a metric. It is a `warning` on every boot of a deployment whose
fleet cannot act, naming the members and naming the remedy. An absence has to
be its own loud verdict; the alternative is exactly what happened, a correct
system with a clean log and nothing running.

FOUR NOT-READY REASONS, KEPT APART, because collapsing them restores the
silence at one remove. `Defined` is the only fault, and the only one nobody
chose. `Suspended` is a live operator decision, reported and never warned
about: nagging someone for a pause they made on purpose is how a line gets
scrolled past, which costs the signal this exists to add. `Deprecated` is
terminal. `absent` is worth seeing on a partly-seeded deployment rather than
counting as a silent zero, the same reasoning `promote_seeded_fleet` already
applies to it.

The sort ranges over the FLEET, never over the status map it is handed. A
member the caller failed to look up has to land in `absent`; ranging over the
map would drop it from the denominator instead, and report "2 of 2 ready" for
a fleet of three. That is the same silent-incompleteness shape the fleet value
was introduced to close, and it is pinned by its own test plus one that counts
against the real `SEEDED_FLEET` rather than a fixture.

Fourteen tests, each proved falsifiable: ranging over the map instead of the
fleet fails three, a `stranded` stuck false fails two, and treating `Suspended`
as stranded fails two more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The boot verdict says it once, to a log. This says it on the surface someone
is actually looking at.

Two places, because they answer to different readers. The subtitle carries
"16 of 20 agents registered but inert" and is the one line always on screen no
matter where the chart is panned, so it reaches someone who never thought to
ask the question. The Actors lane's card carries the detail: the fraction, the
remedy, and every inert member BY NAME, because `RunWitness` tells an operator
what to do where a count does not.

Silent when the fleet is healthy, in the subtitle only. A warning that is
always there is one nobody reads.

The CARD is not silent, and that asymmetry is deliberate. "Ready to act: 20 of
20" draws on a good day precisely so the row earns trust for the day it says
4; a row that appears only when something is wrong teaches its reader that an
absent row means nothing to check, which is the lesson that let a stranded
fleet sit unnoticed for three months.

`actors` joins ALWAYS_ON for a different reason from the other four. They are
always drawn so an empty one reads as "nothing happened here" rather than as a
row nobody drew. This one is always drawn because its card carries whether the
fleet can act at all, and that answer matters most on exactly the days no
agent has emitted anything -- which is when a presence-gated lane would not be
there. It draws today with zero events on it and the readiness row populated,
which is the whole point.

PAUSED AND ABSENT STAY SEPARATE FROM INERT. Suspended is a live operator
decision and Deprecated is terminal; reporting either as a fault would claim a
problem where a person made a choice. Their rows appear only when non-empty,
so a fleet with nothing paused shows no paused row rather than a zero.

Producer side, the readiness rides the snapshot on its own 150-tick (5 minute)
cadence. Reading it costs one event-store load per shipped agent and the
answer only moves when an operator promotes, suspends or deprecates one, so
re-reading twenty streams at 2Hz would be the most expensive thing in the tick
loop. Not cached for the process's lifetime either, which is the other obvious
choice and the wrong one: someone who has just run `promote_seeded_fleet` is
looking at this page to see whether it worked, and "restart the API to find
out" is not an answer.

`build_snapshot`'s `fields` widens from a dict of row lists to `dict[str,
Any]`. Every other field answers "which instances are open" and spells none as
`[]`; this one answers "can the fleet act", which is a single standing fact
and would be a lie as a list, since a healthy fleet would then have to be an
empty one.

The fixture is STRANDED, matching the real deployment on the day this shipped.
A healthy-fleet fixture would render one row and exercise none of the
reporting that exists for the case worth catching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The slots were `2-bm-operator-a` and `2-bm-operator-b`. A seat letter tells a
future reader nothing about why that principal may do anything, and a Policy
grant is exactly the place someone reads it. They are now `2-bm-admin`,
`2-bm-group-manager` and `2-bm-staff`.

Roles are safe in a public repo where names are not, which is the whole point:
the display names still come from the deploy host and still land only in the
`actor_profile` PII vault, and the `ActorRegistered` payload still carries no
name. What the repo gains is the reason, not the person.

THE IDS DID NOT MOVE. They are literals, not derived from the label, so the
rename minted nothing: the id is what the record means by a person and what
every grant already made hangs off. Re-pinning one would leave a second Actor
for the same human, which has happened at this deployment once already, and
the two records are still there with only one of them carrying any grants. A
test now says so in as many words.

The third slot (`...0c0010`) is new and its id was checked against every UUID
literal in the repo before it was picked.

A ROLE CARRIES NO SCOPE, and the docs now say so rather than implying
otherwise. `Policy` holds `(principal, command)` pairs gated by a Conduit and
a Surface; there is no beamline dimension in a grant. So "manages the imaging
group across several beamlines" and "staffs this one" are indistinguishable to
authorization, and those two roles hold identical commands here. That costs
nothing at a single-beamline deployment and becomes real at the second. It is
a gap in the Policy model, not something a label can close, and it must not be
papered over by handing the roles different COMMANDS: recording a difference
of scope as a difference of capability is both false and hard to unpick later.

The slot list was hand-written in THREE places: the tuple, an `add_argument`
call, and a dict literal in `main`. Adding a person meant remembering all
three, and forgetting the third would have parsed a name and then never passed
it to the ceremony. The parser and the slot-to-name map are now built from the
tuple, and the flag rides on the slot, so a slot cannot exist without one and
cannot carry a flag that reaches only one of the two readers.

Adding the third slot broke three tests that had hardcoded two, which is the
protection working. Two of them now count against `len(BEAMLINE_STAFF_SLOTS)`
instead of a literal, and the all-names-configured case builds its input from
the tuple: a hand-written dict of names would keep passing after a fourth slot
arrived and the ceremony would refuse at deploy time against a green suite.

Env vars rename with the slots (`BEAMLINE_STAFF_ADMIN_NAME`,
`..._GROUP_MANAGER_NAME`, `..._STAFF_NAME`). A deploy host still carrying the
old names gets the existing fail-loud refusal, which names the slot and the
variable and never a value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/agent
  fleet_readiness.py
  apps/api/src/cora/api
  _status_push.py
  beamline_staff_seed.py
  main.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit 74fe556 into main Sep 2, 2026
19 checks passed
@xmap
xmap deleted the agent-readiness-verdict branch September 2, 2026 01:37
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