mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
C1 — Production agents-root contract: - scope_init.rs already had the correct base_dir contract (no extra 'agents' join); added production-shaped adoption test that mirrors the exact managed_agents_base_dir semantics to prevent regression. C2/C3 — Deadlock removal + store lock: - workspace.rs: drop rt_transition + store lock BEFORE compensate_drain on all three commit-guard failure paths; hold store lock from drain through commit so concurrent store writes cannot interleave. - identity.rs: drain returns Err((stopped, msg)); compensate uses the real stopped slice, not []; locks dropped before compensate_drain. C4 — Captured-scope completion: - confirm_team_snapshot_import and confirm_agent_snapshot_import: both now capture scope at entry, use _at() APIs throughout, validate generation before first write, resolve RetentionScope from captured. - Mesh recovery (recovery.rs): capture full WorkspaceAgentScope at entry; validate generation before each write to definitions_dir. - Restore missing-record stale-child: when find_managed_agent_mut fails for a spawned child (record deleted between Phase B and C), terminate the child and remove its receipt instead of leaking the process. C5 — Pre-Ready family in scope initializer: - ensure_scope_ready gains owner_pubkey parameter. - New run_pre_ready_family: runs legacy retention migration and persona snapshot backfill before writing _ready so a crash leaves the scope in a retryable state, not permanently marked Ready with incomplete data. - workspace.rs guards remain for pre-existing Ready scopes (idempotent). C6 — Delete dead boot-migration wrappers: - Deleted backfill_standalone_agents, detach_directory_backed_teams, and strip_baked_team_instructions (the #[allow(dead_code)]-suppressed app-level wrappers); their _in_dir equivalents are the authoritative scoped pipeline entry points. - Removed tests for the deleted functions from migration_command_tests.rs. C7 — Structured degradation reporting: - spawn_event_sync return type changed from Result<(), String> to (): the dispatch cannot fail; the false Result contract is removed. - workspace.rs restore spawn now emits workspace-degraded Tauri event when restore_managed_agents_on_launch returns Err, making restore failures observable to the UI instead of silently logged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
117 lines
4.5 KiB
Rust
117 lines
4.5 KiB
Rust
//! Phase 1A (unified agent model): boot-time materialization of each
|
|
//! persona-linked agent record's `runtime` onto the record itself.
|
|
|
|
use std::path::Path;
|
|
|
|
use super::{load_persona_runtimes, patch_json_records};
|
|
|
|
/// Materialize each persona-linked agent record's `runtime` from its linked
|
|
/// persona (unified agent model, Phase 1A). After this, spawn resolution reads
|
|
/// the record's own runtime (`record_agent_command` step 2) instead of the
|
|
/// live persona — same effective command by construction, so the spawn-config
|
|
/// hash is unchanged and no running agent shows a spurious restart badge (see
|
|
/// `spawn_hash::tests::materializing_runtime_keeps_hash_stable`).
|
|
///
|
|
/// Idempotent: records that already carry `runtime` are untouched, as are
|
|
/// records with no linked persona or a persona without a runtime (both keep
|
|
/// resolving through the legacy fallback path unchanged).
|
|
pub(crate) fn materialize_runtimes_in_file(path: &Path) -> Result<(), String> {
|
|
let persona_runtimes = load_persona_runtimes(path);
|
|
if persona_runtimes.is_empty() {
|
|
return Ok(());
|
|
}
|
|
patch_json_records(path, |obj| {
|
|
if obj.contains_key("runtime") {
|
|
return false;
|
|
}
|
|
let Some(runtime) = obj
|
|
.get("persona_id")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|pid| persona_runtimes.get(pid))
|
|
else {
|
|
return false;
|
|
};
|
|
obj.insert(
|
|
"runtime".to_string(),
|
|
serde_json::Value::String(runtime.clone()),
|
|
);
|
|
true
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::materialize_runtimes_in_file;
|
|
use crate::migration::test_support::{
|
|
read_agents_json, write_agents_json, write_personas_json,
|
|
};
|
|
|
|
#[test]
|
|
fn materialize_runtimes_copies_persona_runtime_onto_record() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_personas_json(
|
|
dir.path(),
|
|
&serde_json::json!([{ "id": "persona-1", "displayName": "Alice", "runtime": "goose" }]),
|
|
);
|
|
write_agents_json(
|
|
dir.path(),
|
|
&serde_json::json!([{ "name": "Fizz", "persona_id": "persona-1" }]),
|
|
);
|
|
materialize_runtimes_in_file(&dir.path().join("agents/managed-agents.json")).unwrap();
|
|
let records = read_agents_json(dir.path());
|
|
assert_eq!(records[0]["runtime"], "goose");
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_runtimes_is_idempotent_and_preserves_existing() {
|
|
// A record that already carries runtime (even one diverging from its
|
|
// persona) is never overwritten, and the second run rewrites nothing.
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_personas_json(
|
|
dir.path(),
|
|
&serde_json::json!([{ "id": "persona-1", "displayName": "Alice", "runtime": "goose" }]),
|
|
);
|
|
write_agents_json(
|
|
dir.path(),
|
|
&serde_json::json!([
|
|
{ "name": "Fizz", "persona_id": "persona-1", "runtime": "claude" },
|
|
{ "name": "Buzz", "persona_id": "persona-1" }
|
|
]),
|
|
);
|
|
let agents_path = dir.path().join("agents/managed-agents.json");
|
|
materialize_runtimes_in_file(&agents_path).unwrap();
|
|
let records = read_agents_json(dir.path());
|
|
assert_eq!(
|
|
records[0]["runtime"], "claude",
|
|
"existing runtime preserved"
|
|
);
|
|
assert_eq!(records[1]["runtime"], "goose");
|
|
|
|
let before = std::fs::read_to_string(&agents_path).unwrap();
|
|
materialize_runtimes_in_file(&agents_path).unwrap();
|
|
let after = std::fs::read_to_string(&agents_path).unwrap();
|
|
assert_eq!(before, after, "second run must be a no-op");
|
|
}
|
|
|
|
#[test]
|
|
fn materialize_runtimes_skips_unlinked_and_unknown_personas() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_personas_json(
|
|
dir.path(),
|
|
&serde_json::json!([{ "id": "persona-1", "displayName": "Alice", "runtime": "goose" }]),
|
|
);
|
|
write_agents_json(
|
|
dir.path(),
|
|
&serde_json::json!([
|
|
{ "name": "NoPersona" },
|
|
{ "name": "GonePersona", "persona_id": "deleted" }
|
|
]),
|
|
);
|
|
let agents_path = dir.path().join("agents/managed-agents.json");
|
|
let before = std::fs::read_to_string(&agents_path).unwrap();
|
|
materialize_runtimes_in_file(&agents_path).unwrap();
|
|
let after = std::fs::read_to_string(&agents_path).unwrap();
|
|
assert_eq!(before, after, "no linked runtime → untouched file");
|
|
}
|
|
}
|