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:
sergio
2026-05-11 10:34:27 +00:00
parent 4c9d1b4c1d
commit c3f9c9e36a
7 changed files with 236 additions and 38 deletions
@@ -222,6 +222,25 @@ pub struct PipelineSpec {
/// Útil para pipelines de procesamiento continuo.
#[serde(default)]
pub restart_on_failure: bool,
/// Backoff inicial entre restarts (ms). Crece exponencialmente
/// hasta `restart_max_backoff_ms`. Default 200ms = ~5 restarts/s
/// inicial, escalando rápido.
#[serde(default = "default_restart_backoff")]
pub restart_backoff_ms: u64,
/// Backoff máximo (ms). Default 30s. El backoff no crece más allá.
#[serde(default = "default_restart_max_backoff")]
pub restart_max_backoff_ms: u64,
/// Máximo de restarts antes de dar up. `0` = infinito. Default 0.
/// Útil para fail-loud cuando un pipeline siempre falla.
#[serde(default)]
pub restart_max: u32,
}
fn default_restart_backoff() -> u64 {
200
}
fn default_restart_max_backoff() -> u64 {
30_000
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -502,6 +521,9 @@ mod subst_tests {
edges: vec![],
discern: DiscernPolicy::default(),
restart_on_failure: false,
restart_backoff_ms: 200,
restart_max_backoff_ms: 30_000,
restart_max: 0,
};
let out = substitute_vars(&spec, &vars).unwrap();
assert_eq!(out.label, "p-renamed");
@@ -522,6 +544,9 @@ mod subst_tests {
edges: vec![],
discern: DiscernPolicy::default(),
restart_on_failure: false,
restart_backoff_ms: 200,
restart_max_backoff_ms: 30_000,
restart_max: 0,
};
let out = substitute_vars(&spec, &vars).unwrap();
assert_eq!(out.label, "p-${UNDEFINED}");
@@ -603,6 +628,9 @@ mod tests {
}],
discern: DiscernPolicy::default(),
restart_on_failure: false,
restart_backoff_ms: 200,
restart_max_backoff_ms: 30_000,
restart_max: 0,
};
assert!(p.validate().is_err());
}