diff --git a/Cargo.lock b/Cargo.lock index 0c7395fd..a2468fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "c0mpute-proto", @@ -493,7 +493,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "blake3", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "axum", @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.2.91" +version = "0.2.92" dependencies = [ "aes-gcm", "anyhow", @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "serde", @@ -592,7 +592,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.2.91" +version = "0.2.92" dependencies = [ "blake3", "bs58", @@ -605,7 +605,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "axum", @@ -627,7 +627,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.2.91" +version = "0.2.92" dependencies = [ "c0mpute-envelope", "serde", @@ -637,7 +637,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "async-trait", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "async-trait", @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.2.91" +version = "0.2.92" dependencies = [ "blake3", "hex", @@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.2.91" +version = "0.2.92" dependencies = [ "aes-gcm", "anyhow", @@ -710,7 +710,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "blake3", @@ -728,7 +728,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "c0mpute-proto", @@ -741,7 +741,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "hex", @@ -756,7 +756,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.2.91" +version = "0.2.92" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 1a388ba0..023863c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.2.91" +version = "0.2.92" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/apps/tui/package.json b/apps/tui/package.json index 392c8d32..3547a7de 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/tui", - "version": "0.2.91", + "version": "0.2.92", "private": true, "type": "module", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index 3671084e..e66a9b72 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute", - "version": "0.2.91", + "version": "0.2.92", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts index 7974c667..f406cc0a 100644 --- a/apps/web/src/app/releases/latest.json/route.ts +++ b/apps/web/src/app/releases/latest.json/route.ts @@ -28,7 +28,7 @@ export const dynamic = "force-dynamic"; * this in sync with the workspace version in `Cargo.toml` on each release so a * freshly built CLI reports "already latest" rather than a phantom upgrade. */ -const FALLBACK_VERSION = "0.2.91"; +const FALLBACK_VERSION = "0.2.92"; const FALLBACK_MIN_REQUIRED = "0.0.1"; type Channel = "stable" | "beta" | "nightly"; diff --git a/node/crates/c0mpute-core/src/diffusion.rs b/node/crates/c0mpute-core/src/diffusion.rs index 90a56c7b..d3c908c4 100644 --- a/node/crates/c0mpute-core/src/diffusion.rs +++ b/node/crates/c0mpute-core/src/diffusion.rs @@ -654,6 +654,8 @@ pub fn spawn_diffusion_runner( }; let started = Instant::now(); let job_id = job.accept.job_id.clone(); + // An auto-update waits for this to finish instead of killing it. + let _long = c0mpute_update::LongJob::start(); let result = run_job(&net, &store, &cache_root, &job).await; cleanup_job_dir(&cache_root, &job_id).await; match result { diff --git a/node/crates/c0mpute-core/src/job_progress.rs b/node/crates/c0mpute-core/src/job_progress.rs index f63099ed..98f52b18 100644 --- a/node/crates/c0mpute-core/src/job_progress.rs +++ b/node/crates/c0mpute-core/src/job_progress.rs @@ -111,11 +111,89 @@ pub enum Stored { const MAX_RESULTS: usize = 48; #[derive(Clone, Default)] -pub struct ResultStore(Arc>>); +pub struct ResultStore(Arc>>, Option>); + +/// On-disk form of a stored result's metadata (bytes live beside it). +#[derive(serde::Serialize, serde::Deserialize)] +struct StoredMeta { + at_ms: u64, + ok: bool, + content_type: String, + job_id: String, + worker: String, + status: u16, + message: String, +} + +fn static_content_type(t: &str) -> &'static str { + match t { + "audio/flac" => "audio/flac", + "audio/wav" => "audio/wav", + "image/png" => "image/png", + "video/mp4" => "video/mp4", + _ => "audio/mpeg", + } +} impl ResultStore { + /// A store that also keeps results under `dir`, so they outlive a restart. + pub fn persistent(dir: std::path::PathBuf) -> Self { + let _ = std::fs::create_dir_all(&dir); + // Drop files older than KEEP_MS left by earlier runs. + let now = now_ms(); + if let Ok(entries) = std::fs::read_dir(&dir) { + for e in entries.flatten() { + let old = e + .metadata() + .and_then(|m| m.modified()) + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .is_some_and(|d| now.saturating_sub(d.as_millis() as u64) > KEEP_MS); + if old { + let _ = std::fs::remove_file(e.path()); + } + } + } + Self(Arc::default(), Some(Arc::new(dir))) + } + + fn write_disk(&self, key: &str, at_ms: u64, stored: &Stored) { + let Some(dir) = self.1.as_deref() else { return }; + let meta = match stored { + Stored::Done { bytes, content_type, job_id, worker } => { + if std::fs::write(dir.join(format!("{key}.bin")), bytes.as_slice()).is_err() { + return; + } + StoredMeta { at_ms, ok: true, content_type: content_type.to_string(), job_id: job_id.clone(), worker: worker.clone(), status: 200, message: String::new() } + } + Stored::Failed { status, message } => StoredMeta { + at_ms, ok: false, content_type: String::new(), job_id: String::new(), worker: String::new(), status: *status, message: message.clone(), + }, + }; + if let Ok(json) = serde_json::to_vec(&meta) { + let _ = std::fs::write(dir.join(format!("{key}.json")), json); + } + } + + fn read_disk(&self, key: &str) -> Option<(u64, Stored)> { + let dir = self.1.as_deref()?; + let meta: StoredMeta = serde_json::from_slice(&std::fs::read(dir.join(format!("{key}.json"))).ok()?).ok()?; + let stored = if meta.ok { + Stored::Done { + bytes: Arc::new(std::fs::read(dir.join(format!("{key}.bin"))).ok()?), + content_type: static_content_type(&meta.content_type), + job_id: meta.job_id, + worker: meta.worker, + } + } else { + Stored::Failed { status: meta.status, message: meta.message } + }; + Some((meta.at_ms, stored)) + } + pub fn put(&self, key: &str, stored: Stored) { let now = now_ms(); + self.write_disk(key, now, &stored); let Ok(mut map) = self.0.lock() else { return }; map.retain(|_, (at, _)| now.saturating_sub(*at) < KEEP_MS); while map.len() >= MAX_RESULTS { @@ -129,11 +207,72 @@ impl ResultStore { pub fn get(&self, key: &str) -> Option { let now = now_ms(); - let map = self.0.lock().ok()?; - map.get(key) - .filter(|(at, _)| now.saturating_sub(*at) < KEEP_MS) - .map(|(_, s)| s.clone()) + let hit = { + let map = self.0.lock().ok()?; + map.get(key).cloned() + }; + // After a restart the memory is empty; the disk copy still answers. + let (at, stored) = hit.or_else(|| self.read_disk(key))?; + (now.saturating_sub(at) < KEEP_MS).then_some(stored) + } +} + +/// A tracked request that has not finished: enough to run it again after a +/// gateway restart, under the same request id. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct PendingJob { + pub workload: String, + pub spec: serde_json::Value, + pub required: Vec, + pub max_bytes: usize, + pub started_at_ms: u64, +} + +/// Unfinished tracked requests on disk (`/.json`). +#[derive(Clone, Default)] +pub struct PendingStore(Option>); + +impl PendingStore { + pub fn persistent(dir: std::path::PathBuf) -> Self { + let _ = std::fs::create_dir_all(&dir); + Self(Some(Arc::new(dir))) } + + pub fn save(&self, key: &str, job: &PendingJob) { + let Some(dir) = self.0.as_deref() else { return }; + if let Ok(json) = serde_json::to_vec(job) { + let _ = std::fs::write(dir.join(format!("{key}.json")), json); + } + } + + pub fn remove(&self, key: &str) { + if let Some(dir) = self.0.as_deref() { + let _ = std::fs::remove_file(dir.join(format!("{key}.json"))); + } + } + + /// Unfinished requests younger than the keep window; older ones are dropped. + pub fn take_resumable(&self) -> Vec<(String, PendingJob)> { + let Some(dir) = self.0.as_deref() else { return Vec::new() }; + let now = now_ms(); + let mut out = Vec::new(); + for e in std::fs::read_dir(dir).into_iter().flatten().flatten() { + let path = e.path(); + let Some(key) = path.file_stem().and_then(|s| s.to_str()).map(str::to_string) else { continue }; + let job = std::fs::read(&path).ok().and_then(|b| serde_json::from_slice::(&b).ok()); + match job { + Some(job) if valid_key(&key) && now.saturating_sub(job.started_at_ms) < KEEP_MS => out.push((key, job)), + _ => { + let _ = std::fs::remove_file(&path); + } + } + } + out + } +} + +pub fn now_ms_pub() -> u64 { + now_ms() } /// A caller-chosen request id: short and URL-safe. @@ -212,6 +351,38 @@ mod tests { assert!(store.0.lock().unwrap().len() <= MAX_RESULTS, "bounded"); } + #[test] + fn results_and_pending_jobs_survive_a_restart_on_disk() { + let dir = std::env::temp_dir().join(format!("c0-jobs-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + { + let results = ResultStore::persistent(dir.join("results")); + results.put( + "song-9", + Stored::Done { bytes: Arc::new(b"ID3song".to_vec()), content_type: "audio/mpeg", job_id: "j9".into(), worker: "w9".into() }, + ); + let pending = PendingStore::persistent(dir.join("jobs")); + pending.save( + "song-10", + &PendingJob { workload: "music.txt2music".into(), spec: serde_json::json!({"prompt": "x"}), required: vec![], max_bytes: 1, started_at_ms: now_ms() }, + ); + pending.save( + "stale", + &PendingJob { workload: "music.txt2music".into(), spec: serde_json::json!({}), required: vec![], max_bytes: 1, started_at_ms: 1 }, + ); + } + // "Restart": fresh stores over the same directories. + let results = ResultStore::persistent(dir.join("results")); + assert!(matches!(results.get("song-9"), Some(Stored::Done { ref bytes, content_type: "audio/mpeg", ref worker, .. }) if bytes.as_slice() == b"ID3song" && worker == "w9")); + let pending = PendingStore::persistent(dir.join("jobs")); + let resumable = pending.take_resumable(); + assert_eq!(resumable.len(), 1, "the stale one is dropped"); + assert_eq!(resumable[0].0, "song-10"); + pending.remove("song-10"); + assert!(pending.take_resumable().is_empty()); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn request_ids_must_be_short_and_url_safe() { assert!(valid_key("17a1b21d-a95e-4345-a3fe-d14dfd6868e3")); diff --git a/node/crates/c0mpute-core/src/music.rs b/node/crates/c0mpute-core/src/music.rs index 874634ce..0199f763 100644 --- a/node/crates/c0mpute-core/src/music.rs +++ b/node/crates/c0mpute-core/src/music.rs @@ -662,6 +662,8 @@ pub fn spawn_music_runner( }; let started = Instant::now(); let job_id = job.accept.job_id.clone(); + // An auto-update waits for this to finish instead of killing it. + let _long = c0mpute_update::LongJob::start(); let result = run_job(&net, &store, &cache_root, &job).await; cleanup_job_dir(&cache_root, &job_id).await; match result { diff --git a/node/crates/c0mpute-core/src/speech_gateway.rs b/node/crates/c0mpute-core/src/speech_gateway.rs index 5fc92d78..c3818873 100644 --- a/node/crates/c0mpute-core/src/speech_gateway.rs +++ b/node/crates/c0mpute-core/src/speech_gateway.rs @@ -64,6 +64,8 @@ struct GatewayState { progress: crate::job_progress::ProgressBoard, /// Finished tracked requests, collectable at GET /v1/result/{id}. results: crate::job_progress::ResultStore, + /// Unfinished tracked requests, re-run after a gateway restart. + pending: crate::job_progress::PendingStore, } #[derive(Debug)] @@ -148,7 +150,10 @@ pub async fn serve(bind: std::net::SocketAddr, token: String, max_price_usd: f64 .unwrap_or_else(|| std::path::PathBuf::from("./c0mpute-data")) .join("gateway-penalties.json"), ); - let app = router(supervisor.libp2p, supervisor.store, token, max_price_usd, penalties); + // The served gateway keeps tracked jobs on disk, so a restart (an + // auto-update) resumes them; the loopback tests use `router` in memory. + let data_dir = crate::config::data_dir().unwrap_or_else(|| std::path::PathBuf::from("./c0mpute-data")); + let app = router_with(supervisor.libp2p, supervisor.store, token, max_price_usd, penalties, Some(data_dir)); let listener = tokio::net::TcpListener::bind(bind) .await .with_context(|| format!("bind c0mpute speech gateway at {bind}"))?; @@ -166,6 +171,25 @@ pub fn router( max_price_usd: f64, penalties: crate::penalty_box::PenaltyBox, ) -> Router { + router_with(net, store, token, max_price_usd, penalties, None) +} + +/// `router`, optionally keeping tracked jobs and results under `persist`. +pub fn router_with( + net: Arc, + store: ChunkStore, + token: String, + max_price_usd: f64, + penalties: crate::penalty_box::PenaltyBox, + persist: Option, +) -> Router { + let (results, pending) = match &persist { + Some(dir) => ( + crate::job_progress::ResultStore::persistent(dir.join("gateway-results")), + crate::job_progress::PendingStore::persistent(dir.join("gateway-jobs")), + ), + None => Default::default(), + }; let state = GatewayState { net, store, @@ -181,9 +205,11 @@ pub fn router( embed_slots: Arc::new(Semaphore::new(MAX_CONCURRENT_EMBED_JOBS)), sessions: crate::speech_session::SessionStore::new(), progress: crate::job_progress::ProgressBoard::default(), - results: crate::job_progress::ResultStore::default(), + results, + pending, penalties, }; + resume_pending(&state); Router::new() .route("/healthz", get(healthz)) .route("/v1/voices", get(voices)) @@ -1114,12 +1140,26 @@ async fn run_tracked( }); }; state.progress.start(&key); + // On disk until it finishes, so a gateway restart can run it again. + state.pending.save( + &key, + &crate::job_progress::PendingJob { + workload: workload.to_string(), + spec: spec.clone(), + required: required.clone(), + max_bytes, + started_at_ms: crate::job_progress::now_ms_pub(), + }, + ); let st = state.clone(); let task_key = key.clone(); let task = tokio::spawn(crate::job_progress::CURRENT.scope( (state.progress.clone(), key.clone()), async move { let _slot = slot; + // A gateway auto-update waits for tracked jobs (a caller is + // waiting on them) instead of cutting them off. + let _long = c0mpute_update::LongJob::start(); let result = run_speech_job(&st, workload, spec, required, max_bytes).await; crate::job_progress::set_stage(if result.is_ok() { "done" } else { "failed" }); let stored = match result { @@ -1132,6 +1172,7 @@ async fn run_tracked( Err(ApiError(status, message)) => Stored::Failed { status: status.as_u16(), message }, }; st.results.put(&task_key, stored.clone()); + st.pending.remove(&task_key); stored }, )); @@ -1145,6 +1186,53 @@ async fn run_tracked( } } +/// After a restart: run again every tracked request that had not finished, +/// under the same request id, so `GET /v1/result/{id}` still delivers it. +/// What a resumed request needs: its workload, its queue and its content type. +type ResumeTarget = (&'static str, Arc, fn(&[u8]) -> &'static str); + +fn resume_pending(state: &GatewayState) { + use crate::job_progress::Stored; + for (key, job) in state.pending.take_resumable() { + let (workload, slots, content_type): ResumeTarget = + match job.workload.as_str() { + crate::music::WORKLOAD_TXT2MUSIC => ( + crate::music::WORKLOAD_TXT2MUSIC, + state.music_slots.clone(), + |b| crate::music::audio_content_type(b).unwrap_or("audio/mpeg"), + ), + crate::diffusion::WORKLOAD_TXT2IMG => { + (crate::diffusion::WORKLOAD_TXT2IMG, state.image_slots.clone(), |_| "image/png") + } + _ => { + state.pending.remove(&key); + continue; + } + }; + tracing::info!(%key, workload, "resuming a tracked request after a restart"); + state.progress.start(&key); + let st = state.clone(); + let task_key = key.clone(); + tokio::spawn(crate::job_progress::CURRENT.scope((state.progress.clone(), key), async move { + let Ok(_slot) = slots.acquire_owned().await else { return }; + let _long = c0mpute_update::LongJob::start(); + let result = run_speech_job(&st, workload, job.spec, job.required, job.max_bytes).await; + crate::job_progress::set_stage(if result.is_ok() { "done" } else { "failed" }); + let stored = match result { + Ok((outcome, bytes)) => Stored::Done { + content_type: content_type(&bytes), + bytes: Arc::new(bytes), + job_id: outcome.job_id, + worker: outcome.accepted_bid.bidder_peer_id, + }, + Err(ApiError(status, message)) => Stored::Failed { status: status.as_u16(), message }, + }; + st.results.put(&task_key, stored); + st.pending.remove(&task_key); + })); + } +} + /// 200 with the bytes, their type and the job headers. fn stored_response(done: crate::job_progress::Stored) -> Response { let crate::job_progress::Stored::Done { bytes, content_type, job_id, worker } = done else { diff --git a/node/crates/c0mpute-core/src/video.rs b/node/crates/c0mpute-core/src/video.rs index 861ecedf..049ccb38 100644 --- a/node/crates/c0mpute-core/src/video.rs +++ b/node/crates/c0mpute-core/src/video.rs @@ -631,6 +631,7 @@ pub fn spawn_video_runner( }; let started = Instant::now(); let job_id = job.accept.job_id.clone(); + let _long = c0mpute_update::LongJob::start(); let result = run_video_job(&net, &store, &cache_root, &workload, &image, &job).await; crate::runner::cleanup_job_dir(&cache_root, &job_id).await; match result { diff --git a/node/crates/c0mpute-update/src/lib.rs b/node/crates/c0mpute-update/src/lib.rs index c5d64bfd..dcb0fc5b 100644 --- a/node/crates/c0mpute-update/src/lib.rs +++ b/node/crates/c0mpute-update/src/lib.rs @@ -398,6 +398,57 @@ fn extract_and_swap(tarball: &[u8], artifact: &str) -> Result<()> { Ok(()) } +// ---- drain before re-exec ------------------------------------------------ +// +// A re-exec replaces the process, so whatever it was doing dies with it: a +// song or image half rendered on a worker, a song a gateway was waiting on. +// Long jobs hold a `LongJob` guard; the poller re-execs only when none is +// running (or after MAX_DRAIN, so a stuck job cannot pin an old binary). + +static LONG_JOBS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +/// Held for the life of a job a restart would kill (see `poll_loop`). +#[must_use = "the job only counts while the guard is alive"] +pub struct LongJob(()); + +impl LongJob { + pub fn start() -> Self { + LONG_JOBS.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + LongJob(()) + } +} + +impl Drop for LongJob { + fn drop(&mut self) { + LONG_JOBS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + } +} + +/// How many long jobs this process is running now. +pub fn long_jobs_running() -> usize { + LONG_JOBS.load(std::sync::atomic::Ordering::SeqCst) +} + +/// Longest the poller waits for running jobs before re-executing anyway. +pub const MAX_DRAIN: Duration = Duration::from_secs(30 * 60); + +/// Wait (up to `max`) until no long job runs; returns how many still run. +pub async fn drain(max: Duration, every: Duration) -> usize { + let started = std::time::Instant::now(); + let mut logged = false; + loop { + let running = long_jobs_running(); + if running == 0 || started.elapsed() >= max { + return running; + } + if !logged { + info!(running, "auto-upgrade: waiting for running jobs to finish before re-executing"); + logged = true; + } + tokio::time::sleep(every).await; + } +} + /// Long-running background poller. Every `interval` it checks the feed and, if /// a newer release exists, downloads + verifies + swaps the binary, then /// re-executes into it so the update applies without an operator restart. @@ -427,6 +478,10 @@ pub async fn poll_loop(current_version: String, release_feed_url: String, interv info!(%from, %to, "auto-upgrade: installed new binary"); #[cfg(unix)] if let Some(exe) = &exe { + let still = drain(MAX_DRAIN, Duration::from_secs(10)).await; + if still > 0 { + warn!(still, "auto-upgrade: jobs still running after the drain limit; re-executing anyway"); + } info!("auto-upgrade: re-executing into the new binary"); // reexec only returns on failure (success replaces us). let e = reexec(exe); @@ -447,6 +502,25 @@ pub async fn poll_loop(current_version: String, release_feed_url: String, interv } } +#[cfg(test)] +mod drain_tests { + use super::*; + + #[tokio::test] + async fn drain_waits_for_long_jobs_and_gives_up_at_the_limit() { + assert_eq!(drain(Duration::from_millis(50), Duration::from_millis(5)).await, 0); + let job = LongJob::start(); + assert_eq!(long_jobs_running(), 1); + let t = std::time::Instant::now(); + assert_eq!(drain(Duration::from_millis(60), Duration::from_millis(5)).await, 1, "gives up at the limit"); + assert!(t.elapsed() >= Duration::from_millis(60)); + let waiter = tokio::spawn(drain(Duration::from_secs(5), Duration::from_millis(5))); + tokio::time::sleep(Duration::from_millis(30)).await; + drop(job); + assert_eq!(waiter.await.unwrap(), 0, "returns once the job ends"); + } +} + /// Replace the current process with the (freshly swapped) binary at `exe`, /// preserving PID, session, and the daemon's redirected stdio. Detach flags are /// dropped — we are already the detached worker, so re-running `-d`/`-a` would diff --git a/package.json b/package.json index b948eb7b..17abd2f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute-monorepo", - "version": "0.2.91", + "version": "0.2.92", "private": true, "workspaces": [ "apps/*", diff --git a/packages/shared/package.json b/packages/shared/package.json index 8b959c0b..d1bcfb61 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/shared", - "version": "0.2.91", + "version": "0.2.92", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/plugins/ad-generator-agent/module.toml b/plugins/ad-generator-agent/module.toml index 386d920e..adf5d59f 100644 --- a/plugins/ad-generator-agent/module.toml +++ b/plugins/ad-generator-agent/module.toml @@ -21,7 +21,7 @@ [module] id = "ad-generator-agent" name = "Ad Generator Agent — every ad type from one URL" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "available" diff --git a/plugins/ads-manager/module.toml b/plugins/ads-manager/module.toml index 0c157a5a..2a54f22b 100644 --- a/plugins/ads-manager/module.toml +++ b/plugins/ads-manager/module.toml @@ -38,7 +38,7 @@ [module] id = "ads-manager" name = "Ads Manager — shared ad operations layer" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "partial" diff --git a/plugins/coinpay/module.toml b/plugins/coinpay/module.toml index 636711ae..213df005 100644 --- a/plugins/coinpay/module.toml +++ b/plugins/coinpay/module.toml @@ -7,7 +7,7 @@ [module] id = "coinpay" name = "CoinPay — DID, payments, escrow, reputation" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "available" diff --git a/plugins/crawl/module.toml b/plugins/crawl/module.toml index 3d685222..e6f75fd8 100644 --- a/plugins/crawl/module.toml +++ b/plugins/crawl/module.toml @@ -8,7 +8,7 @@ [module] id = "crawl" name = "Crawl — distributed web fetching" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "data" status = "available" diff --git a/plugins/crowdgpt/module.toml b/plugins/crowdgpt/module.toml index 9b19383f..e95bcb57 100644 --- a/plugins/crowdgpt/module.toml +++ b/plugins/crowdgpt/module.toml @@ -43,7 +43,7 @@ [module] id = "crowdgpt" name = "CrowdGPT: donate idle compute" -version = "0.2.91" +version = "0.2.92" kind = "donation" category = "ai" status = "available" diff --git a/plugins/ctv-ads/module.toml b/plugins/ctv-ads/module.toml index 48250c5d..0173d4bb 100644 --- a/plugins/ctv-ads/module.toml +++ b/plugins/ctv-ads/module.toml @@ -28,7 +28,7 @@ [module] id = "ctv-ads" name = "CTV Ads — VAST and VMAP from every gateway" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "partial" diff --git a/plugins/diffusion/module.toml b/plugins/diffusion/module.toml index 39a0408f..f9c3f703 100644 --- a/plugins/diffusion/module.toml +++ b/plugins/diffusion/module.toml @@ -11,7 +11,7 @@ [module] id = "diffusion" name = "Diffusion — image generation" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/embed/module.toml b/plugins/embed/module.toml index da7315c2..33d4b521 100644 --- a/plugins/embed/module.toml +++ b/plugins/embed/module.toml @@ -19,7 +19,7 @@ [module] id = "embed" name = "Embeddings — vector generation" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/gameserver/module.toml b/plugins/gameserver/module.toml index 8eecf878..08311074 100644 --- a/plugins/gameserver/module.toml +++ b/plugins/gameserver/module.toml @@ -55,7 +55,7 @@ [module] id = "gameserver" name = "Game server — Minecraft / Factorio / Valheim" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/hosting/module.toml b/plugins/hosting/module.toml index 9f10ac2d..0d03cfc0 100644 --- a/plugins/hosting/module.toml +++ b/plugins/hosting/module.toml @@ -30,7 +30,7 @@ [module] id = "hosting" name = "Hosting — p2p static sites" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "data" status = "available" diff --git a/plugins/infernet/module.toml b/plugins/infernet/module.toml index 5748e0d1..5c407766 100644 --- a/plugins/infernet/module.toml +++ b/plugins/infernet/module.toml @@ -22,7 +22,7 @@ [module] id = "infernet" name = "Infernet Protocol — AI inference" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/live-stream/module.toml b/plugins/live-stream/module.toml index 673bc2c9..f35d7870 100644 --- a/plugins/live-stream/module.toml +++ b/plugins/live-stream/module.toml @@ -34,7 +34,7 @@ [module] id = "live-stream" name = "Live Stream — p2p HLS live streaming" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "media" status = "available" diff --git a/plugins/music/module.toml b/plugins/music/module.toml index 41b8ed46..4061fdfb 100644 --- a/plugins/music/module.toml +++ b/plugins/music/module.toml @@ -15,7 +15,7 @@ [module] id = "music" name = "Music — song generation (ACE-Step 1.5)" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/ocr/module.toml b/plugins/ocr/module.toml index 57696f63..409764fa 100644 --- a/plugins/ocr/module.toml +++ b/plugins/ocr/module.toml @@ -19,7 +19,7 @@ [module] id = "ocr" name = "OCR — document text extraction" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/podcast-ads/module.toml b/plugins/podcast-ads/module.toml index b30e9525..3b628220 100644 --- a/plugins/podcast-ads/module.toml +++ b/plugins/podcast-ads/module.toml @@ -31,7 +31,7 @@ [module] id = "podcast-ads" name = "Podcast Ads — dynamic ad insertion" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "partial" diff --git a/plugins/podcasting/module.toml b/plugins/podcasting/module.toml index 03862972..130ee9fb 100644 --- a/plugins/podcasting/module.toml +++ b/plugins/podcasting/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasting" name = "Podcasting — p2p podcast publishing" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "media" status = "available" diff --git a/plugins/podcasts/module.toml b/plugins/podcasts/module.toml index d1b8dc10..f1bba4cb 100644 --- a/plugins/podcasts/module.toml +++ b/plugins/podcasts/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasts" name = "Podcasts — p2p podcast listener" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "media" status = "available" diff --git a/plugins/render/module.toml b/plugins/render/module.toml index db181930..f3829abf 100644 --- a/plugins/render/module.toml +++ b/plugins/render/module.toml @@ -33,7 +33,7 @@ [module] id = "render" name = "Render — Blender 3D rendering" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "media" status = "available" diff --git a/plugins/secure-chat/module.toml b/plugins/secure-chat/module.toml index ccc5c205..3de4df49 100644 --- a/plugins/secure-chat/module.toml +++ b/plugins/secure-chat/module.toml @@ -21,7 +21,7 @@ [module] id = "secure-chat" name = "Secure Chat — E2E encrypted p2p messaging" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "applications" status = "available" diff --git a/plugins/storage/module.toml b/plugins/storage/module.toml index 39666970..ce6917da 100644 --- a/plugins/storage/module.toml +++ b/plugins/storage/module.toml @@ -20,7 +20,7 @@ [module] id = "storage" name = "Storage — Reed-Solomon shard service" -version = "0.2.91" +version = "0.2.92" kind = "service" category = "data" status = "available" diff --git a/plugins/train/module.toml b/plugins/train/module.toml index b7bb8563..b58d753e 100644 --- a/plugins/train/module.toml +++ b/plugins/train/module.toml @@ -55,7 +55,7 @@ [module] id = "train" name = "Fine-tune — LoRA training" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/transcode/module.toml b/plugins/transcode/module.toml index 8182ca9e..4db4b0a1 100644 --- a/plugins/transcode/module.toml +++ b/plugins/transcode/module.toml @@ -7,7 +7,7 @@ [module] id = "transcode" name = "FFmpeg Transcoding" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "media" status = "available" diff --git a/plugins/tts/module.toml b/plugins/tts/module.toml index cd461f33..532b3758 100644 --- a/plugins/tts/module.toml +++ b/plugins/tts/module.toml @@ -15,7 +15,7 @@ [module] id = "tts" name = "TTS — text-to-speech" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/video/module.toml b/plugins/video/module.toml index 8698948f..6502e19a 100644 --- a/plugins/video/module.toml +++ b/plugins/video/module.toml @@ -65,7 +65,7 @@ [module] id = "video" name = "Video — generation (Wan 2.2, Wan 2.1 1.3B on 8 GiB or CPU, LTX-2.3 with audio)" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/webhook/module.toml b/plugins/webhook/module.toml index b37d363e..c7f0a1ca 100644 --- a/plugins/webhook/module.toml +++ b/plugins/webhook/module.toml @@ -7,7 +7,7 @@ [module] id = "webhook" name = "Webhooks — paid, end-to-end encrypted delivery" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/whisper/module.toml b/plugins/whisper/module.toml index 7cabad26..a33cae6e 100644 --- a/plugins/whisper/module.toml +++ b/plugins/whisper/module.toml @@ -7,7 +7,7 @@ [module] id = "whisper" name = "Whisper — speech-to-text" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "ai" status = "available" diff --git a/plugins/workflow-step/module.toml b/plugins/workflow-step/module.toml index 2cef36f7..adbab601 100644 --- a/plugins/workflow-step/module.toml +++ b/plugins/workflow-step/module.toml @@ -14,7 +14,7 @@ [module] id = "workflow-step" name = "Workflow step — webhook workflow steps (meshhook)" -version = "0.2.91" +version = "0.2.92" kind = "workload" category = "infrastructure" status = "available"