feat(workforce): missions hire the agents you already have, and name them by role
Every zeroclaw mission minted a fresh team of claws. They are created
`lifecycle = 'permanent'` and nothing reaps them until the MISSION is deleted,
so the roster grew by a whole team per mission while each member worked exactly
once — "My Workforce" was a list of strangers, and upskilling had nothing
durable to act on.
A mission now hires the claw that already does the job, matched on
`agent_template_link (template_id, role_slot)`, minting only what is missing.
Oldest first, so reuse concentrates on the same few claws and their brains
actually accumulate rather than spreading thinly across a growing pool.
A claw on a RUNNING mission is not offered. Two missions driving the same
ZeroClaw agent and the same `.brain` at once is a data race with a model on the
other end of it, and minting a second claw is much cheaper than reasoning about
that.
A reused claw is NOT re-seeded from the template's brain_seed — that would
overwrite what it learned with its starting point, which is precisely the
accumulation this exists for.
Names are the role now (`planner`), not
`"{mission} · {purpose} · {template} · {slot}"`. That produced
"verify: a repo-less research mission keeps its output · mission · Rust SDLC ·
planner" — unreadable in the roster, the API and every log line at once. Which
mission a claw is on is context a caller can join to; it is not its name.
And the half that makes reuse safe rather than destructive: deleting a mission
now purges only claws no OTHER mission still employs. Without it, tidying up one
mission deletes staff another one holds — presenting as the roster quietly
shrinking rather than as an error. A test asserts the guard exists inside the
reaper AND runs before the purge, because a check after it is decoration.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
895413509d
commit
fe2451fd60
@@ -899,6 +899,31 @@ async fn reap_mission_resources(state: &AppState, mission_id: Uuid) {
|
||||
// drift back into skipping the container teardown.
|
||||
let provisioner = crate::runtime_provision::RuntimeProvisioner::from_env();
|
||||
for cid in &claw_ids {
|
||||
// Only claws this mission is the LAST holder of.
|
||||
//
|
||||
// Claws are reused across missions now (see
|
||||
// `agent_template_link::reusable_claw`), so a mission's team can contain
|
||||
// staff that other missions still employ. Purging those would delete a
|
||||
// user's workforce as a side effect of tidying up one mission — and it
|
||||
// would look like the roster quietly shrinking, not like an error.
|
||||
let shared: i64 = sqlx::query_scalar(
|
||||
"SELECT count(*)
|
||||
FROM team_members tm
|
||||
JOIN mission_teams mt ON mt.team_id = tm.team_id
|
||||
WHERE tm.claw_id = $1 AND mt.mission_id <> $2",
|
||||
)
|
||||
.bind(cid)
|
||||
.bind(mission_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
if shared > 0 {
|
||||
eprintln!(
|
||||
"missions::delete: keeping claw {cid} — {shared} other mission(s) still employ it"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let report = crate::routes::claws::purge_agent(
|
||||
&state.pool,
|
||||
&state.runtime,
|
||||
@@ -1658,6 +1683,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reap_tests {
|
||||
/// A mission's teardown must ask whether anyone else still employs a claw.
|
||||
///
|
||||
/// Claws are reused across missions now, so a mission's team can contain
|
||||
/// staff other missions still hold. The old code purged every claw in the
|
||||
/// team unconditionally, which under reuse deletes a user's workforce as a
|
||||
/// side effect of tidying one mission — and it presents as the roster
|
||||
/// quietly shrinking rather than as an error.
|
||||
#[test]
|
||||
fn mission_teardown_checks_for_other_employers_before_purging() {
|
||||
let src = include_str!("missions.rs");
|
||||
let reaper = src
|
||||
.split("async fn reap_mission_resources")
|
||||
.nth(1)
|
||||
.expect("the reaper exists");
|
||||
// Scoped to the reaper, so the check cannot be satisfied by some other
|
||||
// function elsewhere in the file that happens to mention mission_teams.
|
||||
assert!(
|
||||
reaper.contains("mt.mission_id <> $2"),
|
||||
"the purge must exclude claws held by another mission"
|
||||
);
|
||||
let purge_at = reaper.find("purge_agent").expect("it still purges");
|
||||
let guard_at = reaper.find("mt.mission_id <> $2").expect("guard present");
|
||||
assert!(
|
||||
guard_at < purge_at,
|
||||
"the guard has to run BEFORE the purge, or it is decoration"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod artifact_tests {
|
||||
/// A filename reaches `Content-Disposition` after an AGENT chose it.
|
||||
|
||||
Reference in New Issue
Block a user