# Erratum — draft-fossil-sync-protocol-00 §8.6 / §3.7(c)

| | |
|---|---|
| **Document** | `draft-fossil-sync-protocol-00` |
| **Section** | 8.6 — Announcement and Request Cards; 3.7 — Loop Termination, limb (c) |
| **Type** | Technical (correction — the draft attaches the peer-date gate and the extra round to the wrong private igot) |
| **Status** | Applied in `draft-fossil-sync-protocol-01` (2026-10-12) |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | go-libfossil's conformance suite against Fossil 2.28 (GAP rows SP-3.7-3 and SP-8.6-1); confirmed by reading Fossil's source and by running a Fossil client against a scripted peer |

## 1. Summary

Fossil sends `igot HASH 1` from two different places, and the draft merges
them:

1. **Private-sync inventory.** With private sync on, the client lists every
   private artifact as `igot HASH 1` in every push-carrying request, starting
   with the first. There is no peer-date check, nothing is held until
   `server-version` arrives, and these cards never cause another round.
2. **Private answer to a gimme.** With private sync off, a side asked for an
   artifact it holds as private answers with `igot HASH 1` instead of the
   content, but only when the peer's date is 2020-04-13 or later. On the
   client, an answer appended while processing the first reply is what buys
   the one extra round.

§8.6 applies the date gate and the hold-until-server-version rule to (1).
§3.7(c) ties the extra round to (1) as well. Fossil does neither.

## 2. Location

§8.6 (~lines 1751-1760):

> For peer dates before 2020-04-13, omit private igot entirely rather than
> sending flag 1.  Until server-version establishes the peer date, hold local
> private inventory; if the reply establishes a date on or after 2020-04-13,
> append the eligible private igot cards to that reply's next-request buffer,
> and let Section 3.7(c) dispatch that buffer.

§3.7(c) (~lines 588-594):

> The current request emitted at least one private igot HASH 1 and the
> post-increment round number is exactly 1, *or* processing server-version in
> this reply made private inventory eligible and the post-reply
> remaining-inventory step appended at least one such igot. …

## 3. The divergence

Source, Fossil 2.28 (`src/xfer.c`):

- `send_private()` (lines 978-991) emits `igot %s 1` for every row of
  `private` and has no `remoteDate` test. The client calls it in every push
  request when private sync is on (line 2295), including the first, before
  any `server-version` has been read (`remoteDate` is set only at line 2811).
  Its return value is discarded, so it never touches `nPrivIGot`. The server
  calls it the same way in every pull reply (line 1924); line 1920 is the
  legacy clone path, which clone version 3 does not take.
- `send_file()` (lines 543-560) is the only place that increments `nPrivIGot`
  (line 557). For a private artifact while private sync is off, it emits
  `igot HASH 1` only if `pXfer->remoteDate>=20200413` (line 549); the comment
  there gives the reason: older peers raise an error on a private igot when
  private syncing is disallowed. On the client this runs while answering a
  reply's `gimme` (lines 2564-2569).
- `nPrivIGot` is zeroed right after each exchange (line 2456), so it counts
  only what reply processing appended to the next request. The continuation
  test is `xfer.nPrivIGot>0 && nCycle==1` (line 2989, after `nCycle++`).

Runs, Fossil 2.28 client with one private check-in, against a scripted peer:

| Command | Peer's first reply | Observed |
|---|---|---|
| `fossil sync --private` | `server-version … 20260311 …`, nothing else | request 1 carries both `igot HASH 1` cards; 1 round |
| `fossil sync --private` | `server-version 21000 20190101 000000`, `igot NEW` | request 1 and request 2 both carry the private igots (peer dated 2019) |
| `fossil sync` | `server-version … 20260311 …`, `gimme PRIVHASH` | request 2 carries `igot PRIVHASH 1`; 2 rounds |
| `fossil sync` | `server-version 21000 20190101 000000`, `gimme PRIVHASH` | no private igot; 1 round |

## 4. Impact

- **Wire.** A client following §8.6 sends its private inventory one round
  later than Fossil and withholds it from pre-2020 peers that Fossil sends it
  to. A client following §3.7(c) goes one extra round after a first
  private-sync request where Fossil stops, and does not go one where Fossil
  does.
- **Interop risk of Fossil's way.** Low: a peer accepting `pragma
  send-private` understands `igot HASH 1`. The date gate guards the other
  case, where private sync is off.
- **Conformance.** Neither rule as drafted can be observed in Fossil, so
  go-libfossil has no case for either.

## 5. Correction

```diff
-   igot HASH announces public possession; igot HASH 1 private
-   possession; gimme HASH requests content.  Section 3.3 governs.  No
-   ordering constraint exists between igot and gimme.  For peer dates
-   before 2020-04-13, omit private igot entirely rather than sending
-   flag 1.  Until server-version establishes the peer date, hold local
-   private inventory; if the reply establishes a date on or after
-   2020-04-13, append the eligible private igot cards to that reply's
-   next-request buffer, and let Section 3.7(c) dispatch that buffer.
+   igot HASH announces public possession; igot HASH 1 private
+   possession; gimme HASH requests content.  Section 3.3 governs.  No
+   ordering constraint exists between igot and gimme.  When private
+   sync is in effect, a client lists its private inventory as igot
+   HASH 1 in every request that carries push, starting with the first,
+   and a server lists its own in every pull reply; no peer date is
+   consulted.  When private sync is not in effect, a
+   gimme for content held as private is answered with igot HASH 1 only
+   if the peer's date is 2020-04-13 or later; for an earlier or unknown
+   date, send nothing for it.
```

```diff
-   *(c)* The current request emitted at least one private igot HASH 1
-   and the post-increment round number is exactly 1, *or* processing
-   server-version in this reply made private inventory eligible and the
-   post-reply remaining-inventory step appended at least one such igot.
-   The latter continuation dispatches the newly appended first private
-   inventory exactly once; the former permits only one first-cycle
-   discovery continuation.
+   *(c)* The post-increment round number is exactly 1 and processing
+   this reply appended at least one igot HASH 1 answering a gimme
+   (Section 8.6, private sync not in effect).  Private-inventory igot
+   cards sent under private sync never satisfy this condition.
```

## 6. Rationale

- **The date gate belongs where Fossil put it.** It protects old peers when
  private sync is off. A peer in a private sync has accepted `send-private`
  and already knows the flagged igot.
- **No hold, no extra round.** Fossil has nothing that holds inventory until
  `server-version`, and the second limb of (c) describes that missing
  mechanism. Removing it makes the loop rule one condition Fossil actually
  evaluates.
- **Both limbs become testable** against Fossil, which the current text is
  not.

## 7. Verification

- Source: Fossil 2.28 `src/xfer.c:543-560` (`send_file`, gate at `:549`,
  count at `:557`), `:978-991` (`send_private`), `:2295`, `:2456`,
  `:2564-2569`, `:2807-2813`, `:2989`, `:1920`, `:1924`.
- Not run: a pre-2020 Fossil peer. Whether such a peer accepts the private
  igots of a private sync was not tested; the low-risk claim in §4 rests on
  its having accepted `send-private`.
- Run: the four client runs in §3, each logging decoded request bodies at a
  scripted HTTP peer.

---

*Prepared against `draft-fossil-sync-protocol-00`. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.*
