Skip to content

feat(content): VSS-snapshot content-ingest pipeline for Docenta (UFI.0-UFI.6) - #563

Merged
githubrobbi merged 98 commits into
mainfrom
feat/uffs-content-scaffold
Jul 20, 2026
Merged

feat(content): VSS-snapshot content-ingest pipeline for Docenta (UFI.0-UFI.6)#563
githubrobbi merged 98 commits into
mainfrom
feat/uffs-content-scaffold

Conversation

@githubrobbi

Copy link
Copy Markdown
Collaborator

Summary

Ships the full UFFS-side half of the Docenta content-ingest protocol: a
VSS-snapshot-backed, privileged content-export pipeline that streams file
content for a UFFS query result over a framed wire protocol, without
exposing a raw volume/snapshot handle to the downstream consumer.

New crates:

  • uffs-content — the Coordinator: VSS snapshot leasing, ephemeral uffsd
    spawn + candidate enumeration, job workflow, transport server (--serve).
  • uffs-content-protocol — Coordinator<->consumer wire protocol (frame
    envelope, all 12 frame types, manifest format, codec, error taxonomy).
  • uffs-content-reader / uffs-content-reader-protocol — the privileged
    Reader process and its Coordinator<->Reader IPC protocol
    (OpenFileById against the snapshot device).
  • Broker: Snapshot Manager wire protocol + lifecycle (uffs-vss-requestor,
    --self-test-vss).

Key milestones (UFI.0-UFI.6, see
docs/dev/architecture/uffs-ingest-protocol-v2-vss.md for the full spec):

  • job/failure-bucket ledger, credit-based backpressure, JOB_RESUME +
    connection-blip resume
  • real VSS snapshot lease + privileged Reader content pipeline
  • drive-type-aware read concurrency (SSD: N connections, HDD: 1, avoids
    seek thrashing) and sliding-window pipelining
  • consumer-facing delivery ceiling, explicit protocol_version
    negotiation/rejection, JOB_BEGIN snapshot provenance
  • multi-root JobRequest defaulting to all local NTFS drives, skip
    VSS-unsupported (removable) drives instead of aborting
  • concurrent root enumeration and concurrent per-drive content reads
    (real-hardware benchmarking confirmed both were sequential bottlenecks)
  • volume-hint handle caching + opt-in known_logical_size to skip
    redundant CreateFileW/GetFileSizeEx per candidate
  • full tracing instrumentation across the job pipeline + ephemeral
    daemon log surfacing
  • docs/architecture/filtering-reference.md — filter/type-group reference
    for Docenta's consumer-side extension handling

Real-hardware validated: 721,532-candidate / 66 GiB run across 6 drives
(SSD + HDD), 4 transient read races on actively-changing files correctly
classified retryable (not terminal), 0 unexpected failures.

Test plan

  • Full workspace test suite (host + cargo xwin test --no-run)
  • lint-prod / lint-tests, plain + pedantic + nursery clippy
  • cargo xwin doc --workspace --document-private-items -Dwarnings
  • Recorded frame-stream replay fixture (golden-byte conformance)
  • Real-hardware multi-drive VSS content-export run (see summary)

githubrobbi and others added 30 commits July 14, 2026 12:56
Captures why the 0.12.28 pin still holds (0.13 replaced the
rustls-tls-native-roots feature with a rustls-platform-verifier +
pluggable-crypto model), the recommended rustls-no-provider + ring path that
avoids the aws-lc-sys C build, the xwin/musl cross-compile risk, and a full
validation + rollout + rollback plan. Planning only, no code change.
`just winget-av-submit <tag>` builds the WDSI false-positive submission archive
(password-protected, from the release's actual uffs-windows-x64.zip contents so
it never drifts from the real bin set) and prints the portal URL, SHA-256s, and
every form field to paste. A non-blocking Defender scan in release.yml warns at
release time when the Windows binaries will likely trip the recurring winget
Validation-Defender-Error, pointing at the helper. README documents the drill.
Stopgap until Authenticode signing lands.
…UTF-16 anti-pattern gaps

resolve_ext_ids zipped an unbounded 0_u16.. range against ext_names.iter();
RangeFrom<u16>::next() computes its next state eagerly, so once a drive's
ext_names table reached its legitimate u16::MAX ceiling, a query extension
absent from that drive drove the range one step past u16::MAX and panicked
with "attempt to add with overflow" - taking the whole daemon down. Switched
to .enumerate(), which cannot overflow. Also closes two from_utf16_lossy
anti-pattern gaps: $ATTRIBUTE_LIST/$UsnJrnl:$J name decoding now routes
through the crate's shared malformed-name-safe decoder (WI-4.1) instead of
a redundant lossy implementation, and the two genuinely non-filename Win32
sysinfo decodes (volume label, registry string) are marked AUDIT-OK.

Also documents the --agg <PRESET> top-N cap and its terms:FIELD,top=N
escape hatch in --agg --help, and splits args.rs's static help text into
args_help.rs to stay under the workspace's 800-LOC file policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A production report showed `uffs --daemon start` failing with "Daemon did
not become ready in time / request timed out" on a 7-drive / 25M-record
machine whose load legitimately took 2m27s - 27s past the (fixed) 2-minute
client timeout - even though the daemon was healthy and fully loaded
moments later per `--daemon status`.

await_ready's `timeout` is now an idle budget instead of a hard wall-clock
cutoff: every DaemonStatus::Loading response whose drives_loaded advances
resets the deadline, so a daemon under heavy system load that keeps
visibly making progress is never killed by an arbitrary fixed cutoff -
only one that stalls for a full timeout window is. A hard 5x outer
ceiling still bounds the total wait. Applied to both the sync client (hit
by the CLI) and its async sibling, with new tests proving both the
extend-on-progress and still-times-out-on-stall behavior.

connect.rs split connect/auto-start/retry into connect_autostart.rs to
stay under the workspace's 800-LOC file policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bare-bones Layer 0 (uffs-content-protocol: shared wire types) and Layer 4
(uffs-content: unprivileged coordinator binary) crates for the new
VSS-snapshot-scoped content-export tool. No job intake, VSS, MFT, or
streaming logic yet - placement and naming only, built against Docenta's
uffs-ingest-protocol-v2-vss.md contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
paths, and manifest wire types (UFI.0)

Implements the first slice of the UFFS Content-Ingest protocol per
uffs-ingest-implementation-plan.md UFI.0:

- codec: bounds-checked LE Reader (every length-prefixed field validated
  before allocation, per the enterprise-review's Finding H10), BLAKE3
  digest/checksum32 helpers. Digest is locked as plain unkeyed BLAKE3-256
  so Docenta can use content_digest directly as its content ID.
- error: full stable ErrorCode taxonomy (design-doc S16) with round-trip
  tested as_str()/FromStr.
- state: CandidateOutcome (4-way, unchanged from the addendum) and a new
  JobState lifecycle with a proptested-shape legal-transition graph,
  mirroring uffs-daemon's ShardState pattern.
- path_encoding: lossless UTF-16LE WindowsPath (handles unpaired
  surrogates, which a String-based representation cannot even hold).
- manifest: ManifestHeader/CandidateRecord/ManifestTrailer per design-doc
  S11, with self-describing length fields and BLAKE3-truncated checksums,
  full round-trip + mutation + proptest coverage.

83 tests, clean under lint-prod + lint-tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…UFI.0)

Implements the framed content/control stream per design-doc S12:
FrameEnvelope (magic/version/type/flags/checksums, payload bounds-checked
before allocation) plus JobBegin, FileBegin, ContentChunk, FileEnd,
FileFailed, FileDeferred, FileAck, JobEnd, Progress, Heartbeat, JobCancel,
and WindowUpdate. Split into crates/uffs-content-protocol/src/frame/{mod,
job_begin,file_begin,content_chunk,file_end,file_failed,file_deferred,
file_ack,job_end,control}.rs plus a sibling tests.rs (matching the
compact_cache.rs / tests.rs convention) — a single frame.rs file would
have tripped the 800-LOC file-size gate.

Incorporates one deliberate protocol extension beyond the base spec,
driven by concrete downstream (Docenta) consumer feedback: a two-tier
query/delivery model. Candidate-match filters (ext/date/size-min, same
shape as existing UFFS CLI filters) determine the full candidate set,
independent of a separate content-delivery ceiling
(JobBegin.max_content_delivery_bytes) that controls which already-matched
candidates get a body streamed. Modeled as ReadMode::MetadataOnly +
FileEnd.content_digest: Option<Digest> rather than a new outcome variant,
so the addendum's already-reviewed 4-way completeness formula
(candidate_count = succeeded + failed_retryable + failed_terminal +
deferred_manual) is untouched — a MetadataOnly file is still Succeeded,
just without a delivered body. This keeps large-file metadata available
for a consumer's reap/tombstone reconciliation without transferring bytes
the consumer would only record as metadata anyway.

Adds codec::Reader::read_bytes_exact for payload reads whose length was
already validated via a separately-encoded field (frame envelope), as
opposed to a wire length-prefix.

31 new tests (108 total in the crate), clean under lint-prod + lint-tests
+ the file-size policy gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds crates/uffs-content-protocol/tests/golden_fixtures.rs plus 10 frozen
binary fixtures under tests/fixtures/*.bin: manifest header, candidate
record, manifest trailer, and framed JOB_BEGIN / FILE_END (success) /
FILE_END (metadata-only) / FILE_FAILED / JOB_END, plus two deliberately
invalid fixtures (corrupt checksum, truncated frame) that must be
rejected.

These decode frozen, committed bytes rather than regenerating them from
the current encoder each run — a test that reproduced its own expected
bytes every time would never catch an accidental wire-format regression,
since it would just compare new (wrong) output against itself. This is
also the cross-language conformance surface the addendum (§5.5) calls
for: "a future implementation in another language is supported only
after passing the same conformance corpus."

Regeneration is deliberately gated: `#[ignore]`d tests only write a
fixture when UFFS_REGENERATE_FIXTURES=1 is set, so a routine `cargo test`
run can never silently overwrite the frozen contract.

Added a .gitignore carve-out for these fixtures (blanket *.bin is
otherwise gitignored workspace-wide), following the existing
upcase-tables / MFT-capture carve-out pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (UFI.0)

New Layer 0 crate implementing the private wire protocol between
uffs-content (unprivileged Coordinator) and the privileged Snapshot
Reader process, per the addendum's corrected architecture (S2.1-S2.4):
ReadRequest/ReadResponse, VolumeIdentity, StreamKind, RequestedReadMode,
ActualReadMode, and a narrow ReaderErrorCode subset.

Deliberately does NOT depend on uffs-content-protocol even though both
are Layer 0 - per crate-graph.md, Layer-0-to-Layer-0 internal deps are
disallowed so each stays independently buildable. Duplicates a small
(~300-line) bounds-checked LE codec rather than sharing
uffs-content-protocol's, which is the direct cost of that independence
rule - documented in the crate's Cargo.toml.

The Reader MUST revalidate every ReadRequest field against the snapshot
itself (identity, EOF/VDL, stream) rather than trusting the Coordinator's
claimed range - documented on ReadRequest per addendum S2.3's "resolves
the unnamed stream itself" requirement.

38 tests, clean under lint-prod + lint-tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends uffs-broker-protocol with the Broker's VSS-lifecycle API per
addendum S4.3: CreateSnapshotLease(Result), DuplicateSnapshotHandle,
RenewSnapshotLease, ReleaseSnapshotLease, QuerySnapshotLease, wrapped in
tagged SnapshotManagerRequest/Response envelopes, over a new
SNAPSHOT_PIPE_NAME distinct from the existing daemon<->Broker PIPE_NAME.

DuplicateSnapshotHandle only carries the reader PID over the wire - the
actual DuplicateHandle call and identity verification happen Broker-side
via the existing check_client_identity pattern, never trusted from the
wire (documented on the type).

Split into snapshot_manager/{mod,codec,messages}.rs + a sibling tests.rs
(same pattern as uffs-content-protocol/src/frame/) since one flat file
would have exceeded the 800-LOC file-size gate. codec.rs duplicates the
same small bounds-checked LE primitives as the two content-protocol
crates rather than sharing them, for the same Layer-0-independence
reason.

Existing HandleRequest/HandleResponse (daemon<->Broker MFT handle
protocol) are unchanged.

19 new tests (52 total in the crate), clean under lint-prod + lint-tests
+ file-size policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds crates/uffs-content/src/db/{mod,schema,queries}.rs implementing the
addendum §6 job database: uffs-content-jobs.sqlite3, WAL + foreign_keys +
synchronous=FULL pragmas, and the six-table schema (jobs, candidates,
attempts, consumer_acks, snapshot_leases, job_events) from addendum §6.4.

queries.rs adds a narrow typed API (create_job, insert_candidate,
record_terminal_outcome, completeness_summary) - just enough to prove two
things with real SQLite, not mocks:

- the completeness invariant (design-doc §2.2/§21.7): candidate_count ==
  succeeded + failed_retryable + failed_terminal + deferred_manual, with
  both a passing and a still-incomplete case;
- crash-recovery durability (design-doc §19.2): write a job with a mix of
  terminal and still-pending candidates, drop the connection without a
  clean job-complete step, reopen from the same file, and confirm
  completed candidates stay completed.

record_terminal_outcome takes a new TerminalCandidateState enum (the
four terminal variants only, no Pending) rather than accepting the full
CandidateState and asserting/panicking on a non-terminal value - passing
Pending is now a compile error, so there is no runtime check to get
wrong and no panic in production code.

New workspace dependency: rusqlite (bundled feature, so the Windows
cross-compile via cargo-xwin doesn't need a system sqlite3 import
library). This has not yet been through cargo-vet - flagging for a
follow-up audit pass before this branch ships.

10 new tests, clean under lint-prod + lint-tests + file-size policy.
Workspace-wide `cargo check` confirmed no regressions from the new dep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the durable rusqlite-backed job/candidate/attempt ledger in favor
of a restart-from-zero model: an immutable manifest, an append-only
JSONL failure log, in-memory counters, and an atomically finalized
run summary. A run either completes (a valid final summary exists) or
it didn't, and is retried from a fresh VSS snapshot — no mid-job
transactions, leases, or crash reconciliation to get right. Docenta's
own content-hash deduplication makes re-streaming after a crash a
no-op on the consumer side, so restarting from zero costs nothing.

rusqlite is no longer a dependency anywhere in the workspace.
Implements the real Coordinator workflow (job intake, candidate
enumeration, manifest construction, protocol framing) against swappable
CandidateSource/ContentSource backends, per the implementation plan's
§9.5 "fast" test strategy. DirWalkCandidateSource/FsContentSource are
real, correct, std::fs-backed stand-ins for the VSS-snapshot and
privileged-Reader-backed implementations that land in UFI.1/UFI.2.

Adds the end-to-end dir-walk parity harness: a deterministic fixture
tree (size classes, non-BMP filename, hard links), an independent
plain_walk oracle sharing no code with the pipeline, a minimal
protocol-decoding test consumer, and a parity test that decodes real
wire bytes and recomputes content digests independently rather than
trusting the producer's self-reported ones.
Adds SnapshotLeaseManager, the Broker's in-memory VSS-lease bookkeeping
behind a swappable VssProvider trait, per the implementation plan §4.2.
Deliberately cross-platform (unlike the rest of this Windows-only
crate) so the lease lifecycle — create/renew/release/expire/reconcile —
is unit-tested against a fake provider on every host, not just Windows.

Lease state is purely in-memory: there's no durable table surviving a
Broker restart, so reconcile_at_startup can unconditionally delete
every VSS snapshot the real backend still reports at startup — a
freshly constructed manager holds no lease for anything, so all of
them are definitionally orphaned.

Adds the Win32_Storage_Vss windows-rs feature for the real VSS
requestor COM implementation landing next.
Adds uffs-vss-requestor, a tiny per-run Windows helper the Broker will
spawn once per volume scan, per
docs/dev/architecture/uffs-vss-rust-cpp-shim-implementation-guide.md.

windows-rs does not generate bindings for IVssBackupComponents (the VSS
requestor interface) — verified directly against the crate's own
generated source, not assumed. Rather than hand-roll the COM vtable
from memory (unverifiable, high risk of silent memory corruption), the
actual requestor sequence lives in a narrow native C++ shim compiled
against the official Windows SDK headers (already present in the
cargo-xwin SDK cache) via build.rs + the cc crate. Everything else —
the private control-pipe protocol, process lifecycle, parent-death
watchdog — is ordinary Rust.

Uses VSS_CTX_FILE_SHARE_BACKUP: ephemeral, auto-release, no writer
participation. The session (and therefore the snapshot) lives exactly
as long as this helper process does; on crash, releasing the last
IVssBackupComponents reference is what deletes it — no orphan
reconciliation or persistent snapshot tracking needed, matching the
job model's "rerun from a fresh snapshot" recovery story.

Compile- and link-verified end to end through cargo xwin build
(clang-cl against the real vsbackup.h/vswriter.h/vss.h headers, linked
against vssapi.lib/ole32.lib) and cargo xwin clippy at the same
strictness as this workspace's lint-prod/lint-tests/lint-ci gates —
not just a syntax probe, a real produced PE executable.

Not yet wired into uffs-broker (spawning, Job Object assignment, pipe
creation) — that's the next slice.
Adds the Coordinator-facing Snapshot Manager pipe server
(broker/snapshot_manager/mod.rs): a separate named pipe from the
daemon's MFT-handle channel, verifying the connected client is
uffs-content before dispatching CreateSnapshotLease/
DuplicateSnapshotHandle/RenewSnapshotLease/ReleaseSnapshotLease/
QuerySnapshotLease requests to the lease manager, and handling
DuplicateSnapshotHandle's separate reader-identity check +
DuplicateHandle call for the approved uffs-content-reader process.
Spawned from serve_pipe_requests so it runs under both --run and the
SCM-dispatched service path.

Adds WindowsVssProvider (broker/snapshot_manager/vss_helper.rs): the
real VssProvider backend, spawning one uffs-vss-requestor helper
process per snapshot, suspended, assigned to a
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE Job Object before it ever runs, then
resumed — so a dead Broker's handle-table teardown kills every live
helper automatically. Drives each helper over a private JSON-lines
control pipe (a deliberate, documented duplicate of
uffs-vss-requestor::protocol, since that crate has no library target
and the protocol is tiny/internal-only). list_existing_snapshots is
now a no-op: VSS_CTX_FILE_SHARE_BACKUP is auto-release, so there is
nothing to reconcile at startup — the OS itself cleans up a crashed
Broker's helpers.

Compile- and link-verified end to end via cargo xwin build/clippy at
lint-prod/lint-tests/lint-ci strictness, producing a real linked
uffs-broker.exe.
Adds an #[ignore]d integration test exercising the whole pipeline built
so far (native shim -> uffs-vss-requestor helper -> Broker lease
manager -> Job Object cleanup) at runtime for the first time, rather
than only compile/link-verified. Fixes two real compile errors this
uncovered on the actual Windows target: file_identity()'s unstable
file_index() had no feature-gate, and handle_connection tripped
clippy's cognitive-complexity limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts the VSS create/read/delete round-trip logic (previously only
in an #[ignore]d test) into a production self_test_round_trip function
shared by `uffs-broker --self-test-vss <dir>` and the test itself, so
the two paths can never drift apart. Adds
scripts/windows/vss-snapshot-validation.rs, a thin rust-script wrapper
matching the existing scripts/windows/*.rs convention, so the whole VSS
pipeline (native shim -> uffs-vss-requestor -> Broker lease manager)
can be smoke-tested standalone on a real Windows box without a running
daemon or Coordinator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pre-existing links using unqualified item names that aren't in scope
from their defining module: SnapshotManagerResponse and ReadMode are
defined in the parent module (need super::), and digest32 was a stale
name for the digest function. Caught by the pre-push rustdoc gate,
unrelated to this session's VSS/broker work but blocking the push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…estor version

Root cause of the reported hang: build_command_line wrapped
volume_path in a bare quoted string ("C:\"), but a single trailing
backslash immediately before a closing quote escapes the quote instead
of terminating the argument under Windows command-line parsing rules.
Every argument after volume_path (including --parent-pid) was silently
swallowed into it, so uffs-vss-requestor failed to parse its own args
and exited before ever connecting to the control pipe -- leaving the
Broker blocked forever in ConnectNamedPipe waiting for a connection
that would never come, with the process already gone by the time
anyone checked tasklist. Added quote_windows_arg implementing the real
escaping rule (double any backslash run before an embedded/closing
quote) and applied it to every string argument in the command line,
with unit tests covering the trailing-backslash case plus the other
standard pitfalls.

Also wires uffs-vss-requestor's build.rs to call
uffs_version::emit_build_env(), matching every other UFFS binary --
its --version output was silently missing the git-sha/commit-date
fingerprint entirely, making exactly this kind of stale-vs-fresh-binary
mismatch invisible. The validation script now prints `--version -v`
for both uffs-broker and uffs-vss-requestor up front, and defaults to
target\release\ over the installed ~\bin\ copy so it exercises the
just-built dev binary for this not-yet-released flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every stage of --self-test-vss was previously silent between "Running:"
and the final PASS/FAIL line, so a hang (like the arg-quoting bug just
fixed) gave zero indication of where it was stuck. Adds tracing::info!
checkpoints at every step: pipe creation, helper spawn (with pid),
waiting for the helper to connect, waiting for its Ready/Failed event,
snapshot creation, marker verification, and deletion/release.

Split create_snapshot into wait_for_helper_ready + finish_create_snapshot
to keep its cognitive complexity under the lint ceiling with the new
logging in place, and split the self-test round-trip logic out of
vss_helper.rs into a new sibling file (vss_self_test.rs) since the
added instrumentation pushed vss_helper.rs over the workspace's 800-LOC
file-size policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Command::output() waits for the child to exit before returning
anything, which silently swallowed every tracing::info! progress line
the Broker's self-test now emits -- exactly the visibility the logging
was added for. Switched to Stdio::inherit() + .status() so the
Broker's stdout/stderr stream straight to the terminal in real time,
letting a hang show which step it's actually stuck on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wrapper had no timeout at all -- a hung helper connection blocked
it indefinitely with no way to tell it apart from "still working".
Switched to spawn() + a poll loop (default 120s, --timeout-secs
override) that kills the child and reports a clear timeout instead of
hanging forever; the last "vss: ..."/"self-test: ..." line streamed to
the terminal before the kill now pinpoints exactly which step it was
stuck on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folds the manual "open a second terminal and run tasklist" check we
kept doing by hand into the script itself: a background thread polls
for uffs-vss-requestor.exe every second and logs RUNNING/NOT RUNNING
whenever its liveness changes, so a hang shows immediately whether the
helper is still alive or already gone -- no more asking the operator to
check by hand. Also drops the default timeout from 120s to 30s per
observed round-trip timings (snapshot creation and deletion both take
low single-digit seconds in practice), so a genuine hang is reported
much sooner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… too

Real hardware testing (via the new self-test-vss watchdog) proved
create/read fully work end to end, but every Release command hung
indefinitely with uffs-vss-requestor.exe confirmed still alive the
whole time -- IVssBackupComponents::DeleteSnapshots was the one call
that never returned. VSS_CTX_FILE_SHARE_BACKUP is documented as an
auto-release context; the intended teardown is releasing the last
IVssBackupComponents reference, which this shim already did correctly
on every other exit path (Cancel/PipeClosed/ParentDied) and which the
successful create path already proved COM itself is responsive for.
Removed uffs_vss_delete_snapshot_set (native + FFI + Rust wrapper)
entirely rather than leave a call known to hang, and Release now uses
the same drop-based teardown as the other three paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The DeleteSnapshots removal didn't fix the hang -- it persisted
identically with a plain IVssBackupComponents::Release() call, and
restarting the machine's stuck VSS writers (System/MSSearch/WMI, found
via vssadmin list writers) didn't clear it either. The Broker's own
tracing has zero visibility past "waiting for Released confirmation";
we don't know if the helper is stuck in Rust dispatch, the mpsc
channel, or inside the native Release()/CoUninitialize() call itself.

Adds a best-effort append-only log at
%TEMP%\uffs-vss-requestor-debug.log bracketing every step: pipe
connect, snapshot creation, entering the main loop, each received
event, and explicitly before/after drop(session) and the Released
write. If "session dropped" never appears, that pins the hang inside
destroy_session's native COM calls; if it does appear but "wrote
Released event" doesn't, the hang is in the pipe write instead.
Diagnostic-only -- not a permanent feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vent

The debug log proved drop(session) (native VSS release) completes in
~11ms -- the hang is entirely in the final pipe write, not VSS/COM.
All writers already confirmed Stable (post-reboot) with the hang still
identical, ruling that out too. Splits write_event's single "writing
Released event" checkpoint into three: about to writeln!, writeln!
returned, and flush returned, so the log pinpoints whether the raw
WriteFile call itself never returns or whether it's the flush.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… just Release)

Root cause found via the debug log: it's neither VSS/COM (drop(session)
took ~11ms), AV (disabled, hang identical), nor stuck VSS writers
(rebooted, all Stable, hang identical). It's Windows serializing
synchronous I/O across duplicate handles of one file object from
different threads: the reader thread's next blocking read (waiting for
a message that will never arrive) sat pending on a clone of the same
non-overlapped pipe handle the main thread tried to write the final
reply on, deadlocking that write forever. The prior commit's fix only
covered Release/Cancel; Ping needed the identical fix, since it's meant
to support repeated liveness checks over a lease's lifetime and would
have hit the exact same deadlock on its first Pong reply once a real
Broker used it.

Ping/Pong is now handled entirely inside the reader thread (no session
access needed, no cross-thread handle interleaving at all); only
Release/Cancel are forwarded to the main thread, which stops the
reader immediately after so its read is never concurrently pending
with the main thread's teardown-and-final-write.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ping had no caller anywhere -- the deadlock fix for it was reviewed but
never actually exercised end to end. Adds WindowsVssProvider::ping_lease,
sending a real BrokerCommand::Ping to the live helper session and
waiting for HelperEvent::Pong (not part of the VssProvider trait --
FakeVssProvider has no use for it yet, so it stays a
WindowsVssProvider-only capability until a real feature needs it).
--self-test-vss now calls it between marker verification and deletion,
so the create/ping/delete round trip is proven for real, not just
reviewed. A ping_lease failure surfaces as an overall self-test failure
without skipping the cleanup delete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A real deployment spawns one helper per content-scan job; unconditional
logging would grow an unbounded file over millions of runs. debug_log
is now opt-in via UFFS_VSS_DEBUG_LOG (unset = production-safe no-op)
with a 10MB truncate-before-append cap as a safety net for an extended
troubleshooting session. The validation script sets the env var
automatically for the Broker it spawns, since that script only ever
runs for diagnostics -- no manual step needed to get the log back.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
githubrobbi and others added 20 commits July 18, 2026 19:00
…nnel

Real-hardware full-system content-read scans showed ~40% of candidates on
one drive failing OpenFileById with ERROR_INVALID_PARAMETER, all sharing
file_reference == 0. Bisected against a plain ad-hoc CLI search for one of
the affected files (small result set, delivered inline) which reported the
correct nonzero file_reference — proving the MFT parse and compact-index
build were never at fault.

Root cause: uffs-client's shmem transport (used once a search's result set
crosses SHMEM_THRESHOLD, which any full multi-drive scan does) never carried
file_reference in its compact ShmemRecord — the reader hardcoded 0 under the
assumption that no shmem-path consumer needed it. That assumption predates
uffs-content, which now needs file_reference on every candidate to
OpenFileById against a VSS snapshot. 0 is never a valid NTFS file reference
(it's the reserved $MFT record), so the reader correctly rejected every one
of these reads.

Add file_reference to ShmemRecord (88 -> 96 bytes, format version 3 -> 4),
populate it on write and consume it on read instead of hardcoding 0, and
change the round-trip test's fixture to a nonzero value so a regression
here is actually caught.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Step 1 of validating the "reads candidates in ascending FRS order ==
near-sequential physical disk access" assumption for uffs-content's
read scheduling, before investing in an FRS-sort read-order change.

Takes uffs --format json output (path + file_reference), samples a
subset, and cross-references each file's on-disk starting LCN via
`fsutil file queryextents` to report the Spearman rank correlation
between FRS order and physical order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same diagnostic as the previous check_frs_vs_lcn.ps1 (sample uffs --format
json rows, cross-reference each file's on-disk LCN via fsutil file
queryextents, report the Spearman correlation between FRS order and
physical order) -- rewritten in Rust via rust-script per project
preference over PowerShell tooling. Also fixes a bug in the original: the
CSV output path construction failed when json_path was a bare filename
with no directory component (Join-Path on an empty parent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends check_frs_vs_lcn.rs beyond a single correlation coefficient: now
also reports the total seek distance (sum of |delta LCN| touring the
sample once) under three orderings -- the search response's natural
order (what the read pipeline processes today), ascending-FRS order,
and the oracle (true ascending-LCN order, the unbeatable lower bound).

This turns "FRS correlates weakly/moderately with LCN" into a concrete,
actionable number: what fraction of the achievable seek-distance
reduction does cheap FRS-sorting actually capture, versus how much is
only reachable by a real LCN-resolution pass. Also converts to MiB of
head travel via `fsutil fsinfo ntfsinfo` when available.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a 3rd/4th arg (mode + optional offset): "random" (default, unchanged
-- true uniform draw across every row) or "block" (sample_size
*consecutive* rows in search-response order, from a given or random
offset). Block mode tests a different, more operationally relevant
question than global random sampling: are candidates as the read
pipeline's bounded sliding window would actually encounter them together
physically clustered on disk, versus whether FRS order tracks LCN order
across the whole result set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
query_bytes_per_cluster's digit extraction collected every digit
character anywhere on the matching fsutil line, not just the value --
fragile to whatever exact formatting fsutil uses. Real-hardware run
produced an implied ~40972 bytes/cluster (not a valid NTFS cluster size,
which must always be a power of two between 512 B and 2 MiB), which
silently inflated the printed MiB-of-head-travel numbers ~10x.

Rewrite to only parse what follows the last ':' on the matching line
(handling 0x-hex values too), and add a hard sanity check: reject any
parsed value that isn't a power-of-two NTFS cluster size instead of
silently reporting a nonsense conversion. Falls back to "unknown" if the
format still doesn't match on a given Windows version, same as before.

The bug only affected the MiB display -- the underlying cluster counts,
Spearman correlation, and seek-distance-reduction percentage were always
correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware benchmarking (check_frs_vs_lcn.rs) found ascending-FRS read
order only weakly-to-moderately correlates with actual on-disk physical
layout (Spearman 0.56-0.68 across independent random samples) and
captures only ~28-29% of the achievable seek-distance reduction on a
volume reorganized over years. The rest requires resolving true LCN.

Adds an opt-in daemon-side post-search step instead of teaching
uffs-content-reader to parse the MFT itself: uffsd already performs a
full MFT parse to build its search index (an already-open,
already-broker-authorized volume handle), so grafting one more
targeted-record-read pass onto that is far cheaper than giving the
intentionally narrow, non-elevated uffs-content-reader a whole new
MFT-parsing capability it would otherwise never need, and avoids
re-deriving everything from scratch in a second process.

- uffs-mft: new `lcn_resolve` module. `resolve_frs_to_lcn` (Windows-only)
  opens the MFT's own extents and does targeted per-FRS record reads
  (ascending order, so the reads stay close to sequential within $MFT
  itself) to find each file's first non-sparse $DATA run's LCN. The
  underlying `first_data_lcn` byte-parser is pure and cross-platform,
  kept separately testable without Windows.
- uffs-client: new `SearchParams::resolve_lcn_order` flag (opt-in, off
  by default -- interactive searches never set it).
- uffs-daemon: new `IndexManager::device_paths` map (tracks which
  drives were loaded from a VSS snapshot device vs. a live volume, since
  VolumeHandle::get_mft_extents needs the same one reopened) and a new
  `physical_order` module: a plain post-processing pass over the
  already-built row list, run only when the flag is set, so every other
  search request's code path is completely unaffected. Grouped per
  drive (LCN only means something within one volume) and never fails
  the search -- a drive whose volume can't be reopened just keeps its
  rows in original order with a warning logged.
- uffs-content: sets the flag when building its per-drive enumeration
  SearchParams.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SearchParams::resolve_lcn_order was only ever set by uffs-content's own
job code (bypassing the CLI-args constructor entirely), so there was no
way to exercise the daemon's physical-location-ordering feature through
the ordinary uffs.exe CLI -- regenerating a JSON dump with the plain
CLI always showed unsorted (natural) order regardless of the daemon-side
feature, which was confusing during manual verification.

Adds --resolve-lcn-order as an explicit, diagnostic-only CLI flag (off
by default, same as before) so the read-order optimization can be
exercised and measured directly (e.g. via
scripts/windows/check_frs_vs_lcn.rs) without running a full
uffs-content job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A real-hardware full-drive-set job showed drives were processed strictly
one at a time in read_and_emit_all_candidates: a slow HDD-backed lease
held up every other drive's candidates, including a fast SSD-backed
lease sitting fully idle in queue, even though the two share no
connection pool, no volume handle, and no physical device.

Each lease run now gets its own thread, coordinated through a
Mutex<EmitState> so two drives' candidates can never interleave their
frames on the wire -- the protocol only requires per-candidate
contiguity (FrameOrdering::None), which this preserves exactly.

Also splits the now-826-line workflow.rs into workflow.rs + a new
workflow/emit.rs sibling module (mirroring the file's existing
workflow/pipeline.rs split), keeping both files under the workspace's
800-line file-size policy without trimming any documentation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware runs kept showing single-digit MiB/s content-read
throughput on some drives even after LCN-sorted ordering, with no way
to tell whether that's the drive's actual physical ceiling or still a
software problem.

measure_raw_throughput.rs streams raw sequential reads straight off a
volume/VSS device (dd-style, bypassing the whole read pipeline) at
three zones (outer/middle/inner) to measure the real floor to compare
pipeline throughput against.

check_frs_vs_lcn.rs now also reports fragmentation: how many sampled
files span more than one on-disk extent, and the intra-file seek
distance that costs even under perfect oracle LCN ordering, since
ordering candidates by first-extent LCN says nothing about a file
that's itself scattered across the disk.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ileSizeEx

Real-hardware run against E:/M: failed immediately with
ERROR_INVALID_PARAMETER: GetFileSizeEx does not work on raw
volume/device handles, only regular file handles. Volume size now
comes from FSCTL_GET_NTFS_VOLUME_DATA's TotalClusters * BytesPerCluster,
the same ioctl uffs-mft's own VolumeHandle::get_ntfs_volume_data already
uses against these exact handles for this exact reason.

Verified compiling clean against the Windows target (cargo xwin check
+ clippy); still no live-hardware run available from this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware run against C:/D: showed the outer-edge zone (2% offset)
failing with ERROR_INVALID_PARAMETER while middle/inner zones (0.5/0.9
fractions) happened to succeed: volume-handle I/O requires a
sector-aligned offset even for buffered reads, and an arbitrary
fraction of the volume size essentially never lands on one by chance.

Rounds every zone's offset down to a 1 MiB boundary before seeking, and
changes read_zone to only ever issue full chunk_bytes-sized reads
(dropping a less-than-a-full-chunk remainder) so a ragged final read
can't hit the same alignment failure from the length side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware runs against E:/M: produced an absurd "72 quadrillion
MiB" intra-file seek distance -- traced to fsutil file queryextents
reporting sparse-file holes with Lcn = 0xFFFFFFFFFFFFFFFF (the standard
"no on-disk allocation" sentinel, not a real physical location). Any
file with such a hole swamped the whole sample's total since u64::MAX
dwarfs every real LCN by many orders of magnitude.

query_all_extents now drops extents at that sentinel entirely (they
carry no real seek cost, since there's nothing to seek to), and
intra_file_seek_distance uses saturating arithmetic as defense in
depth against any other unexpected value. Verified with a standalone
rustc test simulating a real-extent/hole/real-extent sequence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware benchmarking against E:/M: showed pipeline throughput
far below their own raw sequential floor even with zero fragmentation,
pointing at per-file open/close overhead as the likely cause -- but
that was still a theory, not a measurement. read_logical now times
each phase of a cache-miss (open_volume_hint, open_file_by_id,
file_size) plus the read itself, and logs one structured debug-level
line per call for later aggregation.

That data was previously unreachable: uffs-content-reader's own
tracing subscriber writes to stderr with no level cap, but its stderr
was piped to Stdio::null() by the Coordinator, discarding every event
including this new timing. reader_client.rs now redirects it to a
discoverable temp log file instead (mirroring ephemeral_daemon's
--log-file for uffsd), and logs that path in the Coordinator's own log.

Diagnostic instrumentation only -- no behavior change to the read path
itself. Full gate (host + xwin check/clippy plain and pedantic/nursery,
fmt, file-size, workspace tests, rustdoc, doctests, xwin test --no-run)
passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
open_volume_hint's CreateFileW was being opened and closed fresh for
every single candidate, even though it only identifies the volume
(never the file) and every candidate on one connection is read from
the same volume -- one physical connection is only ever drawn from one
lease's pool, so device_path never actually changes mid-connection.

ReadHandleCache now caches this handle the same way it already caches
the per-file handle, reopening only if device_path ever changed (a
belt-and-suspenders check, not something that happens in practice).
Removes one whole CreateFileW+CloseHandle cycle per candidate at no
correctness cost -- real-hardware benchmarking against small-file-heavy
drives found this handle was pure per-file waste today.

Full gate (host + xwin check/clippy plain and pedantic/nursery, xwin
doc for the cfg(windows)-gated intra-doc links, fmt, file-size,
workspace tests, doctests, xwin test --no-run) passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware benchmarking against small-file-heavy drives found
GetFileSizeEx a real fraction of per-candidate time even after the
volume-hint and file-handle caching already in place. The manifest
size the real VSS Coordinator already knows for each candidate (read
from the exact same frozen snapshot the Reader opens the file
against) makes that re-query redundant for the one real production
caller.

ReadRequest gains known_logical_size: Option<u64> -- opt-in per
request, never a blanket trust-model change to read_logical itself.
Only VssCandidateSource/ContentReader::begin_read populates it (via
CandidateEntry::logical_size); any other/future caller that leaves it
None gets the original always-re-verify GetFileSizeEx behavior with no
code changes needed on its part.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RUSTDOCFLAGS="-Dwarnings" cargo xwin doc --workspace --document-private-items
had apparently never been run against this workspace before -- every one
of these was a real, pre-existing latent bug across uffs-mft,
uffs-security, uffs-broker, uffs-client, uffs-daemon, uffs-cli, and
uffs-content, invisible on the host (macOS) doc build because the
affected code is #[cfg(windows)]-gated.

Three distinct root causes, fixed accordingly:

- Bare-name intra-doc links inside a MODULE-level (//!) doc comment
  resolve relative to something other than that module's own scope in
  this rustdoc version -- even for items defined in the very same file.
  Fixed by fully-qualifying via the item's real crate-rooted path
  (crate::module::Item), matching how other already-working links in
  the same files were already written.
- Links to items that are genuinely private (module-private or
  pub(crate)) from public-facing doc comments -- de-linked to plain
  backtick text, since the private item can't be a real clickable
  destination in a normal (non---document-private-items) doc build.
- A handful of genuinely stale references: stream::run (renamed to
  stream::spawn), MftReader::new_for_volume (renamed to
  MftReader::open), and DirWalkCandidateSource (referenced via a bare
  name never imported into that file's scope) -- fixed to the current
  real names/paths, not just silenced.

cargo xwin doc --workspace --all-features --no-deps --document-private-items
now passes clean end to end with -D warnings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-hardware run confirmed the running binary had the per-phase
timing instrumentation built in (version=... c596e5c-dirty) and
processed 118,847 candidates, yet the reader's own log file had zero
"read_logical: per-phase timing" lines -- only INFO-level connection
events. tracing_subscriber::fmt()'s default level caps below debug!
when no max level is explicitly configured, silently dropping every
one of the new timing events regardless of how the code was built.

with_max_level(DEBUG) makes the instrumentation this crate already
ships actually reach its own per-job temp log file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run_job's enumeration loop processed request.roots strictly one at a
time -- each root's whole search-and-collect cycle blocking the next
root from even starting, even though each enumerate() call opens its
own independent connection to the daemon and shares no mutable state
with any other call.

Real-hardware benchmarking on a two-drive job showed ~15s + ~13s back
to back (~28s total) for enumeration alone; running both concurrently
cuts that to ~max(15s, 13s), and the effect compounds with every
additional root a job touches.

CandidateSource now requires Sync (mirroring ContentSource's own bound,
already proven safe for exactly this kind of cross-thread sharing), and
enumerate_all_roots_concurrently spawns one thread per root via
std::thread::scope, joining and concatenating results back in roots'
own order -- same shape as the earlier concurrent-lease-runs fix for
content reads.

Verified with a dedicated test (SlowEnumerateCandidateSource, 4 roots
at 100ms each) proving wall-clock time reflects the slowest single
root, not the sum -- 8/8 consecutive runs at ~140ms, comfortably under
the ~300ms sequential-detection threshold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents the full filter surface (SearchFilters/SearchFilterParams,
CLI flags, the narrower content-export JobRequest subset), the two
independent extension/type-group taxonomies and how they combine, and
the lack of magic-byte classification — the concrete questions raised
while scoping Docenta's consumer-side extension handling.
@githubrobbi
githubrobbi enabled auto-merge July 19, 2026 18:36
@githubrobbi
githubrobbi added this pull request to the merge queue Jul 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 19, 2026
…image

is_uffs_content_image's `starts_with("uffs-content")` fallback also
matched uffs-content-reader.exe, so verify_coordinator_identity would
wrongly accept the Reader process as a legitimate Coordinator. Drop
the starts_with fallback on both this and is_uffs_content_reader_image
(same risk class) in favor of exact-name matching; the two literal
forms already cover every real binary/exe pair.

Caught by the merge-queue run of PR #563 failing
recognizes_coordinator_image_names on Windows.
@githubrobbi
githubrobbi enabled auto-merge July 20, 2026 00:17
@githubrobbi
githubrobbi added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit fad111d Jul 20, 2026
29 checks passed
@githubrobbi
githubrobbi deleted the feat/uffs-content-scaffold branch July 20, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant