Repository navigation
Description
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_PLAINIt 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 directoryObserved (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
statfailing: the leaf sentencenot 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
core/src/host/diagnostics.js—stateNotPlainonly: resolve the link withstat(path), keepLINKED_STATEwhen the targetisFile(), answerNOT_PLAINfor a directory / missing target / failedstat. Both sentences stay verbatim.core/test/diagnostics.test.js— one new case (link → folder, red first); the existing link-to-file, hard-link, directory and host: the diagnostics report names whether the journal is recording but never the file it records into #2388/host: the diagnostics report says another dsh holds the team even when the team file needs updating or cannot be read #2167 cases stay green.- regenerate
patches/ds-bot/0001-core-fixes.patchandpatches/ds-bot/0007-version-and-tooling.patch(both carry this file). - one
changelog.d/fragment; one line indocs/subsystems/core-host.mdfor the arch-docs check. - not touched:
store.js,core/client.js,patches/ds-bot/0008, any i18n table. - Tier: T2 (
core/src/host/**), so this needs the formalVerdict: PASSat the PR tip.
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