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

109 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).