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>
75 lines
3.1 KiB
Rust
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)*))
|
|
};
|
|
}
|