Repro (deterministic, against production)
brintos mount <owner>/<repo> /mnt/x &
echo hello > /mnt/x/f.txt
ln /mnt/x/f.txt /mnt/x/f-hard # before the first flush lands
sleep 3
Log output:
brintos-fs: flush error (will retry): constraint failed: UNIQUE constraint failed: inodes.ino (1555)
brintos-fs: 2 change(s) still queued at unmount; locally durable, will resume flushing on next mount (constraint failed: UNIQUE constraint failed: inodes.ino (1555))
The batch retries forever — the create and the hard link never become server-durable, every subsequent fsync(2)/brintos sync barrier fails, and the journal cannot drain past the poisoned batch (it resumes wedged on the next mount too). Reproduced on Linux/WSL2 against https://brintos.io at brintos-fs da15870.
Cause
Cache.Link points the new entry at the source's mirror inode id. When the source is an un-flushed create, that id is the synthetic pending:<path> (internal/cache/write.go:190-220), so two entries rows share one pending inode row.
When the batch (file attach + link, or the file attach alone with the link still queued) commits, reconcileTx (internal/cache/write.go:856-908) handles the pending→real identity swap by:
DELETE FROM entries WHERE path=?(the reconciled path),DELETE FROM inodes WHERE inode_id=? AND inode_id NOT IN (SELECT inode_id FROM entries)— the pending inode is not deleted because the other hard-link entry still references it,INSERT INTO inodes(ino, inode_id, ...) VALUES(curIno, realId, ...)— UNIQUE violation:curInois still held by the surviving pending inode row.
commitFlushed's transaction rolls back, the batch stays claimed→unclaimed→retried, and the same violation recurs on every attempt. Note claimBatch does not order file-create and link-to-it into separate batches (window.blocks only consults in-flight batches, and even in separate batches the shared pending inode makes step 3 collide).
Suggested fix
In the identity-changed branch, when the real inode id does not exist yet, rewrite the identity in place instead of DELETE+INSERT — under PRAGMA defer_foreign_keys=ON:
UPDATE inodes SET inode_id=:real WHERE inode_id=:pending;
UPDATE entries SET inode_id=:real WHERE inode_id=:pending; -- rebinds ALL hard links
-- then the normal upsertEntry(e) refreshes metadata
This keeps st_ino stable for every entry referencing the pending inode, never deletes the entries row (so the dir_listed_at_ns save/restore becomes unnecessary), and the sibling link's own reconcile then takes the same-identity fast path. When the real inode id does already exist (link flushed first), a plain upsertEntry rebinding plus a conditional prune of the now-orphaned pending inode suffices.
The C reimplementation in misc/brintos-fs2 ships this fix (see bfs_mirror_reconcile_tx in src/bfs_mirror.c, marked DEVIATION); its E2E suite covers the sequence.