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
+71 -1
View File
@@ -177,7 +177,34 @@ pub async fn execute_line(host: &Host, line: &str) -> Result<String> {
}
i += 1;
}
call(host, "core.log", params, false).await
match host.call("core.log", params).await {
Ok(entries) => entries
.as_array()
.unwrap()
.iter()
.map(|entry| {
let time =
chrono::DateTime::parse_from_rfc3339(entry["time"].as_str().unwrap())?
.with_timezone(&chrono::Local);
let pid = entry["pid"].as_str().unwrap();
let prefix = format!("{}-", entry["plugin"].as_str().unwrap());
let short = pid
.strip_prefix(&prefix)
.and_then(|uuid| uuid.get(..8))
.map(|uuid| format!("{prefix}{uuid}"))
.unwrap_or_else(|| pid.to_owned());
Ok(format!(
"{} {} {} {}",
time.format("%H:%M:%S%.3f"),
short,
entry["stream"].as_str().unwrap(),
entry["line"].as_str().unwrap()
))
})
.collect::<Result<Vec<_>>>()
.map(|lines| lines.join("\n")),
Err(error) => Ok(format!("error: {}", pretty(&error)?)),
}
}
"activate" if words.len() == 2 => {
call(
@@ -245,7 +272,11 @@ fn indent(text: &str) -> String {
}
/// Run the line editor on a dedicated thread, with notifications through its external printer.
/// Without a terminal (for example, input from a pipe), read plain lines instead.
pub async fn run_input(host: Host) -> Result<()> {
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return run_plain(host).await;
}
let history = host.0.world.state_dir("core").join("history");
let (ready, started) = tokio::sync::oneshot::channel();
let (sender, mut lines) = tokio::sync::mpsc::unbounded_channel::<
@@ -323,3 +354,42 @@ pub async fn run_input(host: Host) -> Result<()> {
};
tokio::select! {result=commands=>result,result=print_notifications=>result}
}
/// Read plain lines from stdin, without line editing or history. A detached
/// thread reads them, so that a blocked read does not hold the runtime open.
async fn run_plain(host: Host) -> Result<()> {
let (sender, mut lines) = tokio::sync::mpsc::unbounded_channel::<String>();
std::thread::spawn(move || {
for line in std::io::stdin().lines() {
let Ok(line) = line else { break };
if sender.send(line).is_err() {
break;
}
}
});
let mut notifications = host.subscribe();
let print_notifications = async move {
loop {
match notifications.recv().await {
Ok(value) => println!("{}", indent(&notification_text(&value))),
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(tokio::sync::broadcast::error::RecvError::Closed) => return Ok(()),
}
}
};
let commands = async {
while let Some(line) = lines.recv().await {
let text = execute_line(&host, &line)
.await
.unwrap_or_else(|error| format!("error: {error}"));
if !text.is_empty() {
println!("{}", indent(&text));
}
if line.trim() == "quit" {
break;
}
}
Ok(())
};
tokio::select! {result=commands=>result,result=print_notifications=>result}
}