Survive the host dying under you
Hosts die. A crash, an out-of-memory kill, a stray signal from an operator — none of them ask your app first. This page shows you how to build a client that survives it: what to read at the handshake, what the SDK tells you when the process is gone, and how the answer forks on one word — durable or ephemeral.
The one idea to take away: read the session durability profile at the handshake, and branch on the discharge — never on your own guess. A durable session’s state outlives its host and reconciles when you resume. An ephemeral session’s state dies with the host, and the SDK hands you back exactly what was in flight so you can tell your user the truth.
Read the profile before you need it
Section titled “Read the profile before you need it”The host declares its durability in the initialize result. Read it through
readSessionDurability the moment you connect — after the host dies is too
late to ask it:
import { MuseClient, readSessionDurability } from "@muse-code/sdk";
const client = await MuseClient.spawn({ museBin: "/usr/local/bin/muse", args: ["serve"], clientInfo: { name: "my-app", version: "1.0.0" },});
const profile = readSessionDurability(client.initializeResult);The profile has three readings, and the third is the one to get right:
- Absent means durable. The member is optional only so adding it was backward compatible; a host that omits it is not ephemeral.
"ephemeral"means nothing survives the host. Plan for the discharge below.- A value you do not recognize guarantees nothing. Do not write
value === "ephemeral" ? ephemeral : durable— that silently grants a durability promise to a value your SDK has never heard of. The conservative read is “assume nothing survives this host”, andreadSessionDurabilityencodes exactly that: it returns{ kind: "unrecognized" }, which the discharge treats like ephemeral.
When the host dies
Section titled “When the host dies”You learn about a death two ways: the process exit (client.exit resolves
with the SDK’s classification of how the host ended) and the transport going
quiet. Report each one to your sessions with Session.hostExited — reporting
the same death twice is not just safe, it is required, because the second
report settles anything the first one’s drain left open.
hostExited returns a discharge, and its kind is your whole branch:
const exit = await client.exit;const discharge = session.hostExited(exit);
switch (discharge.kind) { case "notADeath": // An orderly shutdown. Nothing was lost and the session is still usable. break; case "durableDeath": // The host died, the state survived. Live turn waits have rejected with // MuseHostDiedError; spawn a fresh host and resume the session. break; case "discharged": // An ephemeral host died and took the session with it. What was in // flight is in discharge.terminalUnknownItems and // discharge.retiredCommands — show it, and do not reattach. break;}A clean exit is notADeath on purpose: closing the host yourself is not a
death, and neither is the host draining cleanly on end-of-input. Only an
abnormal end — a crash, a kill signal, an error exit — discharges anything.
The durable branch: waiters reject, then you resume
Section titled “The durable branch: waiters reject, then you resume”On a durable host’s abnormal death, the SDK touches nothing it observed: the
items and pending commands are left exactly as the wire last reported them,
because their real terminals arrive when the session resumes. What it does do
is reject every live turn wait with MuseHostDiedError — a wait on a dead
host would otherwise hang forever:
try { const outcome = await session.turn(turnId).completed;} catch (error) { if (error instanceof MuseHostDiedError) { // error.exit tells you how the host ended. Time to respawn and resume. }}Recovery is a fresh spawn plus resumeSession with the same session id:
const fresh = await MuseClient.spawn({ museBin: "/usr/local/bin/muse", args: ["serve"], clientInfo: { name: "my-app", version: "1.0.0" },});
const resumed = await fresh.resumeSession({ sessionId, excludeItems: false });The tested recipe behind this page proves that end to end: it kills a live
release-built host with SIGKILL mid-session, watches the classification come
back as a crash with the signal named, sees the registered turn wait reject
with MuseHostDiedError, and then resumes the same session on a second host
and reads the surviving state back.
The ephemeral branch: take the discharge and tell the truth
Section titled “The ephemeral branch: take the discharge and tell the truth”An ephemeral host’s death discharges the session in full, and every member of the discharge exists so your UI does not have to invent anything:
terminalUnknownItems— the items that were still in progress. They are annotated terminal-unknown, never marked failed or cancelled, and no completion is synthesized: the honest rendering is “we do not know how this ended”.retiredCommands— the commands you had submitted that the server never wove into the view. Each carries the input you submitted, so you can show the user what was in flight. Deliberately, none of them offers a composer restore: the client cannot know whether the work ran, and inviting a one-keystroke resubmit is inviting a double execution.
After the discharge the session refuses to lie on your behalf: further events
are not folded, new submissions throw MuseSessionDiscardedError, a
late-registered turn wait settles immediately with a terminalUnknown
outcome instead of hanging, and resuming the session is withheld — there is
nothing on the other end to reattach to.
What not to do
Section titled “What not to do”Do not dedupe death notifications. Report the process exit and the
transport EOF through hostExited; the SDK latches the first, replays it on
the second, and uses the repeat to settle waits the drain opened in between.
Do not treat an unrecognized durability value as durable. The enum is open so a third value can land one day; “assume nothing survives” is the read that stays correct when it does.
Do not resubmit a retired command because it looks unfinished. Terminal-unknown means unknown — the work may have run to completion on the host’s side of the crash.
Run it yourself
Section titled “Run it yourself”You need Node 20 or newer, an installed muse binary, and the SDK your own
application depends on — npm install @muse-code/sdk. The recipe programs live
in the SDK repository:
git clone https://github.com/meta-models/muse-code-sdkcd muse-code-sdknpm ci && npm run build --workspace @muse-code/sdkMUSE_BIN=$(command -v muse) \ npm run recipes --workspace @muse-code/sdk-cookbook -- --only survive-the-host-dyingWhere the tested code lives
Section titled “Where the tested code lives”The runnable source for this page is published in full, comments and all, as the survive-the-host-dying example. It is the source that runs, not a retelling of it: it is executed end to end on every change to this area, so this page cannot drift from what actually runs.
Two pieces of that source are harness plumbing, not client guidance, and the
source says so where they appear: the recipe launches its killable host
through a tiny shell wrapper that prints the host’s pid (an external death
does not come through the SDK, so the journey needs a pid to deliver one),
and it spawns hosts with MUSE_EXPERIMENTAL_SDK_ENABLED set explicitly.
muse serve now runs by default; the variable survives as an off-switch, and
the harness pins the run open so an operator’s off in the environment cannot
silently change what this page proves.
Neither is advice for your application — build against muse serve as a
supported command, and let your hosts die on their own.