Files
IdeA/crates/application/src/diag.rs
Blomios 287681c198 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>
2026-06-20 08:56:39 +02:00

75 lines
3.1 KiB
Rust

//! Lightweight, dependency-free diagnostics sink for the orchestrator rendezvous.
//!
//! The orchestrator already emits best-effort traces through `eprintln!` (see
//! [`crate::orchestrator`]). When IdeA is launched by **clicking the AppImage**,
//! that stderr is discarded — so an inter-agent block (an `ask` that never gets
//! its `idea_reply`) leaves no retrievable trace. This module mirrors those
//! traces to a **persistent log file** the user can hand back for diagnosis,
//! while keeping the exact same "zero new dependency, never breaks the
//! rendezvous" discipline: every write is best-effort and infallible.
//!
//! - [`set_log_path`] is called once by the composition root (`app-tauri`) with
//! `<app-data>/logs/idea.log`. Until then (and in tests) writes go to stderr
//! only.
//! - [`diag`] (and the [`diag!`] macro) timestamps a line with epoch-millis — the
//! same clock as the conversation logs' `atMs` — and appends it to the file,
//! always also echoing to stderr so a terminal launch and the test suite still
//! see it.
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
/// The resolved log file path, set once at startup. `None` ⇒ stderr-only.
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
/// Serialises concurrent appends so interleaved rendezvous beacons stay on their
/// own lines (the orchestrator handles many connections in parallel).
static WRITE_LOCK: Mutex<()> = Mutex::new(());
/// Points the diagnostics sink at `path`, creating its parent directory.
///
/// Idempotent and infallible: a second call (or a failure to create the
/// directory) is ignored — diagnostics must never break startup. Called by the
/// composition root once the app-data directory is known.
pub fn set_log_path(path: PathBuf) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = LOG_PATH.set(path);
}
/// Current epoch-millis (same clock as the conversation logs' `atMs`).
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
/// Appends one timestamped diagnostic line. Best-effort and infallible: a missing
/// path or an I/O error is swallowed (the line still reaches stderr). Never call
/// in a hot loop — these are lifecycle beacons, not a metrics stream.
pub fn diag(msg: impl std::fmt::Display) {
let line = format!("{} {msg}", now_ms());
// Always echo to stderr: keeps a terminal launch and the test suite working,
// and preserves the pre-existing `eprintln!` behaviour for these beacons.
eprintln!("{line}");
if let Some(path) = LOG_PATH.get() {
let _guard = WRITE_LOCK.lock();
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(f, "{line}");
}
}
}
/// `diag!("...", ...)` — formats then forwards to [`diag`]. Mirrors `eprintln!`.
#[macro_export]
macro_rules! diag {
($($arg:tt)*) => {
$crate::diag::diag(format!($($arg)*))
};
}