pub struct LeanConfig {Show 50 fields
pub prefix: String,
pub root: PathBuf,
pub floor_secs: u64,
pub sole_writer: bool,
pub access: Access,
pub whole_put_max: u64,
pub chunked: bool,
pub orphan_grace_secs: u64,
pub retire_grace_secs: u64,
pub collect_every_secs: u64,
pub chunk_cache: Option<Arc<dyn ChunkCache>>,
pub chunk_fetch_parallelism: usize,
pub chunk_view: bool,
pub g_shadow: bool,
pub racy_at_gate: bool,
pub tree_reserve: bool,
pub tree_room_for_tests: Option<u64>,
pub racy_wait_min: usize,
pub alias_cache: Arc<AliasCache>,
pub chunk_target: usize,
pub chunk_min: usize,
pub chunk_max: usize,
pub max_bytes: u64,
pub max_files: u64,
pub fanout: usize,
pub upload_fanout: usize,
pub project_id: Option<String>,
pub fetch_inflight_max_bytes: u64,
pub fetch_drivers: usize,
pub range_get_min_bytes: u64,
pub range_get_chunk_bytes: u64,
pub range_get_parallelism: usize,
pub sentinel_mode: SentinelMode,
pub sentinel_min_interval_secs: u64,
pub sentinel_hourly_budget: u64,
pub sentinel_poll_secs: u64,
pub news_poll_secs: u64,
pub news_active_secs: u64,
pub news_min_interval_secs: u64,
pub claim_poll_secs: u64,
pub claim_poll_head_ms: u64,
pub claim_quiet_spacing_secs: u64,
pub claim_deadline_secs: u64,
pub drill_hold_commit_secs: u64,
pub drill_hold_gc_secs: u64,
pub event_trace: Option<Sink>,
pub untracked_sweep_secs: u64,
pub untracked_grace_secs: u64,
pub scan_threads: usize,
pub renew_within_secs: u64,
}Fields§
§prefix: StringBucket key prefix for this subtree (project- or subtree-scoped; the proxy’s tenancy boundary).
root: PathBufWorkspace root (the agent’s tree).
floor_secs: u64Publish cadence floor, seconds (the durability contract).
sole_writer: boolStamp sole_writer on every manifest this syncer installs.
Set for a workspace that is PUBLISHED rather than worked in — forge’s legible export is the shipped case. It tells every later reader that an object off its citation was moved by a stranger, so the reader refuses instead of adopting.
access: AccessRead or read-write (Access). Default read-write.
whole_put_max: u64Whole-object ceiling; larger files use multipart compose.
chunked: boolEntries per manifest chunk, in expectation, and the floor and ceiling around it (chunked design §3). Config rather than constants because §4 already anticipates this moving: the pointer body is O(entries / target), and past ~10M entries the target is one of the two things that can be changed instead of adding a second level. Not surfaced on the CRD — nothing has needed to tune it per workspace yet, and a knob with no caller is a support burden. Publish the manifest as a CHUNK LIST rather than one generation object per citation (chunked design). Default ON since 2026-09-04.
The migration is FAIL-CLOSED and one-way per workspace: a pointer-era binary cannot parse a chunked pointer, which is the point — every layout change has to make an old reader REFUSE rather than conclude the project is empty and re-seed over it.
Small projects are not penalised, which is what made turning it on unconditional rather than threshold-gated: under the target a project is ONE chunk, so it costs a pointer plus one object and two GETs to read — exactly what the single-generation layout already cost. The indirection was paid for by step one.
false is still a supported READ path forever (a workspace that
has not published since the migration) and is what the
single-layout tests set explicitly.
orphan_grace_secs: u64How long an unreferenced chunk must have sat before the reaper
may take it. Config rather than a constant because the model
makes it load-bearing in a workload-dependent way: it must
outlive the LONGEST publish, not the longest plausible one
(LeanChunkGCRacyGrace).
retire_grace_secs: u64RETIRE-AGE G (M1 of the 2026-09-24 simplification analysis): a
handle or chunk a commit stopped referencing is collected only once
its RETIREMENT is this old — not its write, which for a long-lived
file or chunk is long past the moment a UI save supersedes it. A
reader that loaded a document less than G ago can fetch everything
it cites. 0 = collect at once (the shape before; the battery’s
default). FLINT_SYNC_RETIRE_GRACE_SECS.
collect_every_secs: u64THE COLLECTION CADENCE (latency plan 2026-10-23, lever C): how often
a writer’s commit section runs the collection pass — read the retire
logs, reap what is due, the orphan sweep when it is due too, the
generation and chunk sweeps. Each pass is a LIST of retired/, of
manifests/ and of chunks/, and pointer GETs, all under the
lease; run on every publish they were seven sequential requests
added to every commit. Between passes nothing is deleted, so what a
commit retires waits up to G plus this long: storage, never safety
(the model’s reap and sweeps may run at any commit, or never).
0 = every commit, as before (the battery’s setting).
FLINT_SYNC_COLLECT_EVERY_SECS.
chunk_cache: Option<Arc<dyn ChunkCache>>Manifest chunks kept locally by content address, verified on every
use (chunkcache; latency plan lever A). None fetches every
chunk of every load, as before. The syncer keeps them under its
state dir, the gateway in memory.
chunk_fetch_parallelism: usizeChunk fetches a load keeps in flight at once: the misses of a
cached load, every chunk of an uncached one. 1 = one at a time,
the shape before. FLINT_SYNC_CHUNK_FETCH_PARALLELISM.
chunk_view: boolA commit reads the document chunk by chunk (latency plan lever G,
docview): only the chunks its paths fall in, and it re-chunks only
what it changed. false loads and merges the whole document, the
shape before. FLINT_SYNC_CHUNK_VIEW=off.
g_shadow: boolTests only: every chunk-view commit also runs the whole-document merge and asserts the same document, chunk list, retire set and per-path decisions (latency plan §12, “Verification”).
racy_at_gate: boolA checkout measures its entries’ racy flags when it opens the
agent’s gate, not when it wrote each file (latency plan §17;
LeanScanRacy’s MeasureAtGate), so the first scan hashes only what
it wrote in its last window. false leaves every checked-out entry
racy, the shape before. FLINT_SYNC_RACY_AT_GATE=off.
tree_reserve: boolThe tree’s reserved blocks are this syncer’s (the s3csi quota
image reserves 1/16 of itself for a group only the syncer’s worker
holds): tree CONTENT it writes — a checkout, a consume, a sync — is
held to what its tenant could still write (f_bavail), so the
reserve is left for the bookkeeping that must outlive a full tree
(safefs::leave_the_reserve). FLINT_SYNC_TREE_RESERVE=on, set by
the plugin on a quota’d tree; off, nothing is checked.
tree_room_for_tests: Option<u64>Tests only: the bytes leave_the_reserve reads as the tenant’s room
instead of asking statvfs (a full filesystem needs root to build).
racy_wait_min: usizeThe fewest files still racy at the gate for which the checkout opens
it the rest of their window later (at most two seconds), so the
first scan hashes none of them: about where hashing them costs more
than the wait. 0 never waits. FLINT_SYNC_RACY_WAIT_MIN.
alias_cache: Arc<AliasCache>This workspace’s chunks’ aliases, by address (docview): what lets
a commit answer “who else cites this handle” without the document.
Shared by every clone of the config, so a syncer’s views share it.
chunk_target: usize§chunk_min: usize§chunk_max: usize§max_bytes: u64Checkout refusal budgets (0 = unlimited).
max_files: u64§fanout: usizeBounded concurrency for uploads and checkout fetches. The 0b rig measured the sequential loops at 561-854 PUTs/s and 1,000-2,000 GETs/s; fan-out multiplies directly against those.
upload_fanout: usizeConcurrent UPLOADS. Deliberately separate from fanout, and
measured separately: the read path’s width is what the checkout
rig moved, this one is what the publish rig moved. Both paths are
byte-bounded, by different gates — the read side by
fetch_inflight_max_bytes here, the upload side by the store’s
own gate (with_upload_inflight_max_bytes, 512 MiB from the
binary), which charges every whole body and every multipart part
from before its read until its PUT returns. So this number sets
request concurrency, not peak RSS.
project_id: Option<String>The project this workspace claims to be, stamped from the CR
(FLINT_SYNC_PROJECT_ID). When set, lease::verify_claim
refuses to run over a prefix whose claim cell names another
project — the operator’s refuse-foreign, enforced on the DATA
plane (audit 2026-09-03, finding 5). None = unstamped: no
check, the pre-operator posture.
fetch_inflight_max_bytes: u64Ceiling on bytes in flight across the checkout fan-out window.
fanout bounds the number of concurrent fetches but NOT their
size, and get_whole holds each whole object in RAM before it
reaches disk — so peak RSS was fanout x largest object, an
unbounded product of two numbers nobody sets together. A 32-wide
window over 64 MiB objects is 2 GiB in a syncer that ships with
no memory limit at all. Each entry takes permits proportional to
its size, so small-file trees still run the full width.
fetch_drivers: usizeDriver tasks for the checkout fan-out (see materialize). Each
runs fanout / fetch_drivers fetches at a time over its own
slice of the tree, so the per-request SDK work spreads over that
many cores instead of one. Default: the machine’s cores, at most
8 (FLINT_SYNC_FETCH_DRIVERS / spec.fetchDrivers, 0 = auto).
Measured 2026-09-12: one driver is the ~3,300 files/s plateau on
i4i.xlarge whatever fanout says; four are 6,188-6,521 at
fanout 256, and fanout 256 beats 128 for the first time.
range_get_min_bytes: u64Materialise an object this size or larger as PARALLEL RANGES
rather than one whole-object GET. 0 disables it.
Defaults to 8 MiB on the measurement that the repo’s rule asked for — runcr, i4i.large spot, us-west-1, one AZ, n=3, arms interleaved per rep, RANGES not means:
6 x 1 GiB 71.33-71.88 s -> 21.24-23.96 s 4 GiB + 2k x 16 K 57.02-57.83 s -> 15.30-18.30 s 20k x 8 KiB 16.92-19.44 s -> 16.40-17.39 s
The third row is the one that justifies a THRESHOLD rather than
ranging everything: every object there is under it, the ranged
path never fires, and the arms agree. Before this, the shipped
path lost a cold 6 GiB checkout to a plain aws s3 cp of the
same prefix on the same node (21.7 s); it now ties it.
Ranging also LOWERS peak RSS for a large object — see below — so this default costs memory nothing and returns some.
NOTE the interaction with range_get_chunk_bytes: a single part
is “one whole GET with extra steps” and fetch_ranged declines
it, so an object in [range_get_min_bytes, range_get_chunk_bytes)
passes this threshold and still takes the whole-object path. With
the defaults below the EFFECTIVE threshold is therefore 16 MiB,
not 8.
Two costs of get_whole are paid together and this is what
removes both: one object is one TCP stream, so a multi-GiB
checkpoint runs at single-connection throughput no matter how
wide the fan-out; and the body is held whole in RAM before it
reaches disk, which is the product fetch_inflight_max_bytes
exists to bound. Ranges write at their offset as they land, so
peak RSS per object becomes range_get_chunk_bytes x range_get_parallelism instead of the object’s size.
range_get_chunk_bytes: u64Bytes per range request. Large enough that per-request overhead is noise against S3’s first-byte latency; small enough that a retried range is cheap.
range_get_parallelism: usizeRanges in flight FOR ONE OBJECT. The checkout’s byte semaphore still bounds the whole window, so this multiplies concurrency only where the fan-out has run out of objects to work on — which is exactly the tail this exists to shorten.
sentinel_mode: SentinelModeSentinel posture (D0.4 pre-flight override).
sentinel_min_interval_secs: u64A consumed sentinel arriving sooner than this after the previous sentinel-honoring barrier waits; touches inside the interval coalesce into the pending record.
sentinel_hourly_budget: u64Work-metered hourly cap (D3.1): a honor charges
max(1, ceil(published_bytes / whole_put_max)) units, or 0 for
a no-diff honor. Exhaustion defers honors to the floor tick —
the workspace degrades to exactly cadence behavior.
sentinel_poll_secs: u64Sentinel poll cadence (env-only, not a fleet contract).
news_poll_secs: u64While the workspace is active, read the pointer (a writer the cell
too) this often, and run the floor’s barrier early when a peer or
the gateway moved it. 0 = off: news waits for the floor, as
before. FLINT_SYNC_NEWS_POLL_SECS.
news_active_secs: u64How long after the last sign of work (a publish, a consume, an ack,
a served request, news) the workspace counts as active. Past it
the poll stops and only the floor reads the bucket.
FLINT_SYNC_NEWS_ACTIVE_SECS.
news_min_interval_secs: u64The fewest seconds between two news-triggered barriers. News inside
it waits, so two busy writers cannot answer each other’s publishes
faster than this. FLINT_SYNC_NEWS_MIN_INTERVAL_SECS.
claim_poll_secs: u64How often a barrier waiting for the cell re-reads it.
claim_poll_head_ms: u64The poll cadence of the waiter at the head of the queue
(lease::CLAIM_POLL_HEAD_MS).
claim_quiet_spacing_secs: u64Minimum spacing between two observations that COUNT toward the
dead-holder (QUIET_POLLS) and abandoned-handoff thresholds.
The tests set it to 0 so a deposal takes six polls, not a minute.
claim_deadline_secs: u64Longest a barrier waits for the cell before it is abandoned and retried at the next floor.
drill_hold_commit_secs: u64DRILL-ONLY (FLINT_SYNC_DRILL_HOLD_COMMIT_SECS, never stamped by
the operator): sleep this long inside the commit section, right
after the claim. The one straggler the per-barrier lease still
has is a holder that stalls INSIDE its commit section, and that
section is milliseconds long — no drill can hit it by timing, so
this is how run-writers.sh opens the window. 0 = off.
drill_hold_gc_secs: u64DRILL-ONLY (FLINT_SYNC_DRILL_HOLD_GC_SECS, never stamped by the
operator): sleep this long before the collector’s batched DELETE
of what the install retired, once per barrier. Under immutable
handles the collector is one unconditional delete_many with no
HEAD before it, so the window a drill wants is the one between
the CAS and that batch — a peer’s sweep, or its commit, running
while this barrier still owes its tidy-up. 0 = off.
event_trace: Option<Sink>The protocol event trace (trace.rs); None = off.
untracked_sweep_secs: u64How often a WRITER sweeps for an upload a lost writer left at a
cited key (untracked.rs, finding 10), seconds;
FLINT_SYNC_UNTRACKED_SWEEP_SECS, 0 = off. Each sweep is one LIST of
the files prefix plus a manifest and an inbox read.
untracked_grace_secs: u64How long such an object must have sat untracked before the sweep
tracks it — the gateway’s UNTRACKED_GRACE_SECS, past any live
writer’s claim and commit.
scan_threads: usizeThreads the scan walks the tree with (scan::scan_with, lever E);
FLINT_SYNC_SCAN_THREADS. The CPUs this process may use, at most
8; 1 walks with one worker.
renew_within_secs: u64How old the last cell WRITE (claim or renew) may be before a delete in the commit section renews first (review 2026-09-18, C2). A deposal needs the token still for six spaced polls (60 s), so a write younger than this is proof no deposal has happened; an older one is renewed — a conditional write that a deposal has moved, so it fences. Comfortably below the threshold. Tests set 0 to collapse a stall.
Implementations§
Source§impl LeanConfig
impl LeanConfig
pub fn new(prefix: &str, root: impl Into<PathBuf>) -> Self
Sourcepub fn file_key(&self, path: &str) -> String
pub fn file_key(&self, path: &str) -> String
The INGRESS key of a path: where an outside writer’s aws s3 cp
lands, and where every object of this workspace lived before the
immutable-handles layout (design 2026-09-19, R5). Nothing this
crate publishes writes it any more; the read door, the consume and
the checkout fetch HANDLES.
Sourcepub fn handle_key(&self, path: &str, flush: &str) -> String
pub fn handle_key(&self, path: &str, flush: &str) -> String
A HANDLE: the one key a write lands at, files/<path>@<flush>
(design 2026-09-19, R1). Nobody else ever writes it, the manifest
cites it by name, and it is deleted only once no document cites it
(R3). It sorts beside the bare path, so a listing of a directory
shows every live version of a file together. flush is the
writer’s flush id: a barrier’s uuid, or the gateway’s ui-… form
(ui_flush), whose ORDER a consume can read off the name.
Sourcepub fn key_fits(&self, path: &str) -> bool
pub fn key_fits(&self, path: &str) -> bool
Whether every handle path can have is a key the store takes
(L-145): its handle under the longest flush id anyone mints
(MAX_FLUSH_LEN) within MAX_KEY_BYTES. One rule for every
writer, so a path the agent can publish the UI can save too. A
file whose path does not fit is HELD by the barrier (as a name that
is not UTF-8 is), and the gateway refuses to save or move one
there: the PUT would fail on S3 at every attempt.
Sourcepub fn files_prefix(&self) -> String
pub fn files_prefix(&self) -> String
The files namespace this workspace’s handles and ingress objects live under, with its trailing slash — what a sweep lists.
Sourcepub fn handle_parts<'a>(&self, key: &'a str) -> Option<(&'a str, &'a str)>
pub fn handle_parts<'a>(&self, key: &'a str) -> Option<(&'a str, &'a str)>
(path, flush) of a handle under this prefix; None for a key
that is not a handle here — the bare path of an ingress object
included, so a sweep can tell the two apart by the name alone. A
path may itself contain @: the flush is the LAST @-suffix, and
a flush is a uuid or the gateway’s ui- form, neither of which a
path component would be mistaken for in the same position.
Sourcepub fn manifest_key(&self) -> String
pub fn manifest_key(&self) -> String
The LEGACY single-object manifest key. Still read (a workspace
written before the pointer layout has one) and, once migrated,
overwritten with a document that cannot parse as a manifest —
see manifest::REFUSAL_DOC for why deleting it would be a
data-loss shape rather than a tidy-up.
Sourcepub fn current_key(&self) -> String
pub fn current_key(&self) -> String
The pointer: the ONLY mutable metadata object. Small, CAS’d on every citation and on every takeover rotation.
Sourcepub fn generation_key(&self, seq: u64, flush_uuid: &str) -> String
pub fn generation_key(&self, seq: u64, flush_uuid: &str) -> String
One immutable generation of the entries.
Zero-padded seq FIRST, so a plain lexical listing is also
chronological — what the reaper and a human with mc ls both
want — and the writer’s flush uuid after it, so the key is
unique per WRITE rather than per generation. Two writers that
reach the same seq therefore write two different objects and
race only at the pointer, which is the one place a race should
be decided; and a path that legitimately rewrites without
bumping is not refused by a write-once key it never meant to
collide with.
Sourcepub fn chunk_key(&self, addr: &str) -> String
pub fn chunk_key(&self, addr: &str) -> String
A chunk object, addressed by CONTENT rather than by generation. Two generations that share a chunk share the object, which is where the O(changed) publish actually comes from — naming chunks by seq would rewrite every one of them on every publish even when their contents were identical.
pub fn inbox_key(&self) -> String
pub fn epoch_key(&self) -> String
Sourcepub fn claim_key(&self) -> String
pub fn claim_key(&self) -> String
The operator’s claim cell for this prefix (lean_operator:: reconcile::claim_key writes it; the syncer only READS it).
pub fn conflict_key(&self, uuid: &str, path: &str) -> String
Sourcepub fn draft_body_key(&self, user: &str, path: &str) -> String
pub fn draft_body_key(&self, user: &str, path: &str) -> String
A per-user DRAFT: a durable edit that has NOT been published.
Under LEAN_DIR, which is the whole safety argument: that
namespace is never scanned, never checked out, never cited by a
manifest, and never swept (both sweeps are prefix-scoped —
manifest::sweep_generations to manifests/, sweep_chunks to
chunks/). So a draft is durable in the bucket while being
invisible to classify, to checkout, to every other user, and
to the agent — which is exactly what “saved but not published”
has to mean.
Body and meta live under DISJOINT subtrees rather than sharing
one with a suffix: with drafts/<u>/<path>.meta a file legally
named notes.meta would have its BODY at the key that is
notes’s META, and one user’s draft would silently read as
another draft’s base etag.
Sourcepub fn draft_meta_key(&self, user: &str, path: &str) -> String
pub fn draft_meta_key(&self, user: &str, path: &str) -> String
The draft’s base: which version of files/<path> it was edited
against. Written AFTER the body, so the only crash residue is a
body with no meta — an INCOMPLETE draft, which promote refuses
by name rather than publishing against a base it had to guess.
Sourcepub fn draft_meta_prefix(&self, user: &str) -> String
pub fn draft_meta_prefix(&self, user: &str) -> String
Listing prefix for one user’s draft metas (the resume view).
pub fn state_dir(&self) -> PathBuf
Sourcepub fn control_dir(&self) -> PathBuf
pub fn control_dir(&self) -> PathBuf
The workspace-local control namespace (D0).