docir — design documents Graph

Documents / Issues / issue-1e310cf366b8

A daemon that dies at startup is reported as a timeout

The client spawns a daemon, waits, and reports the wait — so a store whose schema will not load reaches the caller as a timeout instead of the reason, which stays in daemon.log.

issue-1e310cf366b8issueresolved#cli#integrity
View as Markdown◉ View in graph

A store whose schema will not load takes the daemon down at startup. The client cannot see that: it spawns one, waits, and reports the wait. The sentence the error was written to carry — which key is wrong, which docir version the store needs — is in daemon.log and nowhere the caller looks.

Measured

With the daemon enabled, which is the default, a store whose schema will not load produced the wait rather than the reason:

text
docir_query -> ToolError: daemon failed to become ready in time

Only docir --no-daemon answered that store correctly, because that mode builds the container — and so resolves the schema — in process. In the default mode the CLI was a socket client that loaded no schema: it spawned the daemon and waited out the same ten-second deadline the MCP server did.

Not the same defect as An unloadable schema kills the MCP server with no message

That one killed the MCP server outright, in process, and is fixed: the surface comes up and every tool returns the reason. This one does not kill anything. The server is healthy, the daemon is the thing that died, and the failure it produces is a timeout with the wrong cause rather than silence.

They share a symptom — the reader never gets the sentence — and nothing else. The fix for the first was to keep a message that already existed; the fix for this one is to carry a message across a process boundary that currently has nowhere to put it.

What closing it needs

A daemon that can say why it died rather than by being absent.

The cheap version: when a spawn times out, the client quotes the tail of daemon.log. It is honest, it needs no protocol change, and it reports a wrong cause as one candidate rather than as the answer.

The honest version distinguishes "not ready yet" from "will never be ready" at the point the client is waiting — a change to the pair of timeouts The daemon keepalives; the reply timeout bounds silence, not work keeps deliberately separate, so it costs a decision rather than a patch.

What shipped

ensure_running records the log's size before it spawns, and on a timeout quotes what the daemon wrote after that point — six lines, pointer rows dropped, capped. A traceback's last line is the exception, so the sentence naming the cause is what survives the window.

Reading from the recorded offset is the whole of the care: the log is appended to across every spawn a store has ever done, so quoting its tail unconditionally would attribute last week's traceback to this morning's timeout — a wrong cause stated with confidence, which is worse than the bare wait it replaces.

Silence gets its own sentence. A daemon that wrote nothing did not get as far as failing out loud, which points at the spawn rather than at the store, and the message says so and names the log.

Quoted as what it said, never as the cause: a healthy daemon can miss this deadline too, on a cold model load, and then those lines are progress. The reader is the one who can tell, and now has something to tell it from.

Then closed a second time, and better. A daemon that cannot be started fails the command instead of running in process made a daemon that will not start a reason to run in process rather than a reason to fail, so this store no longer reaches the timeout at all: the client falls back, the container resolves the schema, and the caller gets the error itself instead of a quoted log tail. Measured on a store whose default_status names no declared status — the default transport and --no-daemon now print the same sentence. The log quoting still matters for a daemon that fails after starting to come up, where the fallback's own warning names the wait and the log names the cause.

The other half of The daemon keepalives; the reply timeout bounds silence, not work is untouched. This is the spawn-and-wait path only — the reply timeout still bounds silence rather than work, and the two still raise different exceptions.

To amend: Re-verify: