Skip to content

A rotation over a linked journal path reports a revision error instead of refusing the link #2146

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: a515913fcd2545b89cb983ac414a20a5
Branch: dev/4-397-journal-rotation-leaf
Base: 35f5f8e
At: 2026-11-02T10:00:41Z

What the user sees

A host that keeps stateJournal on runs out of room and rotates the journal. If something has replaced state.jsonl with a link by then — the same swap #2133 refuses at start-up and on every append — the rotation branch does not look, reads through the link, and stops the writer with a reason that points at the wrong thing:

<path> is not recording: JournalError: path revision does not hold the recorded earlier value

The honest answer is the journal path is not a plain file, the sentence the other two branches give (core/src/host/journal.js:263, :317). As it stands the reason reaches the user: #2135 (merged as 35f5f8e2) puts status().because into the diagnostics report, so the report line now tells them to go looking for a revision mismatch that does not exist.

No bytes escape through the link: rename replaces the name, not the inode, so the file the link names keeps its bytes (verified on the current main). This is a wrong-reason defect, not a data leak — it should be fixed as such, and not reported as a security hole.

Where it is

core/src/host/journal.js:293-306, the if (lastText !== undefined && (bytes + added > maxBytes || …)) branch: pruneEntries([...parseJournal(await readFile(path, 'utf8')), ...fresh]), then writeFile(temp, { flag: 'wx' }) + rename(temp, path). Neither lstat(path) nor the three predicates used at :263 and :317 (isSymbolicLink(), isFile(), nlink !== 1) is consulted here.

Reproduction

On the current main (35f5f8e2), with a real openJournal:

  1. open with maxBytes: 350 and record one entry (~326 B);
  2. rmSync(path) then linkSync(someOtherFile, path) — the journal path is now a hard link, nlink === 2;
  3. record again, so the cap is crossed and rotation runs.

Current result: one warning, … is not recording: … JournalError: path revision does not hold the recorded earlier value; the other file's bytes are unchanged and its nlink is still 2. With the same setup and a symlink instead of a hard link the same wrong reason comes back.

Expected behaviour

The rotation branch asks the leaf question first and refuses with the same sentence the other branches use:

  • before reading the file it is about to replace, lstat the path; if it is a symlink, not a file, or nlink !== 1, stop with the journal path is not a plain file, as at :263/:317;
  • a missing path stays a normal case (rotation writes the temp and renames it into place), exactly as today;
  • no change to open(), the append branch, status() or the warning prefix — only which reason the rotation branch reports.

Acceptance

Red first on the current main with the source unmodified and only the new cases added:

  1. hard link standing in for the journal path plus a write over the cap → warnings has one entry matching /not a plain file/ (today it matches /revision does not hold/), the other file's bytes are unchanged and its nlink is still 2;
  2. symlink standing in for the path plus a write over the cap → same sentence, and the target file is untouched;
  3. control case, green before and after: a plain journal file over the cap still rotates — content is the pruned set, nlink === 1, no warning.

Then the same cases green, TZ=UTC ARCH_DOCS_BASE=<new base> npm run verify, and node scripts/ds-bot-patches.mjs check/canonical (this file is mirrored under patches/ds-bot/files/src/host/journal.js, so regenerate).

Scope

Only the rotation branch in core/src/host/journal.js and its test case in core/test/state-journal.test.js (plus the mirror/patches that regenerate from it). Not in this issue: the leaf predicates themselves, #2074/#2133 behaviour, or the warning text format.

Duplicate check

On live main 35f5f8e2: the rotation branch has no leaf check, and issue search for journal rotation leaf, not a plain file journal, rotation nlink returns only closed leaf-surface issues — #2133 (start-up + append hard link), #2094/#2083/#2079 (memory/work journal linked leaf), #2065/#2056 (linked journal folder), #2100/#2114 (temp name) — none of them the rotation branch. No duplicate.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions