core: review fixes, init builds the agent, console without a terminal

Activation path (review T6R): a broken plugin or a stray file in
/plugins blocks no activation; candidates replace their own entries;
discovery has a 10 s deadline and its process is stopped after the
answer; an unchanged hash keeps plugin.prev.wasm; activations run in
their own task; the watcher is not recursive; no duplicate services;
rpc.retired waits for live processes of its version; reads and hashes
on blocking threads.

Guest I/O and per-call paths: a stdin write error is EOF, stdout and
stderr are drained before the status; stderr is lossy UTF-8 and goes
to the journal from its own reader; compiled components are freed;
shutdown starts no new processes; lock queues drop stopping processes;
compile before the admission gate; CommandPre per component;
core.unlock in order; null errors are successes; the journal thread
survives SQLite errors.

sausage init builds the agent with cargo.build; sausage without a
command creates ./world. The console reads plain lines without a
terminal. PROTOCOL.md and DECISIONS.md describe the decided rules. The
flake tests the whole workspace and checks the packaged cargo.build.
This commit is contained in:
2026-10-23 02:25:03 +01:00
parent 53b90a8ecf
commit d7b78ed0fd
27 changed files with 1800 additions and 352 deletions
+22 -28
View File
@@ -13,7 +13,7 @@ impl Host {
&self,
pid: String,
info: ProcessInfo,
component: Option<wasmtime::component::Component>,
component: Option<std::sync::Arc<crate::runtime::Prepared>>,
native: Option<NativePlugin>,
incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
@@ -38,7 +38,7 @@ impl Host {
&self,
pid: &str,
info: &ProcessInfo,
component: &wasmtime::component::Component,
component: &crate::runtime::Prepared,
mut incoming: mpsc::UnboundedReceiver<Value>,
has_id: bool,
) -> anyhow::Result<String> {
@@ -60,7 +60,17 @@ impl Host {
),
],
};
let mut instance = self.0.runtime.start(component, config).await?;
let mut instance = self
.0
.runtime
.start_logged(
component,
config,
self.0.journal.clone(),
pid.into(),
info.plugin.clone(),
)
.await?;
let mut initial = json!({
"jsonrpc": "2.0",
"method": info.method,
@@ -69,25 +79,17 @@ impl Host {
if has_id {
initial["id"] = json!(pid);
}
self.write_input(pid, &mut instance, initial).await?;
let mut stdin_open = true;
let mut stderr_open = true;
let mut stdin_open = self.forward_input(pid, &mut instance, Some(initial)).await;
loop {
tokio::select! {
line = instance.stderr.recv(), if stderr_open => {
stderr_open = self.stderr_line(pid,&info.plugin,line);
}
line = instance.stdout.recv() => {
if !self.stdout_line(pid,line) { break; }
}
value = incoming.recv(), if stdin_open => {
stdin_open = self.forward_input(pid,&mut instance,value).await?;
stdin_open = self.forward_input(pid,&mut instance,value).await;
}
}
}
while let Some(line) = instance.stderr.recv().await {
self.0.journal.write(pid, &info.plugin, "stderr", &line);
}
Ok(instance.wait().await.to_string())
}
@@ -97,14 +99,14 @@ impl Host {
pid: &str,
instance: &mut Instance,
value: Option<Value>,
) -> std::io::Result<bool> {
if let Some(value) = value {
self.write_input(pid, instance, value).await?;
Ok(true)
} else {
instance.stdin.take();
Ok(false)
) -> bool {
if let Some(value) = value
&& self.write_input(pid, instance, value).await.is_ok()
{
return true;
}
instance.stdin.take();
false
}
async fn write_input(
&self,
@@ -124,12 +126,4 @@ impl Host {
false
}
}
fn stderr_line(&self, pid: &str, plugin: &str, line: Option<String>) -> bool {
if let Some(line) = line {
self.0.journal.write(pid, plugin, "stderr", &line);
true
} else {
false
}
}
}