Repository navigation
search: the portable search service (LanceDB on R2, S3 or a disk) - #777
Conversation
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 956d111df1afe379400e9bf20cc9ae7bc07d5097, reviewed against #772 at 71951b4568ecd8e7a076c61d113190337550f5ee.
The separate service/workspace is appropriate for this layer. I found three failures in its storage and retry contracts.
Blocking:
-
Deleted bytes are not reliably purged (
search/src/store.rs:467–507).compact()usesOptimizeAction::All, which in pinned lancedb 0.40.0 uses default Lance 13.0.0 compaction options: deletions must exceed 10% before a fragment is rewritten on its own. For example, deleting one source from a single compacted file holding 20 equal-sized sources leaves its bytes in the current file; pruning old versions cannot remove that file. Separately, a deletion between the maintenance task's compaction and prune is ignored bymaintaining, with no subsequent compaction scheduled. Restarting before the task completes loses it entirely; opening a table schedules no recovery, and maintenance errors only log. These are ordinary deletes/restarts that break the stated physical-erasure guarantee. Force materialization of every deletion and make cleanup recoverable, retried, and aware of writes arriving during a pass. Cover low-fraction deletes, a delete between phases, restart, and transient failure with checks of the stored bytes, not just search results. -
Empty upserts lose their replay/CAS revision (
search/src/store.rs:268–284, 531–534). Write sourceaatr1, then upsert{source:"a", revision:"r2", replaces:"r1", sections:[]}. The first call succeeds, but deletes the only rows holding the revision. If its response is lost, the identical retry seescurrent: Noneand returns 409 instead of the promised no-op success. A subsequent update replacingr2also conflicts. Preserve the source's revision independently of its chunks (including zero-chunk sources), atomically with replacement, and test empty-write replay/CAS across restart and an initially empty source. -
Interrupted first-write initialization leaves an unrecoverable lexical table (
search/src/store.rs:327–329). The data table is committed before its FTS indexes. A restart or storage error before the first index commits leaves a valid table with the source revision but no FTS index. On retry,entry()opens it and the same-revision path returns success without completing initialization. Pinned Lance'scombined_fieldsplanner explicitly refuses a table with no inverted index on any requested column, so lexical/hybrid searches now return 500; the other write/maintenance paths do not create missing FTS indexes. Make initialization resumable before acknowledging a replay or serving the table, and cover restart/failure between table creation and index creation.
Validation: source traces include the pinned Lance compaction planner and CombinedFields planner. cargo test --manifest-path search/Cargo.toml --locked could not reach tests because this machine lacks protoc (lance-encoding build failure). CI check passed; search check and the e2e shards were still pending. The externally referenced engineering-style file was unavailable on this machine.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 108d4eea652dd8cad52f1bdf70f0a7d86ee918d0, against #772 at 71951b4568ecd8e7a076c61d113190337550f5ee.
None of the three blockers in my previous review is fixed. I checked the new file-based configuration/timing changes and rechecked the affected storage paths; search/src/store.rs and its pinned dependencies are unchanged.
Blocking:
- Physical erasure remains unreliable (
search/src/store.rs:467–507). Default compaction leaves low-fraction deletions in the current data file, which pruning cannot remove. Deletes arriving between compaction and prune receive no subsequent pass; restart loses scheduled cleanup, and failures only log. Force deletion materialization and provide recoverable, retried cleanup that accounts for intervening writes. Test stored bytes after low-fraction deletes, intervening deletes, restart and transient failure. - Empty upserts still lose revision/replay state (
search/src/store.rs:268–284, 531–534). Afterr1, an empty upsert ofr2replacingr1succeeds but deletes its revision. Repeating that request after a lost response returns 409, as does a later write replacingr2. Preserve revision state atomically for zero-chunk sources; cover replay and CAS across restart, including an initially empty source. - First-write initialization is still not resumable (
search/src/store.rs:327–329). Failure or restart between table creation and its first FTS index leaves a table whose same-revision retry succeeds without creating indexes. Lance's CombinedFields planner then rejects lexical/hybrid searches. Resume index initialization when reopening/retrying, and test interruption between these commits.
The separate service workspace and small configuration helper remain appropriate in complexity for this layer.
Non-blocking: search/README.md promises _FILE for every setting, but SEARCH_LISTEN still uses std::env::var (search/src/main.rs:44), silently ignoring SEARCH_LISTEN_FILE. Route it through env::value or narrow the documented support.
Validation: rechecked the pinned Lance compaction defaults and CombinedFields planner against this head. Local service tests were not rerun: protoc remains unavailable (the earlier build stopped there). Current CI search/check/e2e jobs are pending. The external engineering-style file is also unavailable.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 2fdfc1ae9377a4456c63c417d4924dc54a8c5955, against #772 at 71951b4568ecd8e7a076c61d113190337550f5ee.
None of the three blockers in my previous review is fixed. This head only adds a directory cleanup guard to the tests; the storage implementation and dependencies are unchanged. That small cleanup is appropriate in scope and complexity.
Blocking:
- Physical erasure remains unreliable (
search/src/store.rs:467–507). Default compaction can retain low-fraction deletions in the current data file (Lance's threshold is 10%), which pruning cannot remove. A delete between compaction and prune gets no subsequent pass; restart loses scheduled cleanup, and failures only log. Force deletion materialization and make cleanup recoverable, retried, and aware of intervening writes. Verify stored bytes after low-fraction deletes, intervening deletes, restart, and transient failure. - Empty upserts lose replay/CAS state (
search/src/store.rs:268–284, 531–534). An emptyr2replacingr1succeeds but removes the rows holding its revision. Retrying after a lost response returns 409, and a later write replacingr2also conflicts. Preserve revision state atomically for zero-chunk sources; cover replay and CAS across restart and an initially empty source. - First-write initialization is not resumable (
search/src/store.rs:327–329). A failure or restart after table creation but before the first FTS index leaves a table whose same-revision retry succeeds without creating indexes. Lance's CombinedFields planner then refuses lexical/hybrid searches. Resume initialization on reopen/retry and test interruption between these commits.
Validation: reviewed the new cleanup guard and rechecked the storage paths and pinned Lance compaction/CombinedFields code. Local tests were not rerun because protoc remains unavailable. CI search/check/e2e jobs are pending. The external engineering-style file is unavailable on this machine.
2fdfc1a to
416f658
Compare
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 416f658c07a3bed636325a3dc8eb5df54e956415, against #772 at 98c1081af2b65f444f14ce2aa79d676e056c6272.
None of the three blockers in my previous review is fixed. This is a rebase: search/, its workflow, and the eval-sink change are identical to that reviewed head. The separate service workspace remains appropriate in scope and complexity.
Blocking:
- Physical erasure remains unreliable (
search/src/store.rs:467–507). Default compaction can retain low-fraction deletions in the current file (the pinned Lance threshold is 10%), which pruning cannot remove. Deletes between compaction and prune get no subsequent pass; restart loses scheduled cleanup, and failures only log. Force deletion materialization and make cleanup recoverable, retried, and aware of intervening writes. Verify stored bytes after low-fraction deletes, intervening deletes, restart, and transient failure. - Empty upserts lose replay/CAS state (
search/src/store.rs:268–284, 531–534). An emptyr2replacingr1succeeds but removes the rows holding its revision. Retrying after a lost response returns 409; a later write replacingr2also conflicts. Preserve revision state atomically for zero-chunk sources; cover replay and CAS across restart and an initially empty source. - First-write initialization is not resumable (
search/src/store.rs:327–329). Failure or restart after table creation but before the first FTS index leaves a table whose same-revision retry succeeds without creating indexes. CombinedFields then refuses lexical/hybrid searches. Resume initialization on reopen/retry and cover interruption between these commits.
Validation: checked out this exact head, compared the service with the previous reviewed head, and rechecked its storage paths and pinned compaction implementation. Local tests were not rerun: protoc is still unavailable. CI search/check/e2e jobs are pending. The external engineering-style file is unavailable on this machine.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 3ca27eb8805c6e669dc86f95646dbefcffa70a95, against #772 at 63058fdad1976412425f2c05f22990b6fc5eb580.
The earlier empty-source revision/CAS and interrupted-initialization blockers are addressed: marker rows preserve revisions atomically, and opening a table completes its text indexes. SEARCH_LISTEN_FILE is fixed too. Cleanup now forces deletion materialization, rebuilds indexes, serializes passes with writes, and retries failed passes. Two failure paths still leave the physical-erasure blocker unresolved.
Blocking:
-
A committed replacement can escape cleanup after index optimization fails (
search/src/store.rs:400–409, replay at 380–381). On the 32nd write since optimization,merge.executecommits the replacement beforeoptimize(Index).await?. A transient index read/write failure returns before settingremovedor starting maintenance. With no maintenance already pending (e.g. initial source inserts followed by this replacement), retrying the same revision succeeds through the early replay return and never schedules cleanup. The old text remains in storage until another replacement/delete or a restart. The pinned LanceDB Index action propagates errors and does not roll back the preceding merge. Register cleanup immediately after the committed replacement, before fallible optional optimization; cover an optimization failure followed by a successful same-revision replay and no further writes, checking that the old bytes eventually disappear. This ordering hole also existed in the earlier implementation and my earlier reviews missed it. -
Restart recovery abandons cleanup after three failed table opens (
search/src/store.rs:307–321). If a restart interrupts deletion cleanup and that table's reads fail for the sweep's three attempts, the last error is only logged; the sweep advances and eventually marks itself done. When storage recovers, nothing retries that table unless another request opens it. A quiet scope can therefore retain deleted text indefinitely after an ordinary transient storage outage. Keep failed scopes scheduled for retry with bounded backoff (without preventing other scopes from progressing). Cover failed opens through the current retry limit, then recovery without a client request, and assert physical erasure.
The added maintenance state is justified by the durability contract; the service remains appropriately isolated from the platform. The remaining fix boundary is ensuring cleanup cannot be forgotten either between a committed write and its scheduling or during restart discovery.
Validation: source review of the changes and pinned LanceDB optimization path; the new head merges the embeddings base with no search-service changes from d96de7ba. Local cargo test --manifest-path search/Cargo.toml --locked was started but stopped during dependency compilation after confirming protoc remains unavailable; no tests completed. Search/check/e2e CI jobs are pending. The external engineering-style file is unavailable on this machine.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at b40838865cb9bfc550a79a8b3fb52f5ba66f745e, against #772 at 63058fdad1976412425f2c05f22990b6fc5eb580.
Both blockers from my previous review remain. The earlier empty-source revision and interrupted-index initialization fixes remain intact. This head adds permanent scope termination, with an additional interrupted-operation hole.
Blocking:
-
Committed replacements can still escape cleanup after index optimization fails (
search/src/store.rs:410–419). On the optimization write, the merge commits before fallible index optimization, which can return before marking removal or scheduling maintenance. A same-revision retry returns success without scheduling it. With no existing maintenance, old text remains until another removal or restart. Register cleanup immediately after the committed replacement, before optional optimization. Cover optimization failure followed by replay and no further writes, asserting eventual physical erasure. -
Restart recovery still abandons a table after three failed opens (
search/src/store.rs:312–325). After an ordinary storage outage spanning those attempts, the sweep logs the last failure and advances permanently. A quiet scope whose cleanup was interrupted can retain deleted bytes indefinitely after storage recovers. Keep failed scopes scheduled with bounded backoff while allowing others to progress; test recovery after the retry limit without a client request. -
The termination tombstone is ignored when the data table survives (
search/src/store.rs:579–586,entry/upsertat 343–388). Restart after the tombstone commits but before the table drops, or let that drop fail before deleting it. On restart/the next request,entryopens the surviving table without checkingended;upsertthen writes it normally. Onlycreatechecks the tombstone, and the recovery sweep explicitly skips tombstones, so it never completes this interrupted end. Enforce the durable tombstone when opening/admitting writes to an existing table, and recover/retry the outstanding drop. Cover restart between these commits and a failed drop, with a delayed upsert refused and the old table eventually removed without another end request.
The tombstone approach is appropriate in scope and complexity; the missing boundary is making persisted termination authoritative even while its cleanup is incomplete.
Validation: checked out the exact head, reviewed its delta and affected recovery/write paths, and inspected CI (search/check/e2e still running). Local tests were not run because protoc is unavailable. The external engineering-style file is also unavailable on this machine.
|
Thanks, Alex. All three are fixed at the root in d96de7b. b408388 adds one more service change, for #783's lifecycle blocker. Each test below failed before its fix. 1. Physical erasure. A pass of maintenance runs holding the table's writes:
Passes repeat until the table is clean: one version and no deletion file.
Recovery needs no separate marker: the store's own state (more than one version, or a deletion file) says what is left. After a restart, Tests check the store's files:
One limit of the byte check: index files hold words encoded. I checked that the data files hold them as plain bytes and the FTS files don't. For indexes, the guarantee is by construction (rebuilt from the remaining rows after any removal), not by grep. Maintenance tasks now end with their 2. Empty upserts keep their revision. Every source has its own row holding its revision and
On 2fdfc1a it failed exactly as you described: 3. First-write initialization is resumable. A table is opened only with its four FTS indexes:
On 2fdfc1a it failed with "combined_fields requires an inverted index". Non-blocking. For #783. |
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 3214c72a2c25bd3200f4b8c6834eff72007dc798, against #772 at 5b643e9d714b768f30ed6ef1fd5b89f46e2b53a6.
All three blockers from my previous review remain in unchanged code. The new pruning cutoff addresses the separate reader-grace problem; I checked its strict timestamp comparison in pinned Lance. The earlier empty-source revision and interrupted-index initialization fixes remain intact.
Blocking:
-
Committed replacements can escape cleanup after index optimization fails (
search/src/store.rs:410–419). On the optimization write, the merge commits before fallible index optimization, which can return before marking removal or scheduling maintenance. A same-revision retry succeeds without scheduling it. With no existing maintenance, old text remains until another removal or restart. Register cleanup immediately after the committed replacement, before optional optimization. Cover optimization failure followed by replay and no further writes, asserting physical erasure. -
Restart recovery abandons a table after three failed opens (
search/src/store.rs:312–325). A storage outage spanning those attempts makes the sweep log and advance permanently. A quiet scope whose cleanup was interrupted can retain deleted bytes indefinitely after storage recovers. Keep failed scopes scheduled with bounded backoff while allowing others to progress; test recovery beyond the retry limit without a client request. -
A surviving data table bypasses its termination tombstone (
search/src/store.rs:579–586, admission at 343–388). Restart after the tombstone commits but before the drop, or let the drop fail before removing the table.entryopens the surviving table without checkingended, andupsertwrites it. Onlycreatechecks the tombstone; the sweep skips tombstones and never completes the interrupted end. Enforce persisted termination on existing-table admission and recover/retry its outstanding drop. Cover interruption and failed drop, with delayed writes refused and the old table eventually removed without another end request.
The separate service and this small pruning correction remain appropriate in scope and complexity. The outstanding fix boundary is durable cleanup scheduling and authoritative termination across ordinary failures.
Validation: reviewed the exact head and its delta, rechecked the affected storage paths and pinned pruning implementation. Local tests were not run because protoc remains unavailable. CI check passed; search and e2e jobs are pending. The external engineering-style file is unavailable on this machine.
fragment-search, its own workspace and image (search/README.md): one LanceDB table a scope (a name the platform makes from a fragment's identity, incarnation and read scope), its rows a source's sections' chunks, so a section's vectors are its chunks'. Upsert replaces a source whole in one commit (merge_insert, its old chunks deleted in it); the same revision again writes nothing; `replaces` is a compare-and-set; a table's space is pinned by its first write and another is refused; vectors are reused by their text's hash. Search is BM25F over title, heading, context and text, and cosine over the vectors, each leg's sections ranked by their best chunk and fused by reciprocal rank, with snippets and signals. What a write removes leaves the store's files within about three minutes (compaction, then a prune). Bearer-token auth; a body read to its limit and no further. The same image runs in Cloudflare Containers on R2 (its S3 API) and beside celld on S3 or a disk: only its storage URI and keys change. Tests: limits, fusion, snippets; then on a directory, valid, replay, stale, reuse, spaces, restart, sources paging, purge (no deleted word in any file) and drop, and the HTTP API's token; and, by hand, the same on an S3-compatible store (passed on SeaweedFS's gateway). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every setting the service reads may be a file's instead (SEARCH_TOKEN_FILE, SEARCH_STORAGE_<OPTION>_FILE): a secret mounted in, never on a command line. The object-store test reads its keys so too, and says how long each step took (first write, a cold search after a reopen, a warm one, a replace, a purge): the latencies R2 is measured by. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nterrupted tables Physical erasure: a pass of maintenance, holding the table's writes, rewrites every file holding a deleted row (threshold 0: Lance's 10% kept low-fraction deletes for good), rebuilds the text indexes after a removal, and prunes; passes repeat until one version deleting no row is left, a failed pass is retried with backoff, and a write meanwhile gets a pass after it. The store's own state says what is left, so the service sweeps every table after a restart and finishes it. Writes fold rows into indexes only; maintenance alone compacts. Revisions: every source has its own row holding its revision, written in the same merge_insert, so sections: [] keeps it (replay is a no-op, CAS holds, across restarts, from a source's first write). Initialization: a table is opened only with its text indexes, made when missing, so a first write cut off between table and index is finished by its retry or the next open before it is served. Tests check the store's files after a delete of a twentieth of a file, a delete between passes, a restart and a failed pass, and inject the interrupted first write with a read-only index directory. SEARCH_LISTEN reads _FILE too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v1/{scope}/end makes the scope's tombstone, then drops its table;
a write after it is refused 410 ended, however late it comes (one sent
before the end), across restarts. A scope is one life of one fragment, so
it is never needed again. A write that waited on a table dropped under it
starts over (made again after a drop, refused after an end).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lance prunes by when a version was made, so a pass pruned the version it had just superseded (made long before) from under a search still reading it: CI's debug run caught one read failing on a rebuilt index's pruned files. Prune every version older than the newest made more than keep ago (lancedb's absolute cutoff). Tests keep a second, as the service keeps a minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3214c72 to
080727c
Compare
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 080727c3666ad2ae4c43150c704736bea418d7a1.
All three blockers from my previous review remain. This head rebases onto master; search/, its workflow, and the eval-sink change are identical to the previously reviewed head. The earlier empty-source revision and interrupted-index initialization fixes remain intact.
Blocking:
-
Committed replacements can escape cleanup after index optimization fails (
search/src/store.rs:410–419). The merge commits before fallible index optimization, which can return before marking removal or scheduling maintenance. A same-revision retry returns success at lines 390–391 without scheduling it. With no existing maintenance, old text remains until another removal or restart. Register cleanup immediately after the committed replacement, before optional optimization. Cover optimization failure followed by replay and no further writes, asserting physical erasure. -
Restart recovery abandons a table after three failed opens (
search/src/store.rs:312–325). An ordinary storage outage spanning those attempts makes the sweep log and advance permanently. A quiet scope whose cleanup was interrupted can retain deleted bytes indefinitely after storage recovers. Keep failed scopes scheduled with bounded backoff while allowing others to progress; test recovery beyond the retry limit without a client request. -
A surviving data table bypasses its termination tombstone (
search/src/store.rs:579–586, admission at 341–388). Restart after the tombstone commits but before the drop, or let the drop fail before removing the table.entryopens the surviving table without checkingended, andupsertwrites it. Onlycreatechecks the tombstone; the sweep skips tombstones and never completes the interrupted end. Enforce persisted termination on existing-table admission and recover/retry its outstanding drop. Cover interruption and failed drop, with delayed writes refused and the old table eventually removed without another end request.
The separate service remains appropriate in scope and complexity. The outstanding fix boundary is durable cleanup scheduling and authoritative termination across ordinary failures.
Validation: checked out the exact head, compared the service with the previous reviewed head, and rechecked the affected storage and test paths. Local tests were not rerun because protoc remains unavailable. CI search/check/e2e jobs are pending. The external engineering-style file is unavailable on this machine.
…nds that finish - A replacement registers its cleanup as soon as its merge commits, before the optional index fold, which no longer fails the write: a fold error left a committed removal unscheduled, and its replay never scheduled it. - The sweep after a restart keeps a table it cannot open, trying it again with backoff (at most 15 minutes) while the others go on; it gave up after three tries. - An ended scope's tombstone is authoritative: a table that survived its drop (a restart between tombstone and drop, a failed drop) is never opened, and its drop is finished by entry, by the sweep (tombstones are swept), and by a retry loop after a failed drop. Tests, each failing on 080727c: a replacement whose fold fails then replays with no write after; a table unreadable past four sweep tries; an end cut off before its drop; a failed drop with a late write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexlwn123
left a comment
There was a problem hiding this comment.
Pass at d5b9a4dd61ab903ef574311c245afce4394a465b.
All three blockers from my previous review are addressed: committed replacements schedule cleanup before fallible index optimization; restart recovery retains failed scopes and retries with bounded backoff; and persisted termination is checked before serving an opened table, with interrupted and failed drops retried. The earlier empty-source revision and interrupted-index initialization fixes remain intact.
I reviewed the delta and surrounding write, admission, maintenance and termination paths, including the new failure/restart tests. The recovery machinery is appropriate to the durability contract, and I see no remaining blocking regression.
Validation: exact head checked out; diff whitespace check passed. CI search/check/e2e jobs are still pending. Local tests were not run because protoc is unavailable; the referenced external engineering-style file is also unavailable on this machine.
|
Thanks, Alex. All three are fixed at the root in d5b9a4d, a fast-forward on 080727c (no rebase). I'm sorry: I posted my last round's reply before your 03:15 review arrived, so these three went unanswered until now. 1. Cleanup is scheduled at the commit (blocking). A replacement now sets The every-32-writes index fold is now optional. A failure is logged and Test
On 080727c it failed at the last assertion: no maintenance had been scheduled, and the old text stayed. 2. The sweep never gives up on a table (blocking).
Test
This test needs the new 3. The tombstone is authoritative (blocking).
Tests:
Results: the service suite is 17/17, three times in debug (as CI runs it) and in release, with clippy clean. The capability's e2e |
PR 3a of the search stack, on #772. It adds the portable search service: one Rust binary and one image, LanceDB OSS on R2 (Cloudflare), any S3-compatible store, or a disk (self-hosted). The cell's capability (3b) comes next and calls it. This PR changes nothing at runtime.
What it is
search/: its own Cargo workspace and Docker image, plus a CI workflow (.github/workflows/search.yml: clippy and tests). The API, storage and self-host recipe are insearch/README.md.merge_insertwrites a source's new chunks and deletes its old ones in one commit, so a search sees its old revision or its new, never part.(source, revision)again writes nothing.replacesis an optional compare-and-set. A stale replay gets 409conflictwithcurrent.{model, dimensions, version}(ornull, words alone). A write or search in another space is 409space_changed, so vectors are never mixed.known). A grown conversation embeds only its new turns.CombinedFields, which the benchmark preferred over its per-column match.signals.hybrid,lexical,semantic.sections: []keeps it. A table is opened only with its text indexes, so an interrupted first write is finished before it is served.POST /v1/{scope}/endtombstones a scope and drops its table; every write after is 410ended.src/api.rs)./healthzis open.SEARCH_STORAGEis a directory ors3://bucket/prefix.SEARCH_STORAGE_<OPTION>becomes a LanceDB storage option.If-None-Match), so no lock service is needed.Tests
replacesrefuses a stale write;sourcespages in order;purgethe deleted word is in no file;on_an_object_store(ignored; needs a store): the same flow on S3. It passed here against SeaweedFS's S3 gateway; MinIO's images are no longer freely pullable. R2 comes in 3b, since it's a paid store.cargo xtask checkpasses (search/is placed inxtask/src/eval_sinks.rs's RUNTIME).Notes
lancedbis pinned to 0.40.0 by the two-week cooldown. It compiles only with itsremotefeature (an upstream bug);awsprovides the S3 store.🤖 Generated with Claude Code