feat(workforce): missions group the roster, and agents get human names
Three things, all visible on the agents page.
**The roster looked like it was multiplying.** The sidebar flattened
orgs → companies → teams → agents, which renders a claw once per TEAM it
belongs to. Claws are reused across missions now, so a crew of five that had
run five missions appeared as twenty-five rows of the same five people. The
data was right and the view was lying. `GET /api/workforce` returns the roster
grouped by mission, and the tree renders each mission as a collapsible group,
so the repetition means something: the same colleague under each mission they
staffed. Claws on no mission come back under "Not on a mission" rather than
vanishing. The root now counts DISTINCT people, not rows.
**Agents were named after their jobs.** A team came back as planner, coder,
tester, reviewer, committer — the UI showed the same word twice (name on top,
role beneath) and the roster read as a stack of job tickets. New claws get a
given name from a deliberately wide pool (Amara, Vijay, Tomasz, Meredith…),
unique against the workspace roster AND within the team being minted. The role
is untouched in `job_title`, which is what the mission machinery binds on:
team_members.role_slot and the topology node carry the slot, so nothing
downstream keys off the display name. A reused claw keeps the name it had.
**Two latent reap bugs found while investigating a leak that was not one.**
Containers of completed missions are removed by `spawn_sweeper` after a
30-minute grace, and it works — an earlier report of leaking containers was me
reading that deliberate grace as a bug. But:
- the sweeper cleared the runtime binding even when teardown FAILED, and it
selects on `runtime_endpoint IS NOT NULL`. One transient docker error would
therefore hide a surviving container from the only thing that would retry
it, permanently. It now asks docker whether the container actually
survived: gone means clear, still there means keep the binding and retry —
which closes the orphan path without reintroducing the infinite retry the
original comment was guarding against.
- `set_runtime_binding` discarded rows_affected, so a mismatched workspace
updated nothing and returned Ok. The binding is how the sweeper finds a
container; a silent no-op there leaks one with no record of anything wrong.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e2c312b728
commit
0ad53da49c
@@ -376,7 +376,7 @@ pub async fn set_runtime_binding(
|
||||
endpoint: Option<&str>,
|
||||
pairing_code: Option<&str>,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query(
|
||||
let r = sqlx::query(
|
||||
"UPDATE missions
|
||||
SET runtime_container_name = $3,
|
||||
runtime_endpoint = $4,
|
||||
@@ -391,6 +391,16 @@ pub async fn set_runtime_binding(
|
||||
.bind(pairing_code)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
// A `WHERE id = $1 AND workspace_id = $2` that matches nothing is not an
|
||||
// error to sqlx — it updates zero rows and returns Ok. That made a
|
||||
// mismatched workspace indistinguishable from a successful bind, and the
|
||||
// binding is what the sweeper uses to find a mission's container: a silent
|
||||
// no-op here leaks a container with no record that anything went wrong.
|
||||
// Callers log this rather than aborting, which is the point — it becomes
|
||||
// visible instead of invisible.
|
||||
if r.rows_affected() == 0 {
|
||||
return Err(DbError::NotFound);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user