Files
alkgit/docs/research/negotiation-captures.md
T
glm-5.3-flash d6d013e522 docs(architecture): resolve OQ-02 — V2 negotiation ack loop (ADR-014)
- 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
2026-09-25 04:05:11 +00:00

5.5 KiB
Raw Blame History

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).