Repository navigation
Description
Summary
id(petname): Petnames::open (pkgs/id/src/petname.rs) accepts a parseable torn final line — a complete petname record JSON whose trailing newline was lost to a crash mid-append — and the next commit() append glues the new record onto that same line. The resulting file can never be opened again: the glued line is unparseable, so Petnames::open fails fatally with "petname journal line N is malformed", and because open_world propagates that error (world_store.rs:389), the whole world bricks until someone hand-edits petnames.jsonl.
This is exactly the failure #1620 fixed for the envelope outbox ("a parseable torn tail is accepted, then merged into the next record") and #2947 filed for the group log. The sibling rule those fixes established — an unterminated final line is torn, parseable or not, and is dropped and truncated at open — was never applied to Petnames. #1499 (still open) covers Petnames only for the unparseable fragment case; its acceptance criteria drop only a final line that is "unterminated and unparseable", so a parseable torn tail survives that fix and still glues.
Evidence (main 106b0571)
Petnames::commit(petname.rs:184-193) writes one JSON line +\nin a singlewrite_allfollowed bysync_data, so a crash/power loss during that write can leave the full JSON with only its\nmissing.Petnames::open(petname.rs:53-79) replays withtext.lines()(line 63), which yields the final unterminated line like any other; a parseable record is applied viastore.apply(record)with no completeness check and no truncation/repair — there is no torn-tail handling in the function at all.- The next
set/remove→commit()then appends its line directly after the unterminated one, merging two JSON records onto one line, and reports success. - On the next open, that merged line fails
serde_json::from_strand the open returns "petname journal line {N} is malformed".open_world(world_store.rs:389) propagates the failure, so every world open fails the same way. - Contrast the established sibling rule:
Outbox::open(envelope_outbox.rs:141-161) drops an unterminated final line "Parseable or not … Keeping a parseable torn line would let the next append merge two records onto one line and brick the queue (id(outbox): a parseable torn tail is accepted, then merged into the next record — Outbox::open only drops unterminated final lines that fail to parse #1620)";Directory::replay(directory.rs, id(directory): a torn final line in directory.jsonl bricks the directory — Directory::replay refuses the crash-torn tail that ArtifactLog (loaded in the same open) drops and truncates #1521) andArtifactLog::open(artifact.rs) drop and truncate the same way.
Verified (laptop probe, real petname.rs)
Probe: ~/git/fleet/probe-2026-10-30-laneA-petparse/ on the laptop, compiling the real petname.rs (sha256 ff6cba651a19ad5814a2540df12433d1444b44ff5fb258a8478cb891be88723e, identical to the sandbox checkout at main 106b0571) against a stub directory::Refusal; cargo run exit 0:
control journal bytes: "{\"op\":\"set\",\"owner\":\"00ff\",\"account\":\"aa11aa11\",\"name\":\"Ali\"}\n"
CONTROL clean journal: open OK, Ali=Some("Ali")
PARSEABLE TORN TAIL: open OK, Carol=Some("Carol") (accepted despite no newline)
AFTER NEXT APPEND bytes: "{\"op\":\"set\",\"owner\":\"00ff\",\"account\":\"aa11aa11\",\"name\":\"Ali\"}\n{\"op\":\"set\",\"owner\":\"00ff\",\"account\":\"cc33cc33\",\"name\":\"Carol\"}{\"op\":\"set\",\"owner\":\"00ff\",\"account\":\"dd44dd44\",\"name\":\"Dave\"}\n"
REOPEN AFTER APPEND: open FAILED: petname journal line 2 is malformed: trailing characters at line 1 column 64
- Control: a healthy newline-terminated journal opens and replays.
- Case: after appending a complete record with only its trailing
\nstripped (the crash cut), reopen accepts Carol; the nextset(Dave) succeeds but glues onto Carol's line; the final reopen is bricked.
Why it matters
petnames.jsonl is a small private store, but open_world loads it on the world-open path, so one unlucky crash during a petname rename converts a recoverable one-byte tear into a permanently unopenable world on the next ordinary write — silently (the append reports success). Three sibling stores in the same crate already encode the correct rule with regression tests; Petnames is the remaining store that doesn't, and the open #1499 fix as scoped does not close this parseable half.
Acceptance criteria
-
Petnames::opentreats an unterminated final line as torn whether or not it parses: the line is dropped and the file is truncated to the last complete line — mirroringOutbox::open(id(outbox): a parseable torn tail is accepted, then merged into the next record — Outbox::open only drops unterminated final lines that fail to parse #1620) — ideally coordinated with the id: a torn final line in petnames.jsonl bricks the whole world — Petnames::open refuses the crash-torn tail that FileJournal and Outbox drop and truncate #1499 fix so both torn cases land in one replay rewrite (split_inclusive('\n')). - Regression test in
petname.rsmirroring the outbox'sa_parseable_torn_last_line_is_also_dropped: write a valid record, strip only its trailing newline, reopen (record dropped, file truncated), set a new petname, reopen again and replay successfully. - Existing behavior unchanged: a newline-terminated line that does not parse stays fatal with the line-numbered error; blank lines still skipped; healthy journals set/remove/reopen exactly as before.
- Related, do not duplicate: id: a torn final line in petnames.jsonl bricks the whole world — Petnames::open refuses the crash-torn tail that FileJournal and Outbox drop and truncate #1499 (Petnames unparseable torn tail — open; this is its parseable sibling case), id(groups): a parseable torn final line in groups.jsonl is accepted, glued onto by the next append, and then bricks every group command #2947 (group log parseable torn tail — open, same pattern), id(outbox): a parseable torn tail is accepted, then merged into the next record — Outbox::open only drops unterminated final lines that fail to parse #1620 (outbox parseable torn tail — closed), id(directory): a torn final line in directory.jsonl bricks the directory — Directory::replay refuses the crash-torn tail that ArtifactLog (loaded in the same open) drops and truncates #1521 (directory torn tail — closed).
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · state: filed
7 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · parked
queue/5482-2948-petname-torn-tail· worktree/home/user/git/fleet/5482· test shellsh_wxe3l2cqdezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
fix(id/petname): drop a parseable torn final line in Petnames::open (#…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done