feat(shipote): collision detection + stats history server-side (fase O)
- Flow socket names usan pipeline_id full (ULID 26 chars) + edge_idx. Cero colisiones entre pipelines (ULID es único global). Fallback con suffix -N si el path existe (cap 1000 retries). - WorkspaceState.stats_history (VecDeque cap 64) — workspace_stats appendea cada call. API workspace_stats_history(id, tail). Protocol WorkspaceStatsHistory. Shell pide history al primer probe → sparkline hidratada al boot, sobrevive restart del shell. 84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 25, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -465,6 +465,31 @@ async fn dispatch(
|
||||
},
|
||||
},
|
||||
|
||||
Request::WorkspaceStatsHistory { workspace, tail } => {
|
||||
match mgr.workspace_stats_history(workspace, tail).await {
|
||||
Some(samples) => {
|
||||
let mapped: Vec<WorkspaceStatsInfo> = samples
|
||||
.into_iter()
|
||||
.map(|s| WorkspaceStatsInfo {
|
||||
commands_alive: s.commands_alive,
|
||||
commands_total: s.commands_total,
|
||||
rss_bytes: s.rss_bytes,
|
||||
rss_peak_bytes: s.rss_peak_bytes,
|
||||
cpu_usec: s.cpu_usec,
|
||||
cpu_percent: s.cpu_percent,
|
||||
cpu_cores: s.cpu_cores,
|
||||
source: s.source,
|
||||
uptime_ms: s.uptime_ms,
|
||||
})
|
||||
.collect();
|
||||
Response::WorkspaceStatsHistory { samples: mapped }
|
||||
}
|
||||
None => Response::Error {
|
||||
message: format!("workspace {workspace} not found"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Request::WorkspaceFullSummary { workspace } => {
|
||||
let stats = match mgr.workspace_stats(workspace).await {
|
||||
Some(s) => WorkspaceStatsInfo {
|
||||
|
||||
@@ -82,7 +82,20 @@ impl Shell {
|
||||
me.saved_pipelines = snap.saved_pipelines;
|
||||
me.flows = snap.flows;
|
||||
me.quotas = snap.quotas;
|
||||
// Append a la history por workspace.
|
||||
// Hidratar history server-side para workspaces
|
||||
// que no tenían history local (primer probe).
|
||||
for ws in &me.workspaces {
|
||||
let key = ws.id.to_string();
|
||||
if !me.stats_history.contains_key(&key) {
|
||||
if let Some(hydrated) = snap.hydrate_history.get(&key) {
|
||||
me.stats_history.insert(
|
||||
key.clone(),
|
||||
hydrated.iter().cloned().collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Append fresh sample a la history por workspace.
|
||||
for (ws_id, fresh) in &snap.fresh_stats {
|
||||
let h = me
|
||||
.stats_history
|
||||
@@ -148,6 +161,9 @@ struct Snapshot {
|
||||
fresh_stats: std::collections::BTreeMap<String, WorkspaceStatsInfo>,
|
||||
/// Quota report fresco por workspace.
|
||||
quotas: std::collections::BTreeMap<String, QuotaReportInfo>,
|
||||
/// Workspaces nuevos (no en history local): hidratamos history
|
||||
/// server-side al primer probe que los vea.
|
||||
hydrate_history: std::collections::BTreeMap<String, Vec<WorkspaceStatsInfo>>,
|
||||
caps: CapsSummary,
|
||||
/// tail del log del comando más reciente (label + bytes). None si no hay.
|
||||
recent_log: Option<(String, String)>,
|
||||
@@ -175,9 +191,11 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
};
|
||||
|
||||
// Batched: stats+quota+commands+flow_sockets en 1 roundtrip por ws.
|
||||
// Para workspaces nuevos, también pedimos history server-side.
|
||||
let mut commands_map = std::collections::BTreeMap::new();
|
||||
let mut fresh_stats = std::collections::BTreeMap::new();
|
||||
let mut quotas = std::collections::BTreeMap::new();
|
||||
let mut hydrate_history = std::collections::BTreeMap::new();
|
||||
for w in &workspaces {
|
||||
write_frame(&mut stream, &Request::WorkspaceFullSummary { workspace: w.id })
|
||||
.await
|
||||
@@ -193,6 +211,24 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
commands_map.insert(key, commands);
|
||||
}
|
||||
}
|
||||
// History server-side (para hidratar si el shell es nuevo).
|
||||
write_frame(
|
||||
&mut stream,
|
||||
&Request::WorkspaceStatsHistory {
|
||||
workspace: w.id,
|
||||
tail: 24, // mismo cap que STATS_HISTORY_LEN
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("write history: {e}"))?;
|
||||
let resp: Response = read_frame(&mut stream)
|
||||
.await
|
||||
.map_err(|e| format!("read history: {e}"))?;
|
||||
if let Response::WorkspaceStatsHistory { samples } = resp {
|
||||
if !samples.is_empty() {
|
||||
hydrate_history.insert(w.id.to_string(), samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Saved pipelines.
|
||||
@@ -296,6 +332,7 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
flows,
|
||||
fresh_stats,
|
||||
quotas,
|
||||
hydrate_history,
|
||||
caps,
|
||||
recent_log,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user