fix(sync): a refused push stays refused, and says why (BEA-403) (#146)

A device whose journal push the hub 403s reported healthy sync between
every pair of remote passes, and never showed the hub's reason for the
refusal — so a user whose device was not registered to their account
re-ran `bdrive login` (which the message told them to), re-checked their
project permissions (write), and had nowhere left to look.

Two causes, both local to the client:

- Cycle recomputed st.Access from scratch at the end of every pass,
  including the daemon's cheap local-only ticks that never reach the hub.
  Three of those run between remote passes, so the daemon alternated
  "read-only on this project" / "access restored; syncing normally" every
  few seconds and `bdrive status` reported OK moments after a refused
  push. Now each leg records its own verdict — pull clears no-access, push
  records read-only or clears it — and a cycle that asked nothing leaves
  the last answer standing.
- The hub's own sentence was summarized into "read-only (pull only)",
  which describes the STATUS CODE. It is the only thing that tells a
  device-registration refusal from a project the user really is a reader
  on. It now rides in SyncState.AccessReason and Result.Reason(), printed
  by `bdrive sync`, `bdrive status` and the daemon log, and dropped unless
  it passes journal.SafeText — hub text reaching a terminal.

The hub's refusal also now names the upgrade: the binding is made by the
login request naming its device, which a CLI older than the gate does not
do, so "run `bdrive login`" alone sent that user in a circle. Hub and CLI
deploy separately, so the skew is the expected state right after the gate
ships.


Claude-Session: https://claude.ai/code/session_01GSHsQU4pBCzKkPyPeXSwTm

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow Lee (Sungwon)
2026-08-10 20:33:26 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 709c40e76c
commit 922886b949
10 changed files with 214 additions and 15 deletions
+6
View File
@@ -246,6 +246,12 @@ type SyncState struct {
Lamport int64 `json:"lamport"`
PushedOps int64 `json:"pushed_ops"` // how many of our own ops the remote has
Access string `json:"access,omitempty"` // "", "read-only", or "no-access"
// AccessReason is the hub's own words for the last refusal. Without it every
// 403 renders as the same "read-only (pull only)" line, and the hub's most
// actionable answer — "this device is not registered to your account on this
// hub; run `bdrive login`" — reached nobody: the one state a user cannot
// diagnose from the outside was the one the CLI summarized away.
AccessReason string `json:"access_reason,omitempty"`
// IgnoreAccepted is the .bdriveignore text whose scan scope THIS device has
// accepted, and IgnorePulled is the text a peer's version last wrote here.