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.
Holds the history lock for a turn, 600 s grace period, interim texts as
core.notify. Tests with a fake LLM in the real core. The SDK tests of
/proc, /self, /run, and detached notifications now run.
- cargo.build (native): one source copy, tests under WASI, release
build to /state/cargo/builds/<n>/, process groups killed on cancel.
The host writes the SDK copy to /plugins/cargo/sdk/ at start.
- sausage test-runner: one instance per test, in parallel, with its own
mount table (empty world, package folder at its path); answers
core.lock, core.unlock, core.notify, core.log.
- The console: rustyline, notifications while typing, ls/cat through the
process view, methods, ps, log, activate, revert, stop, kill.
- core.notify on stdout is handled in order, before the response.
- /proc is a core layer: core/ (PROTOCOL.md, graph.json, graph.dot,
locks.json, plugins/<name>/discover.json and client.rs) and one folder
per process (info.json, log, and its /self files).
- Locks are released only after the task ended, the store dropped, and
in-flight file operations returned.
- core.activate and core.revert with copies, discovery, compatibility
checks of imports and consumers, groups, force. An admission gate
makes the snapshot of old processes complete.
- Graph of discover documents by hash, refreshed by a file watcher.
- Upgrades: grace periods (x-sausage-grace), stop, service restarts,
rpc.retired.
- SDK plugins in the real core (cli/tests/sdk.rs).
Part 1 fixes: complete stdout writes, buffered stdin, Rust type names
in schemas, enum variant docs, -32603 for serde errors in handlers,
crate docs and modules. End-to-end check of a generated consumer through
a fake core.
Checkpoint. Known issues, fixed in part 2: short writes on stdout,
byte-wise stdin reads, Result suffix on all result schemas, lost enum
variant docs, serde errors as invalid params, missing item docs.
Params go from consumer to provider, results from provider to consumer.
Sender objects are closed, receiver objects open unless they reject
additional properties. Covers the Draft 7 subset with refs, cycles,
nullable values, string enums, arrays, and integer ranges, rejects other
constructs, and gives counterexamples. Property test over random subset
schemas.
Upgrades retire old processes after a grace period, then call
rpc.retired. Services live in boot.json. One preopen / with a mount
table in the sausage-vfs crate, with guards for in-flight file
operations. Draft 7 and our own compatibility check on a fixed subset.
Imports register with inventory. Rules for liveness, stop states,
routing, locks, activation, endpoints (rpc.endpoint handshake), boot,
builds, and a 10 ms scheduling tick.
The design from the redesign discussion: a process per call, JSON-RPC on
stdin and stdout, a virtual /proc, /self, and /run, TCP endpoints, locks,
a central journal, generated imports with interface checks at activation,
and the SDK with examples.
The Nix package, the dev shell, and the test check set CC_wasm32_wasip2
and AR_wasm32_wasip2 to the clang of pkgsCross.wasi32 (wasi-libc), so
build scripts with the cc crate work, for example rusqlite with bundled
SQLite. The runtime-toolchain check links C into a probe with the
packaged host and an empty PATH.
A stopped actor gets the new version installed, keeps its mailbox and does
not run main until the next start. Running actors still restart on the new
version. The upgraded and rolled_back answers say whether the actor runs.
New config field `thinking` (OpenAI: reasoning_effort still wins): Anthropic adaptive thinking +
output_config.effort (thinking blocks kept and sent back), OpenAI reasoning.effort. A 400 that rejects
the effort is retried with the nearest accepted level (from the error's list, lower on a tie), logged,
never dropped to off.
buildDepsOnly builds the dependencies of the host and the tests once
(keyed only on Cargo.toml/Cargo.lock); packages.default and checks.tests
reuse them, so a change to our crates no longer recompiles every crate.
checks.package builds the binary the host loop runs.
The host watches the whole world folder recursively with the notify crate
(any writer counts, .sausage-tmp-* skipped, ~100 ms coalescing per path).
A new or changed actor/<id>/actor.wasm (or new folder actor/<id>) is
booted as at start once quiet: new = spawn (tested first), changed =
upgrade. The broker `scan` request is gone; the boot pass stays, plus a
full pass when the OS watcher overflows.
New broker requests watch/unwatch {path} (saved in actor.json `watches`)
deliver fs_changed {path, op: write|create|remove|rename, to?} from `fs`,
not routed, dropped when the mailbox is full; overflow sends every watcher
one fs_changed {path:"/actor", op:"rescan"}.
Card #91 (multi-host step 1).
- cli: the subcommand is optional; without one, sausage runs ./world and
runs init first when ./world is not a world yet (test: cli/tests/cli.rs).
So `nix run git+https://gitea.surma.technology/surma/sausage` works.
- flake: .#loop without a folder runs bare sausage.
- README: mention the bare command and the bare loop.
{"type":"remove","actor"} shuts the actor down normally, drops its
mailbox, supervisor and actor.json entry (so also its subscriptions and
tap), and deletes /actor/<id>/. Native actors cannot be removed.
Re-applied from card-1-broker-remove (3c91747) onto card-2-broker-tap.
Tapped copies {type:tapped, message:{id,from,to,reply_to,body}, ts} are
delivered directly (never tapped themselves), dropped when the tapper's
mailbox is full, and the tap is saved in actor.json like subscriptions.
File operations under /actor/<id>/fs/ become messages from `fs` to <id>
(fs_read, fs_write, fs_mkdir, fs_remove, fs_rename) with a 10 s timeout.
vfs.rs wraps the wasi:filesystem implementation of wasmtime-wasi.
- flake: checks.tests = the sausage package with doCheck (tests of sausage,
sausage-actor, agent; fixtures build offline from vendored crates).
- .gitea/workflows/ci.yml: `nix flake check -L` on runner label `nixos`.
Not compiled: written by the Sausage agent.