Summary
When the daemon fails to start due to a mount error (e.g. VaultError: refusing to mount 'X' also as primary), the readiness pipe receives empty bytes instead of the documented error: <reason>\n payload. The MCP client (or any other spawn caller) then raises:
DaemonSpawnError: unexpected readiness payload from spawn pipe: ''
masking the real cause (the underlying VaultError) and forcing the operator to read the daemon log to figure out what actually went wrong.
Protocol
Per src/engram/daemon/spawn.py:140-152, the spawn-pipe protocol is:
text = line.decode("utf-8", errors="replace").rstrip("\n")
if text == "ready":
return SpawnReadiness.ready()
if text.startswith("error:"):
return SpawnReadiness.error(text[len("error:"):].strip())
msg = f"unexpected readiness payload from spawn pipe: {text!r}"
raise DaemonSpawnError(msg)
So error: <reason>\n is a first-class outcome. The daemon writes nothing (empty bytes) on certain mount failure paths, which falls into the "unexpected payload" bucket and loses the real error context.
Reproduction
- Configure a per-user vault with
name: X differing from per-vault vault_name: Y (same path).
- Run
engram serve (which spawns a daemon).
- The daemon fails to mount due to
VaultError: refusing to mount 'X' also as primary.
- The proxy receives
'' on the readiness pipe.
DaemonSpawnError: unexpected readiness payload from spawn pipe: '' propagates to the MCP client.
- The real cause (
VaultError) is only visible in the daemon log, not surfaced to the caller.
Expected
On any startup-time failure before the daemon would have written ready\n, the daemon should:
write_to_readiness_pipe(f"error: {exception_summary}\n")
so the proxy gets a clean SpawnReadiness.error(...) value with the real reason instead of DaemonSpawnError.
Likely fix locations: the startup paths in src/engram/cli/serve.py and any other place that holds the readiness pipe fd before mount completes. Wrap the mount + ready signal in a try/except that emits error: <msg> on failure.
Context
Surfaced during the 2026-05-16 daemon-fix investigation. The misleading DaemonSpawnError: ... '' was the visible failure; the underlying VaultError (filed as the multi-vault alias issue) was the actual root cause. Surfacing the latter through the readiness pipe would have shortened debugging by ~15 minutes.
Summary
When the daemon fails to start due to a mount error (e.g.
VaultError: refusing to mount 'X' also as primary), the readiness pipe receives empty bytes instead of the documentederror: <reason>\npayload. The MCP client (or any other spawn caller) then raises:masking the real cause (the underlying
VaultError) and forcing the operator to read the daemon log to figure out what actually went wrong.Protocol
Per
src/engram/daemon/spawn.py:140-152, the spawn-pipe protocol is:So
error: <reason>\nis a first-class outcome. The daemon writes nothing (empty bytes) on certain mount failure paths, which falls into the "unexpected payload" bucket and loses the real error context.Reproduction
name: Xdiffering from per-vaultvault_name: Y(same path).engram serve(which spawns a daemon).VaultError: refusing to mount 'X' also as primary.''on the readiness pipe.DaemonSpawnError: unexpected readiness payload from spawn pipe: ''propagates to the MCP client.VaultError) is only visible in the daemon log, not surfaced to the caller.Expected
On any startup-time failure before the daemon would have written
ready\n, the daemon should:so the proxy gets a clean
SpawnReadiness.error(...)value with the real reason instead ofDaemonSpawnError.Likely fix locations: the startup paths in
src/engram/cli/serve.pyand any other place that holds the readiness pipe fd before mount completes. Wrap the mount + ready signal in a try/except that emitserror: <msg>on failure.Context
Surfaced during the 2026-05-16 daemon-fix investigation. The misleading
DaemonSpawnError: ... ''was the visible failure; the underlyingVaultError(filed as the multi-vault alias issue) was the actual root cause. Surfacing the latter through the readiness pipe would have shortened debugging by ~15 minutes.