feat(shipote): pipeline backoff + quota card + logs follow (fase M)
- PipelineSpec.restart_backoff_ms + restart_max_backoff_ms + restart_max: backoff exponencial entre relaunches (anti-thrash). take_pending_restarts aplica restart_max (0 = infinito); excedido = supervisor descartado con warning. Daemon hace tokio::sleep(backoff) antes del relaunch y escala current_backoff x2 hasta el cap. - shipote-shell card "Quota breaches": probe extiende con WorkspaceQuota por workspace. Color rojo si hay breaches, verde si no. - shipote logs --follow: poll cada 200ms al daemon, imprime suffix nuevo hasta que el comando termine. Sin cambios al protocolo. Best-effort: si el ring rota más rápido que el poll, se pierden bytes. 83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 24, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
|
||||
use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
|
||||
use shipote_protocol::{
|
||||
default_socket_path, read_frame, write_frame, CommandInfo, FlowInfo, Request, Response,
|
||||
WorkspaceStatsInfo, WorkspaceSummary,
|
||||
default_socket_path, read_frame, write_frame, CommandInfo, FlowInfo, QuotaReportInfo, Request,
|
||||
Response, WorkspaceStatsInfo, WorkspaceSummary,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -46,6 +46,8 @@ struct Shell {
|
||||
flows: Vec<FlowInfo>,
|
||||
/// History de RSS por workspace (últimas N samples).
|
||||
stats_history: std::collections::BTreeMap<String, std::collections::VecDeque<WorkspaceStatsInfo>>,
|
||||
/// Quota report fresco por workspace.
|
||||
quotas: std::collections::BTreeMap<String, QuotaReportInfo>,
|
||||
caps: Option<CapsSummary>,
|
||||
last_probe_ms: u64,
|
||||
recent_log: Option<(String, String)>,
|
||||
@@ -79,6 +81,7 @@ impl Shell {
|
||||
me.commands = snap.commands;
|
||||
me.saved_pipelines = snap.saved_pipelines;
|
||||
me.flows = snap.flows;
|
||||
me.quotas = snap.quotas;
|
||||
// Append a la history por workspace.
|
||||
for (ws_id, fresh) in &snap.fresh_stats {
|
||||
let h = me
|
||||
@@ -106,6 +109,7 @@ impl Shell {
|
||||
me.commands.clear();
|
||||
me.saved_pipelines.clear();
|
||||
me.flows.clear();
|
||||
me.quotas.clear();
|
||||
me.caps = None;
|
||||
me.recent_log = None;
|
||||
}
|
||||
@@ -126,6 +130,7 @@ impl Shell {
|
||||
saved_pipelines: Vec::new(),
|
||||
flows: Vec::new(),
|
||||
stats_history: std::collections::BTreeMap::new(),
|
||||
quotas: std::collections::BTreeMap::new(),
|
||||
caps: None,
|
||||
last_probe_ms: 0,
|
||||
recent_log: None,
|
||||
@@ -141,6 +146,8 @@ struct Snapshot {
|
||||
flows: Vec<FlowInfo>,
|
||||
/// Stats fresco por workspace (id.toString → stats).
|
||||
fresh_stats: std::collections::BTreeMap<String, WorkspaceStatsInfo>,
|
||||
/// Quota report fresco por workspace.
|
||||
quotas: std::collections::BTreeMap<String, QuotaReportInfo>,
|
||||
caps: CapsSummary,
|
||||
/// tail del log del comando más reciente (label + bytes). None si no hay.
|
||||
recent_log: Option<(String, String)>,
|
||||
@@ -170,6 +177,7 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
// Commands por workspace.
|
||||
let mut commands_map = std::collections::BTreeMap::new();
|
||||
let mut fresh_stats = std::collections::BTreeMap::new();
|
||||
let mut quotas = std::collections::BTreeMap::new();
|
||||
for w in &workspaces {
|
||||
write_frame(&mut stream, &Request::CommandList { workspace: w.id })
|
||||
.await
|
||||
@@ -192,6 +200,16 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
if let Response::WorkspaceStats { info } = resp {
|
||||
fresh_stats.insert(w.id.to_string(), info);
|
||||
}
|
||||
// Quota por workspace.
|
||||
write_frame(&mut stream, &Request::WorkspaceQuota { workspace: w.id })
|
||||
.await
|
||||
.map_err(|e| format!("write quota: {e}"))?;
|
||||
let resp: Response = read_frame(&mut stream)
|
||||
.await
|
||||
.map_err(|e| format!("read quota: {e}"))?;
|
||||
if let Response::WorkspaceQuota { info } = resp {
|
||||
quotas.insert(w.id.to_string(), info);
|
||||
}
|
||||
}
|
||||
|
||||
// Saved pipelines.
|
||||
@@ -294,6 +312,7 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
|
||||
saved_pipelines,
|
||||
flows,
|
||||
fresh_stats,
|
||||
quotas,
|
||||
caps,
|
||||
recent_log,
|
||||
})
|
||||
@@ -455,6 +474,21 @@ impl Render for Shell {
|
||||
"definiciones reusables vía run-saved".to_string()
|
||||
};
|
||||
|
||||
// Quota breaches por workspace.
|
||||
let mut breach_items: Vec<String> = Vec::new();
|
||||
for (ws_id, q) in &self.quotas {
|
||||
for b in &q.breaches {
|
||||
let short = &ws_id[20..];
|
||||
breach_items.push(format!("{short} {b}"));
|
||||
}
|
||||
}
|
||||
let breach_count = breach_items.len().to_string();
|
||||
let breach_descr = if breach_items.is_empty() {
|
||||
"todos los workspaces dentro de quota".to_string()
|
||||
} else {
|
||||
"ws_suffix · recurso · uso > limit".to_string()
|
||||
};
|
||||
|
||||
// Flow channels (data plane).
|
||||
let flow_count: usize = self.flows.iter().map(|f| f.sockets.len()).sum();
|
||||
let flow_items: Vec<String> = self
|
||||
@@ -547,6 +581,16 @@ impl Render for Shell {
|
||||
text,
|
||||
text_dim,
|
||||
&flow_items,
|
||||
))
|
||||
.child(stat_card(
|
||||
cx,
|
||||
"Quota breaches",
|
||||
breach_count,
|
||||
&breach_descr,
|
||||
if breach_items.is_empty() { accent_up } else { accent_down },
|
||||
text,
|
||||
text_dim,
|
||||
&breach_items,
|
||||
));
|
||||
|
||||
// Live tail del comando más reciente con output.
|
||||
|
||||
Reference in New Issue
Block a user