# 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 ]* — 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 ` — 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 ` + `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 ` 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).