A malformed .jp/.id silently reassigns the workspace ID
- Status: Todo
- Kind: Bug
- Authors: jp
- Date: 2026-08-19
- Label: package=jp_workspace
- Label: type=bug
Workspace::open_with_storage_dir reads the workspace ID with:
let id = Id::load(&storage)
.transpose()
.ok()
.flatten()
.unwrap_or_default();Id::load returns Option<Result<Id>> (id.rs:41): None when the file cannot be read, and Some(Err(_)) when its last line fails from_str, which rejects anything that is not five characters of [0-9a-z] (id.rs:96-106). .ok() discards that Err arm, and .unwrap_or_default() then mints a fresh random ID.
The consequence is not local. with_user_storage(&user_root, slug, id.to_string()) keys the user-local silo by that ID, so a fresh ID selects a different silo, and id().store(&storage) immediately afterwards overwrites .jp/.id with the replacement. Every --local conversation in the original silo disappears from listings, and the ID that would find them again is gone from disk.
Why it is reachable
.jp/.id is a tracked file in this repository, so a conflicted merge in it is ordinary. Conflict markers fail both the length and the alphabet check. A truncated write does the same.
Nothing is reported. The run succeeds and simply lists fewer conversations.
Fix
Treat only a genuinely missing ID as the new-workspace case, and propagate a malformed one before any user storage is touched:
let id = match Id::load(&storage) {
None => Id::new(),
Some(result) => result?,
};Error::Id already exists (id.rs:97), so open needs no new variant and jp_cli's From<jp_workspace::Error> already covers it.
Test
A workspace whose .jp/.id holds invalid content: open returns Error::Id, and the file is left exactly as it was.
Scope
Not introduced by the move into jp_workspace — the same chain lived in jp_cli::load_workspace beforehand and was relocated verbatim, which is why it was left alone in that PR. open's doc comment there was widened to say an ID file that is "missing, unreadable, or malformed" gets a fresh ID, so the behaviour is at least written down until this lands. That sentence should go back to describing only the missing case once it does.