Changes touching this path
- day 0: loot hosts loot
f4c30e75 · dbf3dbe6… - evidence: crew minted and verified (#86)
1fada823 · dbf3dbe6…diff - normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text)
e58fdda6 · dbf3dbe6…diff - Relay: explicit 64 MiB body limit; push batches byte-capped (#309)
0ac2fa23 · dbf3dbe6…diff - loot push: send the change delta once per push, not once per 32-object batch (#680)
The defect, receiver-counted (#633) by the CLI tier (#635): a push of the
2000-file fixture put 14.4 MB on the wire for ~0.5 MB of bundle, because
every 32-object batch re-carried the full change delta — O(batches x delta)
where O(delta) is available, the same shape #98/#99 fixed one level up.
bundle_wanted_batched now builds the first bundle exactly as before (change
delta, attestations, purge lane, first object batch) and every follow-up as
object-only: the batch ciphertext plus the public keys for exactly those
objects. Keys stay per-batch because a receiver files a riding key only for
an object stored from the same bundle; everything else lands with bundle
one, ahead of all object bytes (the sequential send in cmd_push is
load-bearing).
Compatibility class: no format change in either direction. A Sync frame
with changes=0 has been a valid encoding since v1 (the golden fixtures pin
it) and stow stores objects without requiring a change in the same bundle,
so the deployed relay ingests the new shape on its existing path — and an
old client re-sending the delta every batch stays accepted (pinned by a
forge test). bundle_impl is untouched, so single-bundle products —
bundle_bytes, the fetch server, the metadata-only /ingest bundle — are
byte-identical.
Resume after interruption still works and is tested: an interrupted push
leaves the relay with an incomplete closure, negotiation_have (#217)
refuses to claim such heads, and the re-run re-offers the delta in its own
first bundle — once per attempt, never once per batch.
Forge: /stow defers the change at batch one and no longer resolves it
alone; the closing /ingest (whole delta, all keys, unchanged) does. An
object arriving in a change-less follow-up takes introduced_by from the
envelope pusher — same key as the author on a self-push. Documented at
loot-forge::ingest and in the ADR 0024 amendment.
Measured with loot-perf-cli from this lane against a loopback relay,
small_files/2000, reps 2, idle box: wire_bytes 14392934 -> 600236 bytes
(-95.8 percent), bit-exact story unchanged (0 discarded batches).
1f59fa97 · dbf3dbe6…diff - what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799)
#40's fault-injection harness found a peer holding 3 of 6 files whose head had
advanced to the sender's tip, with the negotiation reporting up to date. The
ticket read that as "an interrupted pull cannot resume." It does resume:
`pull_via_interrupted_fetch_resumes` has been proving that since #217, and
`pull_via` declares `negotiation_have()` at all three of its call sites.
The harness resumed by declaring `bob.repo.heads()` instead. That is the
defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>`
and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc
said "what a peer passes as have", so the trait was teaching it. `loot clone`
was doing it too, correct only because a freshly inited repo has no heads.
What a peer may declare is now `Have`, and `negotiation_have()` is the only
thing that makes one — the #217 filter is the check the type certifies.
`Have::nothing()` is the one other constructor, for transports probing a relay
and clients with no repo to ask yet; declaring less can only over-send, and
over-declaring is the strandable direction. Threaded through the
receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and
the forge's signed twins.
The sender side keeps `&[Oid]` deliberately. `have` means one thing in both
directions — what the recipient of the bundle holds — so there is no second
concept to name; what differs is provenance, and a type cannot carry a
guarantee across a network. Wrapping it would advertise a check that does not
happen. That reasoning lives on `Have`'s doc, which is the only place it is
written.
Candidate 1 from the ticket — refuse to advance the head over an incomplete
closure — is not built, because ADR 0024 already priced and rejected it under
"cross-batch atomicity is deliberately not provided". A confirmation note says
so there, so the next reader does not re-derive it. The new
`tests/sync_resume.rs` asserts the head advance rather than treating it as the
failure, and covers the fault the suite genuinely missed: a bundle that is
short but valid, where every batch succeeds, the pull returns Ok, and the
operator is told it worked while holding half the files. The next pull heals
it, which is what is pinned.
Acceptance criterion 3 ("am I up to date" must answer no while any object is
absent) is deliberately not built: `closure_complete` is unsatisfiable on the
forge path, where a reader legitimately never receives ciphertext it holds no
grant for, so the warning would fire forever on every forge repo with
restricted content. That is #803.
`an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted.
271dd5cf · dbf3dbe6…diff - the docs gate themselves: a land refuses a link that resolves to nothing, five rotted ADR cross-links are fixed, every amended ADR warns in its status, and the artifact sweep retires with its producer
586e3260 · dbf3dbe6…diff - a forge push declares the delta past the heads the forge itself names rather than the whole history: declare_forge_heads built its bundle with have = &[] while the RefState it had just read for the CAS generation carried heads it read no other field of, so every push re-encoded and re-POSTed every finalized change in the repo, each with its whole manifest (#288), however little had moved. this is push own #728 move made on the route a forge declares heads on, and the forge side needed nothing built: prepare resolves against already-stowed state and the completeness refusal is over the changes in the bundle, so a change the /stow batches defer is one the push is introducing, which nothing the forge declares reaches, and it still rides. a declared head this position does not hold expands to nothing because the closure walk filters its seeds, so it subtracts nothing and whatever only it would have covered is sent as before, which is the safe direction since /ingest writes content-addressed change rows. WHAT IS GIVEN UP IS NAMED RATHER THAN LEFT TO BE FOUND LATER: the ANYONE key lane no longer re-presents a key for every published object in the history on every push, so a publication row missing for history already ingested no longer heals on the next push of anything; the steady state is untouched, published_key being global and append-only and publication being per oid and repo with a terminal retraction, both written by the ingest that first carries the change. measured twice and both readings live in the tree rather than out of repo: on the real binary against a real forge the /ingest request body of a push moving one change over a 7-change history of 16 paths reads 8880 B before and 2104 B after, and on the bundle at two depths over 24 paths the empty have reads 12039 B over 8 changes and 22615 B over 16 while the scoped arm reads 2817 B over 1 change at both, so the before figure grows with the history where the after one does not. red under mutation, counts read each time: the helper made to ignore its have failed both pins (30 passed and 1 failed in the sync unit tests, 11 passed and 1 failed in forge_push), the call site made to pass the empty have again failed only the wire pin as designed (31 passed and 0 failed in the unit tests, 11 passed and 1 failed in forge_push, reading 8880 B against the 7906 B of the first push), and the deep fixture made no deeper than the shallow one failed the growth control (30 passed and 1 failed, 12039 B then 12039 B), each restored to 31 and 12 passed with 0 failed. the now-false prose is corrected where it stood: RemoteSync haves and declared_heads, the loot-forge ingest and publish module docs, the publish re-verification comment, two forge test comments, the ADR 0024 amendment and the CONTEXT Ingest entry. no migration, no wire or format byte moves and no host behaviour moves, but what a client sends at /ingest moves, so this rides the next release and owes no deploy. the workspace suite is green (4142 passed over 132 binaries, 8 ignored) (#2222)
22beae44 · dbf3dbe6…diff - a forge push stows the delta past the forge heads rather than the whole change delta: push round zero now asks a forge on /ref, the route the closing declaration already reads, where RemoteSync::haves answered None for a forge and so negotiated nothing, and stow_batches, the round-zero and round-one planning lifted out of push_with, scopes the object offer and the first /stow batch by the closure of those heads, as it already did for a relay (#728) and as loot propose already does. it strands no object: a forge answers /wants from its object rows, and a change row exists there only once every object its tree names has a row, because prepare resolves against those rows, /stow defers what it cannot resolve, /ingest refuses it and a declared head must be held, and no MetadataStore method removes an object row today; so the scoped offer is asked for exactly what the unscoped one is, pinned against a real forge, and an interrupted push moves no ref, so the next push offers its change again, pinned by stowing one batch of three and reading the other two asked for. given up, and recorded at the code: a forge holding a head without its ancestry, the shallow push ADR 0089 section 6 refuses at the client, is no longer re-sent the changes it lacks. the cost is a /ref round trip ahead of /wants; a forge whose /ref cannot be read is planned the full delta as before, and the unscoped plan an older client sends is still taken, since no forge line, route or wire byte moves. read on the real binary against a real forge, 16 paths, one change pushed over a history the forge holds: /stow bodies of 8,873 B over 7 changes and 16,657 B over 15 before, 2,097 B at both after, and /wants bodies of 835 B and 1,091 B before, 611 B at both after; on the bundles, 24 paths, 13,405 B over 9 changes and 23,981 B over 17 before, 2,861 B over 1 change at both after. red first (14 passed and 1 failed). red under mutation, counts read each time, unit then wire: the forge not asked (2 passed and 1 failed, 0 and 1), the bundle unscoped (2 and 1, 0 and 1), the offer unscoped (3 and 0, 0 and 1), the own heads taken as the forge heads (1 and 2, 0 and 1), and an unread /ref taken as an empty head set (2 and 1, 1 and 0, a reading that plans the same full delta), each restored to green. CONTEXT.md and ADR 0024 record it. no migration, no format byte and no published wording moves and the forge is untouched, so this owes no deploy. the workspace suite is green (4390 passed over 139 binaries, 12 ignored) (#2308)
3c87a4a7 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.