feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -76,6 +76,7 @@ impl FakeContexts {
|
||||
manifest: AgentManifest {
|
||||
version: 1,
|
||||
entries: Vec::new(),
|
||||
orchestrator: None,
|
||||
},
|
||||
contents: HashMap::new(),
|
||||
saves: 0,
|
||||
@ -1058,7 +1059,10 @@ async fn swap_claude_to_codex_removes_claude_seed_and_projects_codex() {
|
||||
"the orphan .claude seed must be removed; removed={:?}",
|
||||
f.fs.removed()
|
||||
);
|
||||
assert!(!f.fs.has_file(&seed_path), "the seed is gone from the FS state");
|
||||
assert!(
|
||||
!f.fs.has_file(&seed_path),
|
||||
"the seed is gone from the FS state"
|
||||
);
|
||||
|
||||
// The relaunch projected the Codex sandbox config + args.
|
||||
assert!(
|
||||
@ -1066,7 +1070,10 @@ async fn swap_claude_to_codex_removes_claude_seed_and_projects_codex() {
|
||||
"the relaunch must project the Codex config"
|
||||
);
|
||||
let toml = String::from_utf8(f.fs.read_file(&codex_path).unwrap()).unwrap();
|
||||
assert!(toml.contains("sandbox_mode = \"workspace-write\""), "{toml}");
|
||||
assert!(
|
||||
toml.contains("sandbox_mode = \"workspace-write\""),
|
||||
"{toml}"
|
||||
);
|
||||
assert!(
|
||||
f.pty.last_spawn_args().contains(&"--sandbox".to_owned()),
|
||||
"sandbox args folded into the relaunch spawn: {:?}",
|
||||
@ -1102,7 +1109,10 @@ async fn swap_claude_to_claude_does_not_remove_seed() {
|
||||
f.fs.removed()
|
||||
);
|
||||
// It is still present (re-clobbered by the relaunch's projection).
|
||||
assert!(f.fs.has_file(&seed_path), "the seed survives the same-family swap");
|
||||
assert!(
|
||||
f.fs.has_file(&seed_path),
|
||||
"the seed survives the same-family swap"
|
||||
);
|
||||
}
|
||||
|
||||
/// (3) **Codex→Claude**: Codex owns no Replace file ⇒ nothing is removed on the
|
||||
@ -1138,13 +1148,19 @@ async fn swap_codex_to_claude_removes_nothing_and_keeps_codex_config() {
|
||||
f.fs.removed()
|
||||
);
|
||||
// The co-owned Codex config is untouched by cleanup…
|
||||
assert!(f.fs.has_file(&codex_path), "the .codex/config.toml is never deleted");
|
||||
assert!(
|
||||
f.fs.has_file(&codex_path),
|
||||
"the .codex/config.toml is never deleted"
|
||||
);
|
||||
assert!(
|
||||
!f.fs.removed().iter().any(|p| p == &codex_path),
|
||||
"the .codex/config.toml is not in the removed list"
|
||||
);
|
||||
// …and the relaunch projected the new Claude seed.
|
||||
assert!(f.fs.has_file(&seed_path), "the relaunch writes the Claude seed");
|
||||
assert!(
|
||||
f.fs.has_file(&seed_path),
|
||||
"the relaunch writes the Claude seed"
|
||||
);
|
||||
}
|
||||
|
||||
/// (4a) **No-op (no registry)**: without a projector registry wired on the swap,
|
||||
@ -1153,7 +1169,8 @@ async fn swap_codex_to_claude_removes_nothing_and_keeps_codex_config() {
|
||||
async fn swap_without_registry_removes_nothing() {
|
||||
let agent = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(1));
|
||||
// The default fixture wires NO projector registry on the swap.
|
||||
let f = fixture_with_profiles(&agent, vec![claude_profile(pid(1)), codex_profile(pid(2))]).await;
|
||||
let f =
|
||||
fixture_with_profiles(&agent, vec![claude_profile(pid(1)), codex_profile(pid(2))]).await;
|
||||
|
||||
let seed_path = format!("{}/{CLAUDE_SEED_REL}", run_dir_of(&agent.id));
|
||||
f.fs.put(&seed_path, b"{\"permissions\":{}}");
|
||||
@ -1251,7 +1268,8 @@ async fn swap_with_cleanup_preserves_pair_id_and_handoff() {
|
||||
let run_dir = run_dir_of(&agent.id);
|
||||
f.fs.put(&format!("{run_dir}/{CLAUDE_SEED_REL}"), b"{}");
|
||||
let pair = pair_uuid();
|
||||
f.handoffs.seed(&pair, "État au dernier tour : LP3-4.", Some("objectif"));
|
||||
f.handoffs
|
||||
.seed(&pair, "État au dernier tour : LP3-4.", Some("objectif"));
|
||||
|
||||
let host = nid(1);
|
||||
seed_live_agent_session(&f.sessions, agent.id, host, sid(42));
|
||||
@ -1273,7 +1291,10 @@ async fn swap_with_cleanup_preserves_pair_id_and_handoff() {
|
||||
// …and the pair id was preserved on the persisted leaf (P8d invariant).
|
||||
let (conv, running) = leaf_state(&f.fs, host).expect("leaf persisted");
|
||||
assert_eq!(conv.as_deref(), Some(pair.as_str()), "pair id preserved");
|
||||
assert!(!running, "engine running flag reset by invalidate_engine_link");
|
||||
assert!(
|
||||
!running,
|
||||
"engine running flag reset by invalidate_engine_link"
|
||||
);
|
||||
|
||||
// …and the handoff (keyed by that pair id) was re-injected into the new engine's
|
||||
// convention file — proving the relaunch threaded the preserved pair id. (The
|
||||
|
||||
Reference in New Issue
Block a user