fix(stdlib): build with --no-default-features again, unbreaking the auto-optimize relink (#7764) - #7772
fix(stdlib): build with --no-default-features again, unbreaking the auto-optimize relink (#7764)#7772proggeramlug wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthrough
Changesstdlib feature compatibility
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…uto-optimize relink (#7764)
85c5616 to
60ed63a
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-stdlib/src/worker_threads.rs`:
- Around line 973-974: The immediate-termination path around
js_promise_new_for_native_resolution must balance the Promise pin when
resolved_now is true. Route creation and settlement through the async_shim in
both branches, or invoke unpin_promise_after_native_resolution() before the
direct perry_runtime::js_promise_resolve() only under async-runtime, while
preserving normal pending-worker processing.
In `@crates/perry-stdlib/src/worker_threads/async_shim.rs`:
- Around line 44-50: The async-runtime Promise lifecycle is unbalanced between
deferred and immediate settlement paths. Update
js_promise_new_for_native_resolution in
crates/perry-stdlib/src/worker_threads/async_shim.rs:44-50 and the immediate
js_promise_resolve path in crates/perry-stdlib/src/worker_threads.rs:973-974 so
the Promise remains rooted until queued worker resolutions finish, or is
explicitly unpinned before immediate settlement, matching the bridge contract.
- Around line 52-73: Update queue_deferred_resolution and the corresponding
async bridge so converter is never executed on the spawned
wait_for_direct_message_ack thread. Dispatch the conversion to the
main-thread/runtime context, then call queue_promise_resolution with the
converted value there; preserve the existing success/rejection behavior and
call-site compatibility.
- Around line 39-43: Update the no-async-runtime inline ensure_pump_registered()
path so it registers the stdlib worker pump and active-handle hooks instead of
remaining a no-op. Keep Tokio-specific runtime setup gated by async-runtime, and
preserve the unconditional callers from js_worker_threads_worker_new() and
js_worker_threads_message_channel_new() so queued worker events, parentPort
messages, and termination promises continue draining.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b939eadb-f308-4f5e-a8de-eb2cebb22ded
📒 Files selected for processing (8)
changelog.d/7772-stdlib-no-default-features.mdcrates/perry-stdlib/src/common/dispatch/init.rscrates/perry-stdlib/src/worker_threads.rscrates/perry-stdlib/src/worker_threads/async_shim.rscrates/perry-stdlib/src/worker_threads/broadcast_channel.rscrates/perry-stdlib/src/worker_threads/direct_message.rscrates/perry-stdlib/src/worker_threads/message_port.rscrates/perry-stdlib/src/worker_threads/worker_pump.rs
| let promise = | ||
| unsafe { crate::worker_threads::async_shim::js_promise_new_for_native_resolution() }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files of interest:"
fd -a 'worker_threads\.rs|async_bridge\.rs|async_shim\.rs' . | sed 's#^\./##'
echo
echo "Inspect worker_threads around lines 950-1015:"
if [ -f crates/perry-stdlib/src/worker_threads.rs ]; then
sed -n '940,1025p' crates/perry-stdlib/src/worker_threads.rs | cat -n
fi
echo
echo "Search for async_shim symbols:"
rg -n "queue_promise_resolution|js_promise_resolve|js_promise_new_for_native_resolution|promise_unpin|native_resolution|pin|resolve" crates/perry-stdlib/src crates -g '*.rs' | head -n 200Repository: PerryTS/perry
Length of output: 26635
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "async_shim outline:"
ast-grep outline crates/perry-stdlib/src/worker_threads/async_shim.rs --view expanded | sed -n '1,220p' || true
echo
echo "async_shim relevant content:"
sed -n '1,260p' crates/perry-stdlib/src/worker_threads/async_shim.rs | cat -n
echo
echo "async_bridge outline:"
ast-grep outline crates/perry-stdlib/src/common/async_bridge.rs --view expanded | sed -n '1,240p' || true
echo
echo "async_bridge relevant content:"
sed -n '1,260p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n
echo
echo "worker_threads module/header relevant content:"
sed -n '1,80p' crates/perry-stdlib/src/worker_threads.rs | cat -n
sed -n '680,790p' crates/perry-stdlib/src/worker_threads.rs | cat -nRepository: PerryTS/perry
Length of output: 28996
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Inspect async_bridge resolution/settle paths:"
sed -n '350,430p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n
echo
echo "Inspect process_pending and pending drain:"
sed -n '440,530p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n
echo
echo "Search unpin_promise_after_native_resolution:"
rg -n "unpin_promise_after_native_resolution|js_stdlib_process_pending" crates/perry-stdlib/src crates -g '*.rs'Repository: PerryTS/perry
Length of output: 17175
Balance the native-resolution promise on immediate termination.
When async-runtime is enabled, js_promise_new_for_native_resolution() pins the Promise. The resolved_now branch calls perry_runtime::js_promise_resolve() directly, so unpin_promise_after_native_resolution() is never run through js_stdlib_process_pending(). Repeated termination of exited workers leaves pinned Promises that cannot be collected. Route native Promise creation and settlement through async_shim in both branches, or add a branch that directly calls unpin_promise_after_native_resolution() before resolve only for the async-runtime path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-stdlib/src/worker_threads.rs` around lines 973 - 974, The
immediate-termination path around js_promise_new_for_native_resolution must
balance the Promise pin when resolved_now is true. Route creation and settlement
through the async_shim in both branches, or invoke
unpin_promise_after_native_resolution() before the direct
perry_runtime::js_promise_resolve() only under async-runtime, while preserving
normal pending-worker processing.
Source: MCP tools
| #[cfg(not(feature = "async-runtime"))] | ||
| mod inline { | ||
| /// No bridge means no pump to register. | ||
| pub(crate) fn ensure_pump_registered() {} | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'ensure_pump_registered|js_register_stdlib_pump|js_register_stdlib_has_active|js_worker_threads_(process_pending|has_pending|channels_process_pending|channels_has_pending)' \
crates/perry-stdlib crates/perry-runtimeRepository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- worker async_shim feature gates ---\n'
sed -n '1,140p' crates/perry-stdlib/src/worker_threads/async_shim.rs
printf '\n--- worker pump outline and relevant sections ---\n'
ast-grep outline crates/perry-stdlib/src/worker_threads/worker_pump.rs --view expanded || true
rg -n -C 8 'js_worker_threads_(process_pending|has_pending)|start_worker|start_stdin_reader|worker_threads_process_pending|worker_threads_has_pending|ensure_pump_registered|queue_promise'| crates/perry-stdlib/src/worker_threads src/worker_threads crates/perry-runtime/src/common crates/perry-runtime/src/event_pump 2>/dev/null || true
printf '\n--- async_bridge registrations and active/pending methods ---\n'
sed -n '430,475p' crates/perry-stdlib/src/common/async_bridge.rs
rg -n -C 5 'js_register_wait_driver|ensure_gc_scanner_registered|js_stdlib_init_dispatch|ensure_pump_registered|js_run_stdlib_pump|js_stdlib_process_pending|js_stdlib_has_active_handles' crates/perry-runtime crates -g '*.rs' | head -240
printf '\n--- Cargo features for perry-stdlib/runtime ---\n'
rg -n 'async-runtime|perry-stdlib|perry-runtime' Cargo.toml crates -g 'Cargo.toml' | head -160Repository: PerryTS/perry
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- worker async_shim feature gates ---'
sed -n '1,140p' crates/perry-stdlib/src/worker_threads/async_shim.rs
printf '%s\n' ''
printf '%s\n' '--- worker pump outline and relevant sections ---'
ast-grep outline crates/perry-stdlib/src/worker_threads/worker_pump.rs --view expanded || true
rg -n -C 8 'js_worker_threads_(process_pending|has_pending)|start_worker|start_stdin_reader|worker_threads_process_pending|worker_threads_has_pending|ensure_pump_registered|queue_promise' \
crates/perry-stdlib/src/worker_threads crates/perry-runtime/src/common crates/perry-runtime/src/event_pump 2>/dev/null || true
printf '%s\n' ''
printf '%s/n' '--- async_bridge registrations and active/pending methods ---'
sed -n '430,475p' crates/perry-stdlib/src/common/async_bridge.rs
rg -n -C 5 'js_register_wait_driver|ensure_gc_scanner_registered|js_stdlib_init_dispatch|ensure_pump_registered|js_run_stdlib_pump|js_stdlib_process_pending|js_stdlib_has_active_handles' \
crates/perry-runtime crates -g '*.rs' | head -240
printf '%s\n' ''
printf '%$s\n' '--- Cargo features for perry-stdlib/runtime ---'
rg -n 'async-runtime|perry-stdlib|perry-runtime' Cargo.toml crates -g 'Cargo.toml' | head -160Repository: PerryTS/perry
Length of output: 46426
Provide a no-default worker pump registration path.
js_worker_threads_worker_new() and js_worker_threads_message_channel_new() call worker_threads::async_shim::ensure_pump_registered() unconditionally. Without async-runtime, the inline shim is a no-op, so worker events/parentPort messages and termination promises can remain queued, and the event loop can exit before they drain. Keep the Tokio runtime setup behind async-runtime, but still register the stdlib pump and active-handle hooks in the no-default path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 39 - 43,
Update the no-async-runtime inline ensure_pump_registered() path so it registers
the stdlib worker pump and active-handle hooks instead of remaining a no-op.
Keep Tokio-specific runtime setup gated by async-runtime, and preserve the
unconditional callers from js_worker_threads_worker_new() and
js_worker_threads_message_channel_new() so queued worker events, parentPort
messages, and termination promises continue draining.
Source: MCP tools
| /// # Safety | ||
| /// Mirrors `async_bridge::js_promise_new_for_native_resolution`. | ||
| /// | ||
| /// No pinning: pinning guards the deferral window, and there is none here. | ||
| pub(crate) unsafe fn js_promise_new_for_native_resolution() -> *mut perry_runtime::Promise { | ||
| perry_runtime::js_promise_new() | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg 'crates/perry-stdlib/src/(worker_threads(_test)?|common/async_bridge\.rs)' || true
echo
echo "async_shim outline:"
ast-grep outline crates/perry-stdlib/src/worker_threads/async_shim.rs --view expanded || true
echo
echo "worker_threads relevant sections:"
wc -l crates/perry-stdlib/src/worker_threads.rs crates/perry-stdlib/src/worker_threads/async_shim.rs
sed -n '1,90p' crates/perry-stdlib/src/worker_threads/async_shim.rs
sed -n '930,990p' crates/perry-stdlib/src/worker_threads.rs
echo
echo "Search promise pins/unpins and js_promise_new:"
rg -n "js_promise_new|js_promise_(pin|unpin)|Promise|native_resolution|resolve" crates/perry-stdlib/src crates/perry-rt crates/perry-runtime crates/perry-rt-bindings 2>/dev/null | head -n 200Repository: PerryTS/perry
Length of output: 28993
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate async_bridge docs and perry_runtime Promise APIs:"
rg -n "js_promise_new|js_promise_new_for_native_resolution|pinning|deferral|queued settlement|unpin" crates/perry-stdlib/src crates 2>/dev/null | head -n 300
fd -a 'async_bridge\.rs' .
fd -a -i 'promise|worker_threads' . | head -n 200Repository: PerryTS/perry
Length of output: 40429
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "async_bridge full:"
cat -n crates/perry-stdlib/src/common/async_bridge.rs | sed -n '1,110p'
cat -n crates/perry-stdlib/src/common/async_bridge.rs | sed -n '470,555p'
echo
echo "worker_threads full context around promised termination:"
cat -n crates/perry-stdlib/src/worker_threads.rs | sed -n '960,1025p'
echo
echo "direct_message call sites:"
cat -n crates/perry-stdlib/src/worker_threads/direct_message.rs | sed -n '1,140p'
echo
echo "promises tests related to 859 native pin:"
cat -n test-files/test_issue_859_native_promise_pin.ts | sed -n '1,220p'Repository: PerryTS/perry
Length of output: 22648
Keep the bridge Promise contract balanced for async-runtime too.
With async-runtime, js_promise_new_for_native_resolution() now pins the Promise, but the inline immediate-settlement path still resolves it without unpinning. Keep deferral/non-deferral behavior in sync: preserve a GC root until all queued worker resolutions complete, or unpin before the immediate js_promise_resolve() path in the async-runtime fork as well.
📍 Affects 2 files
crates/perry-stdlib/src/worker_threads/async_shim.rs#L44-L50(this comment)crates/perry-stdlib/src/worker_threads.rs#L973-L974
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 44 - 50,
The async-runtime Promise lifecycle is unbalanced between deferred and immediate
settlement paths. Update js_promise_new_for_native_resolution in
crates/perry-stdlib/src/worker_threads/async_shim.rs:44-50 and the immediate
js_promise_resolve path in crates/perry-stdlib/src/worker_threads.rs:973-974 so
the Promise remains rooted until queued worker resolutions finish, or is
explicitly unpinned before immediate settlement, matching the bridge contract.
Source: MCP tools
| /// Settle now rather than queueing for a pump that does not exist. | ||
| pub(crate) fn queue_promise_resolution(promise_ptr: usize, is_success: bool, result_bits: u64) { | ||
| if promise_ptr == 0 { | ||
| return; | ||
| } | ||
| let promise = promise_ptr as *mut perry_runtime::Promise; | ||
| let value = f64::from_bits(result_bits); | ||
| if is_success { | ||
| perry_runtime::js_promise_resolve(promise, value); | ||
| } else { | ||
| perry_runtime::js_promise_reject(promise, value); | ||
| } | ||
| } | ||
|
|
||
| /// As above, running the converter inline. The `Send + 'static` bound is | ||
| /// kept so the two configurations accept the same call sites. | ||
| pub(crate) fn queue_deferred_resolution<F>(promise_ptr: usize, is_success: bool, converter: F) | ||
| where | ||
| F: FnOnce() -> u64 + Send + 'static, | ||
| { | ||
| queue_promise_resolution(promise_ptr, is_success, converter()); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 10 \
'queue_(deferred_)?resolution|std::thread::spawn|worker_messaging_error_value|js_promise_(resolve|reject)|js_string_from_bytes|js_error_new' \
crates/perry-stdlib/src/worker_threads
rg -n -C 8 \
'thread-local arenas|main thread|converter' \
crates/perry-stdlib/src/common/async_bridge.rsRepository: PerryTS/perry
Length of output: 36754
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- Rust files under perry-runtime/thread and promise ---\n'
git ls-files -- crates/perry-runtime | rg 'thread|promise|value|gc' | sed -n '1,200p'
printf '\n--- Runtime imports in async_bridge.rs ---\n'
rg -n 'js_(resolve|reject|promise_)|thread-local|arena|RuntimeRootVisitor|RuntimeHandleScope|main_thread' crates/perry-stdlib/src/common/async_bridge.rs crates/perry-runtime | sed -n '1,240p'Repository: PerryTS/perry
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Runtime files likely to contain thread arena/runtime context ---'
git ls-files -- crates/perry-runtime | rg 'thread|promise|value|gc|main' | sed -n '1,200p' || true
printf '%s\n' ''
printf '%s\n' '--- Async bridge import/context ---'
sed -n '1,80p' crates/perry-stdlib/src/common/async_bridge.rs
printf '%s\n' ''
printf '%s\n' '--- Direct message rejection path with spawned worker worker ack ---'
sed -n '130,180p' crates/perry-stdlib/src/worker_threads/direct_message.rsRepository: PerryTS/perry
Length of output: 13680
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- thread.rs arena definitions/usages ---'
sed -n '1,240p' crates/perry-runtime/src/thread.rs
printf '%s\n' ''
printf '%s\n' '--- promise API/resolve/reject definitions ---'
rg -n -C 8 'fn js_promise_(new|resolve|reject|resol|rejec)|js_promise_new_for_native_resolution|RuntimeHandleScope|RuntimeRootVisitor|ThreadLocal|thread_local!' crates/perry-runtime/src/promise crates/perry-runtime/src/gc/roots crates/perry-runtime/src/gc/mod.rs | sed -n '1,260p'
printf '%s\n' ''
printf '%s\n' '--- inline shim resolution functions ---'
sed -n '4,85p' crates/perry-stdlib/src/worker_threads/async_shim.rsRepository: PerryTS/perry
Length of output: 39964
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- locate RuntimeHandleScope definitions ---'
rg -n -C 8 'struct RuntimeHandleScope|impl RuntimeHandleScope|enum RuntimeHandleScope|RuntimeHandleScope' crates/perry-runtime/src/gc/roots crates/perry-runtime/src/gc/mod.rs crates/perry-runtime/src/gc | sed -n '1,220p'
printf '%s\n' ''
printf '%s\n' '--- locate thread-local arena initializers and allocation symbols ---'
rg -n -C 5 'thread_local|THREAD_LOCAL|PERRY_THREAD_LOCAL|gc_init|GcInit|Arena|alloc|malloc|thread_spawn|worker_thread|spawn' crates/perry-runtime/src/gc/mod.rs crates/perry-runtime/src/gc/roots.rs crates/perry-runtime/src/gc/roots crates/perry-runtime/src/thread.rs crates/perry-runtime/src/promise/mod.rs | sed -n '1,260p'
printf '%s\n' ''
printf '%s\n' '--- direct_message imports and start call paths ---'
sed -n '1,110p' crates/perry-stdlib/src/worker_threads/direct_message.rs
rg -n -C 6 'js_worker_threads_post_message_to_thread|wait_for_direct_message_ack|DirectMessaging|worker_messaging_error_value|queue_worker_messaging_rejection' crates/perry-stdlib/src/worker_threadsRepository: PerryTS/perry
Length of output: 50371
Do not run deferred promise converters on background threads.
js_worker_threads_post_message_to_thread() spawns wait_for_direct_message_ack(), and rejection converts worker_messaging_error_value(...) through queue_deferred_resolution(...). In the fallback shim, that converted js_error_new() runs on the spawned thread; in the async bridge, deferred converters are queued because perry-runtime uses thread-local arenas and the async bridge converters create JS values. Keep main-thread conversion in both shims/bridges so rejected promises do not allocate through the background thread’s runtime context.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 52 - 73,
Update queue_deferred_resolution and the corresponding async bridge so converter
is never executed on the spawned wait_for_direct_message_ack thread. Dispatch
the conversion to the main-thread/runtime context, then call
queue_promise_resolution with the converted value there; preserve the existing
success/rejection behavior and call-site compatibility.
Source: MCP tools
Fixes #7764.
What was broken
cargo build -p perry-stdlib --no-default-featuresproduced 12 errors. That is the configuration the auto-optimize relink builds with, so everyperrycompile that triggered auto-optimize fell back to the prebuilt archives with a warning, and any ad-hoc build neededPERRY_NO_AUTO_OPTIMIZE=1.Both causes violate the contract
common/mod.rsalready states in prose:Cause 1 — one line, exactly as diagnosed (#7745)
js_set_native_events_dispatchreferencedcrate::eventswithout the#[cfg(feature = "bundled-events")]that gates the module. The neighbouring registrations in the same function are gated —database-sqliteon the very next line — which is what makes this an omission rather than a decision. One#[cfg].Cause 2 — eleven sites the issue listed as "the same shape"
They are the same shape but not the same fix.
worker_threadsis always-on and reachescommon::async_bridgein eleven places across five files, and neither obvious repair works:#[cfg]per call site — two of them are value-producing (js_promise_new_for_native_resolution), so gating the statement leaves nothing to return.worker_threadsmodule — worse. It has no feature of its own, so its FFI symbols would vanish from the stripped archive and a program importingnode:worker_threadswould fail to link. That trades a build error for the test_gap_fetch_request_from_node_incoming_message SIGABRTs deterministically on pristine main, and is in no allowlist #7629 family of failure, which is a strictly worse outcome.So
worker_threads/async_shim.rssupplies the four entry points in both configurations — forwarding toasync_bridgewhen it is compiled in, settling inline when it is not.That inline arm is not invented semantics. The queue exists to hand work to the pump; with no pump there is nothing to hand it to, and doing the same work synchronously reaches the same observable end state (the promise settles). The pinning
js_promise_new_for_native_resolutionperforms is likewise a consequence of deferral — it guards the window between creation and the pump's resolution — and an inline settle spans no collection point, so a plainjs_promise_newis its correct counterpart. Each of those arguments is written down at the shim.Validation
cargo build -p perry-stdlib --no-default-features: 0 errors (was 12).cargo build -p perry-stdlib(default features): 0 errors.Both directions deliberately: my first cut of the shim accidentally imported itself, and
--no-default-featurescould not see it because that arm is#[cfg]'d out. Checking only the configuration named in the issue would have shipped it.The actual payoff, checked end to end:
perry compileof a small program now completes the auto-optimize relink with no fallback warning and runs correctly.cargo test -p perry-stdlib --lib: 111 passed, 0 failed.Targeted parity across worker-threads / events / message-port / broadcast / async / promise gap tests: all pass.
cargo fmt --all --check,scripts/check_file_size.sh: clean.One process note: a
test_gap_workerCOMPILE_FAIL appeared once and did not reproduce — it raced a rebuild that was still writing the archives. Re-run twice clean before believing that shape of failure.No version bump.
Summary by CodeRabbit