Unparseable conversation metadata reports as Not found
- Status: Todo
- Kind: Bug
- Authors: jp
- Date: 2026-08-24
A conversation whose metadata.json exists but does not deserialize is reported as missing:
$ jp q
Not found
target Conversation metadata
id jp-c17874925101The file is right there. Nothing in the output says it could not be read, what about it could not be read, or which file to look at.
Where it happens
maybe_init_conversation (crates/jp_workspace/src/lib.rs:896) discards the load error:
let Ok(meta) = loader.load_conversation_metadata(id) else {
warn!(%id, "Failed to load conversation metadata. Skipping.");
return;
};The OnceLock stays empty, so Workspace::metadata falls through to Error::not_found("Conversation metadata", id). The reason reaches the trace log and nowhere else. maybe_init_events has the same shape.
Sanitization does not catch it first: validate.rs only confirms metadata.json is a JSON object, using IgnoredAny to skip the field values (RFD 052). A file whose fields are wrong passes validation and then fails at load, which is exactly the gap this falls into.
Why it matters
Every unreadable-metadata case is indistinguishable from a deleted conversation, so the reported cause is wrong rather than merely thin:
- A hand-edited
metadata.jsonwith a typo — RFD 031 supports editing these files directly, so this is the case a user reaches on their own. - A partially written file from an interrupted persist.
- A field whose type changed between two builds. Found this way, running two development builds against one workspace: a build predating RFD 103 read a
labelsarray as a string and reported the conversation missing.
The type-change case does not reach released binaries — a build that predates a field skips it as unknown — so it is a hazard for people running several builds rather than a shipping bug. The other two are not.
All three are recoverable in seconds if the error names the file and the parse failure. As "Not found", the natural next step is to go looking for a conversation the user believes they lost.
Shape of the fix
Thread the load error through the lazy-init path rather than collapsing it: maybe_init_conversation and maybe_init_events return the failure, and Workspace::metadata / events distinguish "no such conversation" from "the conversation is there and unreadable", carrying the path and the underlying serde message.
LoadError already holds both (jp_storage::load), so the information exists and is being dropped at the OnceLock boundary. The work is in the callers: the init helpers are used from several read paths that currently cannot fail.
Not in scope
Making metadata tolerant of unknown fields. It already is — Conversation carries no deny_unknown_fields, so a field added by a newer JP is skipped, and an_unknown_field_is_ignored in conversation_tests.rs pins that. It is what keeps a released binary reading conversations written by a development build.
That tolerance does not extend to a known field whose type changed, which is the case above: no amount of unknown-field skipping helps a reader that knows labels and expects a string. Widening the reader before narrowing the writer is what covers that, and it is a release-sequencing question rather than something this ticket can fix.