Commit adc81fa
fix(core): stream the resumable-upload resume instead of buffering the whole file (#279)
## Incident (2026-08-14)
Quitting Driven mid-upload of an 88.6 GB file (`dev-drives/dev.vhdx`)
and relaunching made the app consume >10 GB of RAM in ~30 s (at disk
read speed) until Windows killed it - and it repeated on every relaunch.
Diagnostics zip + the laptop's `pending_ops` row confirmed the
mechanism:
- The startup **reconcile** (runs before any scan, so the UI still shows
the indeterminate state) found the persisted resumable session
(`size=88655003648`, `acked_offset=7436500992`), passed the
resume-identity gate, and called `read_hash_encrypt` - which **buffers
the entire upload body into one `Vec`**. The live upload path has
streamed with bounded channels since DESIGN s11.4.3; only the
restart-resume path (and the adopt re-hash) still used the legacy
buffered read.
- The OOM kill happens before `delete_pending_op`, so the op survives
and every launch retries the same 88 GB read.
- The whole fatal phase logged **nothing** at INFO - the diagnostics
bundle was blind to it.
## Fix
- **`resume_persisted` now streams** via a pass-based design: one
sequential read that hashes every byte, discards the already-acked
prefix, and pushes the tail in bounded wire chunks (~2 in flight,
`ResumeAcc` RAII-guarded MemGauge accounting that stays balanced even on
`?` error unwinds). The pacer is charged only for pushed wire bytes.
- **Backend offset semantics** are interpreted against the streaming
window (`offset` always equals the file position of the accumulator's
first byte): driven-s3's one-shot post-hydration rewind-to-0 and
sftp/localfs resyncs RESTART the pass from the store's stated offset
(one-restart budget); in-window forward offsets drain exactly the newly
covered bytes; an exact stall abandons. Without this, every S3 resume
with prior progress degraded to a full re-upload.
- **Legacy rows** (recorded hash, no identity) are verified by a
hash-only prepass before any byte is pushed; identity-carrying rows get
a cheap EOF re-fstat that aborts a mid-stream-modified file before the
finalizing chunk (md5-vs-store remains the correctness backstop).
- **`rehash_local_plaintext` streams** (hash-only): its old callee's
plaintext arm buffered the whole body despite the comment claiming
otherwise.
- **Excluded paths**: reconcile skips only the *resume* for a path the
user has since excluded and lets adopt-or-requeue run - a finalized
object is adopted into a `file_state` row (never left as an invisible
untracked orphan on the remote), an unfinalized one requeues into
nothing (the next scan excludes the path; the abandoned session is
GC'd).
- **`push_chunks`** (fresh-session uploads) gets the same no-progress
stall guard the resume path has.
## Instrumentation (make the next diagnostics zip self-diagnosing)
- INFO breadcrumbs: cycle start with tick source, orchestrator state
transitions (variant *name* only - never the Error variant's free-text
details, which could leak partially-redacted paths), reconcile entry
with pending-op count, resume start/abandon/restart with sizes + offsets
+ reasons.
- RSS watchdog task (`driven::app::memlog`): samples every 15 s on the
blocking pool, logs on >=128 MiB movement, keeps a trailing ~15 min
window.
- Diagnostic bundle: new `pending_ops.txt` (op shapes, sizes, offsets,
ages, recovery-flag presence; session URLs are capability secrets and
are never included) and `memory.txt` (current + peak RSS, a
`sample=fresh|stale-or-unread` honesty flag, and the trailing sample
window). Redaction policy text updated in the same change.
## Review
An ultra multi-agent review ran against the first cut; its confirmed
findings (S3 rewind misread, MemGauge leak on error unwind, excluded-op
orphan gap, push-before-validate on legacy rows, state-transition Debug
leak, plus several consistency nits) are fixed in the follow-up commit.
Refuted findings (e.g. mid-stream prefix modification "undetected" - the
full-stream md5 vs the store's stored md5 catches exactly that) are
documented in code comments.
## Tests
- `crash_mid_upload_resumes_persisted_session_byte_for_byte` asserts
MemGauge `peak > 0` (proves the resume routes through the instrumented
streaming loop) and `peak <= 3 * WIRE_CHUNK` (boundedness).
- New: `resume_honours_a_one_shot_rewind_to_zero` (S3 contract; red on
the stall-guard misread),
`resume_error_midstream_keeps_the_mem_gauge_balanced` (red without the
RAII guard), `reconcile_skips_resuming_a_now_excluded_paths_session` (+
control proving the gate),
`reconcile_adopts_finalized_orphan_even_when_path_now_excluded` (red on
a drop-the-op design),
`pending_ops_summary_names_resumable_sessions_without_urls`, memlog
sampler tests.
- Full workspace suite green locally (macOS): driven-core 547 lib + 23
e2e + integration suites, driven-app 437.
README checked - no changes needed (it does not enumerate bundle
contents, and no feature claims changed).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01A7q3CvJzL4zZmDA9CbXyQQ
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 4175050 commit adc81fa
8 files changed
Lines changed: 1945 additions & 108 deletions
File tree
- crates/driven-core
- src
- tests
- src-tauri
- src
- commands
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
102 | 120 | | |
103 | 121 | | |
104 | 122 | | |
| |||
932 | 950 | | |
933 | 951 | | |
934 | 952 | | |
935 | | - | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
936 | 975 | | |
937 | 976 | | |
938 | 977 | | |
| |||
2340 | 2379 | | |
2341 | 2380 | | |
2342 | 2381 | | |
| 2382 | + | |
| 2383 | + | |
| 2384 | + | |
| 2385 | + | |
| 2386 | + | |
| 2387 | + | |
2343 | 2388 | | |
2344 | 2389 | | |
2345 | 2390 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
947 | 947 | | |
948 | 948 | | |
949 | 949 | | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
950 | 961 | | |
951 | 962 | | |
952 | 963 | | |
| |||
957 | 968 | | |
958 | 969 | | |
959 | 970 | | |
960 | | - | |
| 971 | + | |
| 972 | + | |
961 | 973 | | |
962 | 974 | | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
963 | 986 | | |
964 | 987 | | |
965 | 988 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
193 | 193 | | |
194 | 194 | | |
195 | 195 | | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
196 | 201 | | |
197 | 202 | | |
198 | 203 | | |
| |||
0 commit comments