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:
@@ -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