feat(shipote): drain shutdown + persist live pipelines + batched query (fase N)

- Daemon SIGTERM/SIGINT: snapshot ANTES, stop_with_grace(1s) de todos
  los workspaces DESPUÉS. Grace permite app-level cleanup.
- Snapshot v3 con live_pipelines: pipeline_supervisors se persisten;
  daemon relanza al restore con sus recursos (Incarnator+DiscernPipeline).
  RestoreOutcome separado para que core no necesite incarnator.
  Forward-compat v1/v2 via #[serde(default)].
- WorkspaceFullSummary: stats+quota+commands+flow_sockets en 1 roundtrip.
  Shell reduce N×4 requests/probe a N×1 + 4 globales.

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:48:11 +00:00
parent c3f9c9e36a
commit a823c40fe1
4 changed files with 185 additions and 47 deletions
+10 -27
View File
@@ -174,42 +174,25 @@ fn probe_blocking(path: &std::path::Path) -> Result<Snapshot, String> {
other => return Err(format!("unexpected list resp: {other:?}")),
};
// Commands por workspace.
// Batched: stats+quota+commands+flow_sockets en 1 roundtrip por ws.
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 })
write_frame(&mut stream, &Request::WorkspaceFullSummary { workspace: w.id })
.await
.map_err(|e| format!("write commands: {e}"))?;
.map_err(|e| format!("write summary: {e}"))?;
let resp: Response = read_frame(&mut stream)
.await
.map_err(|e| format!("read commands: {e}"))?;
if let Response::CommandList { items } = resp {
if !items.is_empty() {
commands_map.insert(w.id.to_string(), items);
.map_err(|e| format!("read summary: {e}"))?;
if let Response::WorkspaceFullSummary { stats, quota, commands, .. } = resp {
let key = w.id.to_string();
fresh_stats.insert(key.clone(), stats);
quotas.insert(key.clone(), quota);
if !commands.is_empty() {
commands_map.insert(key, commands);
}
}
// Stats por workspace.
write_frame(&mut stream, &Request::WorkspaceStats { workspace: w.id })
.await
.map_err(|e| format!("write stats: {e}"))?;
let resp: Response = read_frame(&mut stream)
.await
.map_err(|e| format!("read stats: {e}"))?;
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.