- ADR-014: the multi-round ack loop, grounded in duplex git 2.43.0 captures cross-checked against fetch-pack.c: no-done rounds get acknowledgments (ACK <oid> per recognized have, NAK when none, flush; never ready so FLUSH is always the terminator), the done round generates closure(wants) - closure(haves) with no cross-round server state (clients re-send wants + commons every round), wait-for-done stays (no capability change), want-less rounds answered empty, the ack check is a new GitPackGen::common_haves seam (honest boundary at the trait), budgets unchanged kinds - docs/research/negotiation-captures.md: the normative negotiation record (grammar, client behavior, malformed-section failure modes) - transport.md: fetch section rewritten to the decided loop; references updated - OQ-02 resolved Verification: cargo test / clippy -D warnings / fmt --check / doc pass
109 lines
5.5 KiB
Markdown
109 lines
5.5 KiB
Markdown
# Research: V2 multi-round fetch negotiation captures
|
||
|
||
**Status**: complete
|
||
**Date**: 2026-09-25
|
||
**Client**: git 2.43.0 (duplex `git fetch` over git:// against a
|
||
ground-truth mock upload-pack that served the negotiation grammar; client
|
||
parsing rules confirmed against `fetch-pack.c` v2.43.0 `do_fetch_pack_v2`
|
||
/ `process_ack` / `send_fetch_request`). This is the normative
|
||
negotiation record ADR-014 is built on.
|
||
|
||
## Round grammar (definitive, source + captures)
|
||
|
||
A fetch round request is: `command=fetch`, caps (agent, object-format),
|
||
`0001` delim, then args (thin-pack, no-progress, ofs-delta, want lines,
|
||
have lines, done), then `0000` flush. **Wants are re-sent every round**
|
||
(`add_wants` runs unconditionally in `send_fetch_request`); common haves
|
||
are re-sent every round too (`add_common` — the accumulated common set),
|
||
plus new haves from the negotiator (`add_haves`).
|
||
|
||
Server response to a round **without** `done`:
|
||
|
||
```
|
||
acknowledgments
|
||
[ACK <oid>]* — each have the server recognizes (common)
|
||
[NAK] — when no acks in the round (observed: NAK alone)
|
||
0000 FLUSH — always, when ready was NOT sent
|
||
```
|
||
|
||
- The acknowledgments section terminates with **FLUSH when no `ready`**
|
||
and **DELIM (0001) when `ready` was sent** (source: `process_ack` —
|
||
"a DELIM is expected if ready is sent, and a FLUSH otherwise").
|
||
- If `ready` is sent, the packfile section follows in the same response
|
||
(optional shallow-info / wanted-refs / packfile-uris sections first,
|
||
each DELIM-terminated). We never send `ready` (see ADR-014), so our
|
||
section always ends at FLUSH.
|
||
- Client-side validation (observed by feeding it wrong shapes): acks
|
||
section containing `packfile` → `fatal: unexpected acknowledgment
|
||
line: 'packfile'`; acks + FLUSH after `ready` → `fatal: expected
|
||
packfile to be sent after 'ready'`; acks header missing → `expected
|
||
'acknowledgments'`.
|
||
|
||
## Client round behavior (captures + source)
|
||
|
||
- Have batching: first round sends up to 16 haves (`INITIAL_FLUSH`),
|
||
doubling per round (32, 64, … 16384 stateless / +32 duplex) —
|
||
client-side only; it is our budget's ceiling, not our obligation (the
|
||
ADR-009 haves default should sit at or above 16384 so legitimate
|
||
negotiations are not clipped).
|
||
- `done` is sent when: the negotiator has no more haves to offer
|
||
(`!haves_added`), or `seen_ack && in_vain >= 256` (`MAX_IN_VAIN`).
|
||
- Want-less round (captured): a client with everything up-to-date sends
|
||
a fetch round with no want lines and exits; the correct server answer
|
||
is an empty acknowledgments section (`acknowledgments` + `NAK` +
|
||
flush, no pack). The client then exits without a done round.
|
||
- On receiving ACKs (no ready): client marks commons, sends another
|
||
round (wants + common haves + new haves). Observed: after 5 ACKs the
|
||
client re-sent the same 5 as common haves plus continued negotiation.
|
||
- On NAK-only: client proceeds to `done` quickly when out of new haves
|
||
(observed: 1 have → NAK → next round want + done).
|
||
- The `done` round carries wants + common haves + (maybe no new haves) +
|
||
`done`. **The server must therefore honor haves present in the done
|
||
request when generating the pack** — that is where negotiation pays
|
||
off.
|
||
- On the `done` round the response has **no acknowledgments section**
|
||
(client goes straight to FETCH_GET_PACK): optional sections then
|
||
`packfile` + sideband + flush (POC-1's observed shape, re-confirmed).
|
||
|
||
## ACK line format
|
||
|
||
`ACK <oid>` — bare 40-hex oid accepted (`process_ack` parses `ACK ` +
|
||
exactly hexsz chars; suffixes like `continue`/`common`/`ready` after the
|
||
oid are the V0 grammar, not required in V2). `NAK` lines are tolerated
|
||
anywhere in the section (`continue` in the ack loop).
|
||
|
||
## Statelessness
|
||
|
||
Each POST (stateless http) re-derives cleanly: the client re-sends wants
|
||
and all common haves every round, so the server needs **no cross-request
|
||
state** — each round's haves are the complete common view. Duplex needs
|
||
no session state either (client re-sends). POC-1/POC-3 substrate facts
|
||
apply unchanged.
|
||
|
||
## Captures of interest (from mock negotiation runs)
|
||
|
||
- Round 1 `want + 5×have`, no done → `acknowledgments` + 5×`ACK <oid>`
|
||
+ `ready` + DELIM + `packfile` + band: client accepted framing and
|
||
consumed the pack section (failed later only because the mock's pack
|
||
was empty — `did not send all necessary objects`, which is the correct
|
||
client response to a pack missing wanted objects).
|
||
- Round 1 `want + 1×have`, no done → `acknowledgments` + `NAK` + FLUSH →
|
||
client round 2: `want + done` (haves remembered server-side are NOT
|
||
needed; the client trusts the server saw round 1's haves — but the
|
||
done round's pack correctness does not depend on it when the server
|
||
generated no pack in round 1: nothing was subtracted yet).
|
||
- After ACKs without ready (mock bug shapes): client re-negotiates,
|
||
re-sending wants + common haves. Errors observed for malformed
|
||
sections confirm the client's section parser is strict.
|
||
|
||
## Implications for alkgit (design, not capture)
|
||
|
||
1. Serve the no-done round with `acknowledgments` + `ACK <oid>` per
|
||
recognized have + `NAK` when none + FLUSH; never `ready` (honest
|
||
under `fetch=wait-for-done`).
|
||
2. On the done round, generate the pack as closure(wants) minus
|
||
closure(haves) — the GitPackGen seam already takes (wants, haves,
|
||
limits), so have-honoring is the negotiation payoff and free at the
|
||
trait boundary.
|
||
3. No cross-round server state required (client re-sends wants + common
|
||
haves every round). Budgets per ADR-009 (rounds cap, haves cap). |