0.9.1 — 2026-05-11
Release Notes
Patch release rolling up the post-0.9.0 daemon-stall fix (#55) and the
daemon-health deep-probe + SDK admin wrappers follow-up (#56), plus the
docs/CI groundwork from #51, #52, #54.
Added
daemon.health({deep: true})shim probe. AddsShimRequest::Health
CBOR variant + shim-side handler returning
ShimHealthInfo { uptime_ms, requests_served, last_request_at_ms }. The
daemon fans out probes concurrently with per-shim and overall budgets
(defaults 1 s / 3 s; env-overridable viaLOOM_PROBE_TIMEOUT_MSand
LOOM_DEEP_HEALTH_BUDGET_MS). Payload is now typed
Vec<ShimDeepHealth>(replacing the prior placeholder
Vec<serde_json::Value>) with a typed
ProbeStatus { Ok, Timeout, Error }enum. The new shim variant
requires same-tree daemon+shim shipping; version-mismatched shims
surface asprobe_status: "timeout".daemon.healthcontinues to
require the existing socket-auth token handshake — same auth surface
as before; no anonymous reachability. Known gap: no per-connection
rate limit ondaemon.healthitself (tracked as follow-up).ShimState.restart_count/last_restart_at_ms. Daemon-side
bookkeeping for shim respawn events, bumped inget_or_spawnonly
when a prior state entry exists (avoids overcounting under
open-breaker rejection). Exposed via the deep-health payload.- SDK wrappers (TS + Python) for the admin RPCs. Hand-written
Session.kill()(sync + async parity in Python),killSession()
(TypeScript) /kill_session()(Python sync) admin free functions
with documented "ADMIN ESCAPE HATCH — 5 s ceiling then SIGKILL"
warnings, anddaemonHealth()(TS) /daemon_health()(Python sync)
free functions. JSON-RPC requestidallocator switched from
hardcoded1to monotonic per-connection; transports moved to
id-keyed response demultiplexing (persistent reader task in async
Python; persistentdatalistener in TS) to supportrequest.cancel
correlation while another call is in flight. Sync Python keeps
single-in-flight and gains anAsyncSession-redirecting doc note for
cancellation. TS addsAbortSignalsupport oncall()via
LoomAbortError(name === "AbortError"); Python async transport
transparently emitsrequest.cancelonasyncio.CancelledErrorand
re-raises soasyncio.wait_for/TaskGroup/asyncio.timeout
compose cleanly. SessionScopeprimitive (loom-core/src/session_scope/).
Per-session structured-concurrency parent:tokio_util::sync::CancellationToken
paired with atokio::task::JoinSet. All session-lifetime spawns become children;
drain(grace)cancels cooperatively then force-aborts survivors. Replaces five
fire-and-forgettokio::spawnsites that previously leakedJoinHandles and
saturated the daemon runtime after 4–6 sequential client sessions.- Per-request server-side deadline.
LOOM_REQUEST_TIMEOUT_MS(default
30000) wrapsrouter.dispatchso a hung shim or stuck dispatcher can't hold
a connection task. Returns the typedrequest-timeoutenvelope on expiry. request.cancelRPC. Connection-scoped: cancels an in-flight request on
the same connection by JSON-RPCid. Returns{cancelled: bool}. The
cancelled request returns the typedrequest-cancelledenvelope.session.killRPC. Admin escape hatch for stuck sessions. Performs the
abort flow plus blocks on shim teardown with a 5 s ceiling, then SIGKILL
(shutdown_processalready escalates SIGTERM(2s) → SIGKILL(1s) inside the
ceiling).daemon.healthRPC. Operational snapshot:active_sessions,
shim_breaker_states,otel_exporterstatus. Shallow path is
non-blocking;{deep: true}slot reserved for a follow-up shim probe.LoomErrorCode::RequestTimeout(wire stringrequest-timeout) and
LoomErrorCode::RequestCancelled(wire stringrequest-cancelled).
Both SemVer-minor additions per the existing wire-stability commitment.- Multi-session stress test at
loom-core/tests/multi_session_stall_repro.rs
— 100 sequential session create+close cycles complete in under 1 s; the
user-facing success criterion from the original investigation prompt.
Changed
ShimManager::record_failurecircuit-breaker eviction no longer fires
tokio::spawn(shutdown_process(p))fire-and-forget. Spawns into a
cleanup_tasks: JoinSet<()>field with opportunistictry_join_next
reaping on everyrecord_failureandshutdown_session.CoreBridge::close_session_raw(daemon-side) similarly tracks
host.shutdown_sessionbackground tasks in acleanup_tasksJoinSet
instead of leaking them via baretokio::spawn.Session.budget_timeris now a SessionScope child with
tokio::select! { _ = cancel.cancelled() => {} _ = sleep(budget_ms) => ... }
so a closed session's timer exits cooperatively before its sleep
completes, never tripping the kill callback after lifecycle exit.
Removed
Session.task_handlefield — was dead code (declared but never
populated). Replaced by the always-populatedSession.scope: Arc<SessionScope>.Session.budget_timerfield — superseded by the SessionScope-owned
cancellation-aware timer.ReceiptMarshaller.queue_depthfield + the docstring claiming
"Background queue depth 256; full → synchronous append on the calling task".
The field was dead and the docstring lied — there was no bounded channel,
notry_send, no backpressure. Removed both.
Install loom-cli 0.9.1
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mentiora-ai/loom/releases/download/v0.9.1/loom-cli-installer.sh | shInstall prebuilt binaries via Homebrew
brew install mentiora-ai/loom/loomDownload loom-cli 0.9.1
| File | Platform | Checksum |
|---|---|---|
| loom-cli-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| loom-cli-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| loom-cli-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| loom-cli-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |