Skip to content

host: a state file that links to a folder is described as read through the link, then reported unreadable (EISDIR) #2416

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: 86221fec935f4bc197bbcbe5f8614ebc
Branch: dev/1-state-link-target
Base: 67aead2
At: 2026-11-04T16:26:27Z

What happens

When <home>/state.json is a symbolic link whose target is a directory, the diagnostics report contradicts itself in two adjacent rows:

state file: not a plain file (described only; the store reads the team file through the link and the next save replaces the name)
Read-only:  The Bot team file … could not be read (EISDIR). …

Both halves of the state file sentence are false in this case. readFile(statePath) follows the link and throws EISDIR, so the store lands on stuck; and because writable() is false, the next save never happens — the name is never replaced. The second row is the truthful one, and it says the opposite of the first.

That is exactly the mutual contradiction #2404 set out to remove: LINKED_STATE ("read through the link, the next save replaces the name") is only true when the link resolves to a readable plain file.

Why

core/src/host/diagnostics.js:254-260 (stateNotPlain) answers LINKED_STATE for any symlink:

const info = await lstat(path)
return info.isSymbolicLink() || (info.isFile() && info.nlink > 1) ? LINKED_STATE : NOT_PLAIN

It lstats the entry itself and never resolves the target. core/src/host/real-dir.js:44-46 (fileKind) answers other for any symlink too, so nothing in this path learns what the link points at.

Reproduction

In a temporary home, make state.json a link to a folder, then run the state-file diagnostics for that home:

mkdir -p /tmp/t
ln -s /tmp/t "$HOME/state.json"      # the link target is a directory

Observed (three-row group, current main 67aead26239b59ca9197104b6341f74568e5a660): the state file row carries the LINKED_STATE sentence and the Read-only row in the same report carries could not be read (EISDIR).

Control that must keep working — the link points at a readable plain file:

printf '{"version":99}' > /tmp/t2.json
ln -sf /tmp/t2.json "$HOME/state.json"

That one is correct today: LINKED_STATE plus could not be read (saved by a newer FlyBot). #2404 added a case for the control only; the link-to-folder case has no test.

Expected

  • link → readable plain file: unchanged (LINKED_STATE + saved by a newer FlyBot).
  • link → directory, link → missing target, or stat failing: the leaf sentence not a plain file (nothing is read or written), i.e. the same answer a plain directory at that path gets.
  • the target is never opened: stat (metadata) is the only call, no content read of a file that may belong to someone else.
  • every other row and every other sentence in the report is unchanged byte for byte.

Scope

Dedupe

On live main 67aead26239b59ca9197104b6341f74568e5a660: open issues matching symlink → 0; "not a plain file" → 0; state file in:title → 0. Code side, isSymbolicLink() in this path appears only at diagnostics.js:263.

Ready

Failing test and fix are verified locally: on base 67aead26 the new case fails (1 fail, 56/57 pass, only the link-to-folder group red), after the fix 57/57; verify 4827/4827 with 0 fail / 0 cancelled, 10 patches apply cleanly, generate is a no-op on a second run. PR follows, linked to this issue.

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