Skip to content

chore(release): pending release v0.107.1 - #1515

Merged
marcusrbrown merged 8 commits into
releasefrom
next
Sep 2, 2026
Merged

chore(release): pending release v0.107.1#1515
marcusrbrown merged 8 commits into
releasefrom
next

Conversation

@fro-bot

@fro-bot fro-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Pending Release: v0.107.1

This PR tracks changes pending release. Released on the next auto-release cycle (Sunday/Wednesday) or via manual dispatch.

Merge this PR to trigger a release. Releases also run automatically on Sunday/Wednesday at 20:00 UTC, or via manual workflow dispatch.

Commits Since Last Release


Auto-generated by the release pipeline. Updated: 2026-09-02 20:30 UTC

* fix(gateway): narrow int64 repository ids at the API boundary

GitHub models repository ids as int64, and @octokit/openapi-types v28 now
types them honestly as number | bigint. databaseId is a redaction denylist
deny key matched by Set<number> membership, where a bigint never matches and
a lossy Number() above 2^53 could collide with a different repo's id — so
neither propagating the union nor converting blindly is safe.

The conversion now happens once, at the only boundary where a repository id
enters, and only when the value round-trips exactly. An id that cannot be
represented yields a null databaseId rather than a wrong one, and keeps its
nodeId, which is independently usable as a deny key. Consumers omit a null
rather than persisting it, leaving the stored binding shape unchanged, and
the runtime narrowing in bindingToRepoKey is untouched, so a binding with
neither key is still denied.

Unreachable today at ids near 1e9, and no behavior changes at any id in use.
It compiles against both the current and the bumped octokit types, so it
stands on its own rather than riding a dependency update.

Refs #1467

* fix(gateway): centralize the repo-id narrowing and prove denial without a numeric key

Review follow-ups on the int64 narrowing.

The narrowing is now a named helper rather than an expression, and rejects
any id that is not a positive exactly-representable integer. Its parameter
spans both octokit response shapes, so the file still compiles against the
current and bumped types.

Both binding writers shared one omit-when-null rule expressed two different
ways; that policy now lives in a single helper, which also strips a stored id
that is not a usable number rather than carrying it forward. The backfill's
skip check asked whether a stored id was merely defined, while its sibling in
the redaction gate asked whether it was a number — the two now agree, so a
malformed stored id is repaired instead of being mistaken for a complete one.

Adds the test the design actually rests on: a binding with no numeric key but
a denied nodeId is still denied, an allowed one still allowed, and a binding
with neither key denied. Also covers the non-bigint rejections.

Refs #1467

* fix(gateway): apply one repository-id rule on both sides of the deny-key match

The previous commit made the binding side strict about repository ids and
left the denylist source side accepting any finite number. Both sides used to
be lossy in the same way, so an oversized id matched itself by accident; with
the binding side now yielding null, a redacted repos.yaml entry carrying only
a database_id and no node_id would have stopped matching — allowing a repo
that must be denied, and falsifying this change's own claim that a binding
keyed by nodeId still denies.

One predicate now defines a usable numeric repository id, and the read
boundary, the write boundary, and the denylist source all consult it. A
redacted entry whose only key is unrepresentable fails the load closed, which
is what the surrounding hardening already intended.

The backfill also treats a nodeId as sufficient completion, so a binding
whose id is not representable is no longer re-resolved and rewritten on every
pass; such a binding can only come from that same path, so re-fetching could
never have produced a numeric key anyway.

Refs #1467

* fix(gateway): make a deny-all schema failure locatable and say what actually failed

Tightening the denylist source means one malformed database_id now takes the
whole operator surface to deny-all, and the errors named neither the entry
nor the real defect. One message still blamed a missing database_id field for
entries where the field was present but unusable, and neither carried
anything an operator could use to find the row in a file the message must not
identify by name.

Both now report the entry's index. That is a position, not an identity: the
message reaches only doRefresh's server-side logger, never a response or the
audit stream, and anyone who can read the file can already count rows.

Also records the surviving non-positive bigint check as a fast path rather
than distinct validation, and corrects the AGENTS.md paragraph that still
claimed the denylist source rule was unchanged after the commit that changed
it.

Refs #1467

* fix(gateway): name the offending index on the malformed-entry failure too

The two schema failures that fail the denylist load closed gained an entry
index; the third, one line above them in the same loop, did not — and it is
the most reachable of them, since a stray dash or a commented-out body
produces a null entry while the others need a value past 2^53.

The index was already in scope. No no-oracle concern: the entry is still
unclassified at that point, so the code has not determined whether it is
redacted, and the index carries less than the two messages already reporting
it.

Refs #1467
@fro-bot
fro-bot Bot requested review from fro-bot and marcusrbrown as code owners August 31, 2026 06:41
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
@fro-bot fro-bot Bot mentioned this pull request Aug 31, 2026
77 tasks
* docs(plans): plan the session-cache bootstrap trap fix

Refs #1407

* docs(plans): deepen the session-cache trap plan

The first draft's central claim was wrong. Declining to persist bad state
prevents new poisoning but cannot escape existing poisoning: save keys are
unique per run and restore keys are prefixes returning the newest match, so a
run that declines to save leaves the poisoned entry as the newest one and the
next run restores it again. The loop stays absorbing.

The repair belongs on restore. Checkpointing a hot database between restore
and bootstrap recovers the session instead of discarding it, heals a stuck
repository in one run, and lands outside the budget that was failing, since
no timer spans those two calls.

Also corrects three things the draft asserted without grounds: that the
harness could await the child's exit, which it cannot observe; that the
checkpoint belonged in the session runtime, when both save paths and the
object-store sync share one function at the cache boundary; and that whether
the runtime could checkpoint at all was unknowable before implementation.

Refs #1407

* fix(cache): normalize the transported database set and make the bootstrap budget visible

Units 1, 4, 5 and 6 of the session-cache trap fix.

The wal-index is machine-local and SQLite never syncs it, so a copy from
another runner is stale by construction. It is no longer captured on save,
and a copy carried in by an older cache is deleted on restore rather than
left to age out. Restore still tolerates its presence so entries written
before this change still work.

Declaring storage corrupt now actually clears the database. It lives beside
the storage directory rather than inside it, so 'proceed with clean state'
had been leaving the very files it meant to discard. The object-store restore
also gains the corruption and version checks the cache path already had: it
wins on restore and returns before either one ran, so the authoritative path
was the unchecked one.

Which files constitute the database is now defined once, in the runtime
package, because the object-store sync there cannot import from the Action.
The name had been written out in three places.

The server bootstrap budget is configurable and reported. It was the SDK's
5000ms default, never passed and never logged, against runs observed landing
16ms inside it. Time spent in the spawn is measured apart from the total so
the budget comparison is exact and a slow port bind is distinguishable from
slow server init.

session-retention now reaches pruning. It was parsed, validated, logged, and
then never read, so a consumer bounding their cache with it got a validated
no-op. Its default matches the hardcoded value it replaces, so an unset input
changes nothing.

Refs #1407

* fix(cache): checkpoint the session database before anything captures it

No run had ever checkpointed it. server.close() sends a kill and returns
without waiting, so a valid session could sit entirely in the write-ahead log
with the database file at zero bytes, and the cache carried that hot log to
the next run, which paid recovery cost opening it — measured at 2416.8ms for
a 185MB log against 60.2ms once merged.

The checkpoint runs inside saveCache, which both the cleanup phase and the
post hook already call and which also owns the object-store sync, so every
capture path inherits it without a flag they must agree to read. It runs
before anything inspects file sizes, since merging changes which files are
non-empty.

Success is judged by the log's size on disk rather than the pragma's own
count, which reported zero on a verified-successful truncation. The outcome
has three states: collapsing 'already clean' into 'failed' would decline
saves that were always safe.

The harness cannot observe whether the writer exited, so the attempt is the
liveness probe — a live writer surfaces as busy and is retried within a bound.
A log that still cannot be merged declines the save and says why in the job
summary, because a save that silently does not happen is the failure this
repository already paid for once.

Refs #1407

* fix(cache): repair a restored database before the server opens it

This is what gets a repository out of the loop rather than merely stopping it
from entering one. Save keys are unique per run and restore keys are prefixes
returning the newest match, so a run that declines to save writes no entry and
the next restore hits the same bad one. Declining forever is a stable state.

Checkpointing the restored database heals it in place. It runs between restore
and bootstrap, where no timer applies — the budget covers createOpencode alone
— so the recovery cost moves out of the window that was timing out instead of
being paid inside it.

Repairing rather than wiping is the point. Wiping would also break the loop,
but it discards session history on every transient bootstrap failure, which is
the second reporter's case exactly.

A repair is logged because it is the evidence a stuck repository healed, and
its absence later is the evidence it stayed healed. The healthy case is silent,
and a failed repair still lets bootstrap try.

Closes #1407

Refs #1407

* fix(cache): correct a false claim about the database's on-disk shape

A comment asserted a valid session could leave opencode.db at zero bytes with
its data in the write-ahead log. Three independent reviewers read it and each
filed the same bug against the checkpoint helper's zero-byte guard, claiming
it skipped the case it exists for.

The guard is right and the comment was wrong. A database in write-ahead mode
with no checkpoint leaves the main file at its header page — 4096 bytes on
Node 24.20.0, never zero — and forcing it to actually zero makes the log
unreadable, so that shape holds no session to save. The comment now states
the measured value, and a test pins the real shape so the misreading cannot
recur.

Also from review: the errno checks in the two delete paths assumed every
thrown value carries a code, so a non-errno failure was silently treated as a
missing file; both now narrow before reading it. The bootstrap timeout is
bounded, since an unbounded value would hang a run to the job timeout rather
than failing fast, which is the opposite of what that input is for. And the
restore repair gains a test that drives a real database rather than a mock,
covering the wiring the ordering tests cannot see.

Refs #1407

* fix(cache): treat an unusable restored database as corrupt rather than passing it on

Review follow-ups on the trap fix.

A malformed database still trapped a repository in the loop this branch
exists to break, reached through corruption instead of a hot log. The
checkpoint already distinguished a live writer from a file SQLite says is not
a database; only the first is worth retrying, and the second now routes into
the clean-slate path restore already used for corrupt storage.

That covered a malformed database carrying a log, since that is the only
shape anything opened. One with no log was never opened at all, so it reached
bootstrap, failed, and was saved again. Restore now checks that a database it
is about to hand over can actually be read — one schema-page read, a few
milliseconds and independent of size, outside the budget. Construction alone
does not surface this: opening a file of plain text succeeds and only the
read reports it is not a database.

The repair's retries bounded their sleeps but not their work, so a large log
could spend far longer than intended ahead of bootstrap. A deadline between
attempts caps that without ever interrupting one in flight.

Also records why declining to save rarely costs a run its work — the post
hook retries once the child has exited — because a refactor that marked the
cache saved on decline would remove that quietly. And corrects a comment
claiming save paths are filtered by existence when only the log is.

Refs #1407

* test(cache): size the truncation target through its own open handle

CodeQL flagged the new integrity test: it measured the database with stat and
then reopened the same path to truncate it, which resolves the path twice and
is a check-then-use window. Statting the already-open handle closes it and
also makes the truncation apply to the file that was actually measured.

Refs #1407

* fix(cache): only delete a database SQLite says is structurally corrupt

The clean-slate path was reached by negating retryability, and retryability
was an allowlist of busy and locked. So every error outside that pair —
a full disk, an I/O fault, exhausted descriptors, a permissions problem —
answered a transient condition by deleting the repository's session history.
That is the trade this work rejected when it declined to wipe on any
bootstrap failure, reached through a narrower door.

Corruption is now matched on its own terms rather than inferred from the
absence of a retry, so an unrecognized error leaves the database alone. Both
comments already described the allowlist; the code now agrees with them.
Verified in both directions: a file of plain text still reports it is not a
database and is still cleared, while a database that merely cannot be opened
is left where it is.

The corruption downgrade also returned the original restore result, which
still described a hit with a path that had just been deleted, and reported a
source for a cache that no longer existed. Both now match the state on disk.

Save paths are built after the checkpoint rather than before, so the guard
reflects the log the checkpoint actually left behind.

Refs #1407

* fix(cache): keep the post-hook save symmetric with cleanup's

Review follow-ups.

The post hook built its save without the project-id path, so an archive that
landed only there omitted .git/opencode. That was harmless while the post
hook was an incidental fallback, but this work made it the documented reason
a declined save is acceptable, so the two call sites should agree. The path
derives from a runner-level variable available in the post process exactly
as the storage and auth paths already are.

Closing the acquired server on the bootstrap-failure path was the last
statement before returning an error, so a throw there would have rejected
from a function whose whole try/catch exists to return a result instead. It
is now best-effort, matching how this change already closes databases, and a
test drives a throwing close to prove the contract holds.

Also pins the usability probe against the object store rather than only the
cache. That source wins on restore and its own integrity checks cannot see
past the directory, so the probe is its only real defense there.

Refs #1407

* fix(cache): say what is true at the point each message is written

Review follow-ups.

The cacheable-content note justified inspecting the write-ahead log with a
case its own control flow forecloses: a declined checkpoint returns before
that check is reached, so the log there is always already merged or empty.
The header-page-only session it protects is real but arrives the other way,
and a test cited the old wording as its guard.

The decline summary claimed the next run may restore an older session, but
it is appended, not replaced, so it survived the post-hook retry that
usually saves the cache moments later and left the run asserting something
untrue. It now states only what holds when it is written.

Six action inputs declare a default in YAML that a constant repeats in
TypeScript, agreeing by hand. A guard now compares them, so a drift fails
loudly instead of quietly changing every run.

Refs #1407

* fix(cache): wait for the writer to exit before trusting a checkpoint

A successful checkpoint did not mean what the code assumed. shutdown()
signalled the OpenCode child and returned without awaiting its exit, so a
still-live connection could append to the write-ahead log immediately
after the checkpoint reported success:

  wal before checkpoint  2068272
  checkpoint returns {busy:0, log:0, checkpointed:0}, wal -> 0
  one late INSERT from an idle live writer            wal -> 4152

That is the same root cause as the bug this branch fixes: nothing ever
checkpointed *because* shutdown never waited.

The SDK exposes no way to await the child. createOpencode returns only
{client, server: {url, close}}, and close() calls proc.kill() on a handle
it never surfaces -- no pid, no exit event. So shutdown() now polls the
server's own port until connections are refused, which happens once the
OS reclaims the socket. The pinned server registers no SIGTERM handler,
so termination is immediate rather than drained. This is a boundary, not
a proof, and both the code and its comment say so: a timeout means the
child's fate is unknown, and cleanup warns that the checkpoint which
follows may race a writer.

With the writer actually gone, the log is empty on every path that
reaches transport, so neither transport carries it. Removing it from
DB_TRANSPORTABLE_BASENAMES only covered the object store; the Actions
cache added it independently in buildSaveCachePaths, and both had to
change.

That also fixes a regression this branch introduced. Uploads skip missing
files and never delete, so once healthy saves stopped producing a local
log, opencode.db-wal stopped being refreshed while opencode.db kept being
overwritten at the same prefix. A later restore then paired a fresh
database with a log from an older generation -- reproduced twice, once
surfacing as "database disk image is malformed" (which this branch
answers by deleting session history, then re-saving without overwriting
the orphan, so it repeats every run) and once far worse: the checkpoint
succeeded and silently replayed the stale log, turning 3000 rows back
into 2000 with no error at all.

The two sources need opposite treatment. An Actions cache entry is one
atomic archive, so a legacy pair is consistent and its log may hold
committed transactions -- it is checkpointed, never discarded. The object
store has no generation marker, and the silent-replay case proves SQLite
cannot reliably tell a stale pair from a valid one, so a downloaded log
is untrusted and deleted before anything opens the database. That runs
unconditionally after the sync, because an object store holding only a
sidecar can fall through to a cache miss and leave the file on disk.

Discarding an untrusted log can lose that generation's last
transactions. That is a real cost, recorded in the code, and still
better than silently rolling a session back.

Refs #1407

* fix(cache): make the untrusted-log deletion actually unconditional

Three follow-ups from review, all of which were true of the code and not
of what its comments claimed.

The object-store write-ahead log deletion sat inside the try, while its
comment said it ran unconditionally after the download. syncSessionsFromStore
calls fs.mkdir per key, a real throw site under ENOSPC/EACCES, and object
keys list lexicographically so opencode.db-wal arrives before any sessions
sidecar. A throw partway through left the log on disk, skipped the
deletion, returned a miss, and let the Actions-cache restore extract a
database beside it -- rebuilding the exact fresh-db/stale-log pairing the
previous commit removed, silent-replay variant included.

It now runs in a finally. Hoisting the call up into restoreCache was the
other option and would have been wrong: the store-disabled guard returns
before the try, so a locally-present log on a persistent self-hosted
runner has nothing to do with the object store and must not be deleted.
The finally covers every exit from the download attempt and none of the
paths where no download happened.

isPortOpen settled only on connect or error, so the bound on the
quiescence wait lived in an assumption about loopback rather than in the
code -- if neither event fired, cleanup awaited it forever. It now has a
socket timeout of one poll interval and a settled guard so teardown
resolves exactly once. A port that parses to 0 or out of range now
reports unconfirmed instead of reporting success without having checked
anything.

The duplicate OpenCodeServerHandle in features/agent still declared
shutdown as returning void after the runtime's began returning a promise.
No type error, because a promise-returning function is assignable to a
void-returning one, which is exactly why it was worth removing -- the
same drift the DB filename lists have guards against. One definition now,
imported through the existing sibling re-export, so no new cross-layer
import.

The throw-path test was checked against the old shape and fails there:
the log survived the download failure. Also pins the quiesced:false
warning branch, and a legacy Actions-cache pair surviving a round trip
with its transactions intact -- the half of the source-specific split
that preserves data rather than discarding it.

Refs #1407
* docs(solutions): record why repair must precede capture

Writes up #1407 and #1519 for docs/solutions/. The durable lesson is an
ordering rule -- repair on restore has to happen before capture on save,
or a transient failure gets encoded into durable state that the next run
then trusts.

The most useful section is what didn't work. Five plausible fixes were
tried and refuted by evidence: "only persist cleanly-checkpointed state"
(nothing had ever produced one), "declining to save breaks the loop"
(save keys are unique per run and restore keys are prefixes, so the
poisoned entry stays newest), "wipe and save empty state" (destroys
history on every transient failure), "the log is provably empty at
transport" (a late write from a live writer regrew it after the
checkpoint reported success), and "route non-retryable failures into the
clean-slate path" (an allowlist of retryable, negated, hands a full disk
to the delete branch).

Also records the regression the fix itself introduced, since it was
caused by the fix and caught only in review: once healthy saves stopped
producing a local write-ahead log, an upload that only ever sends what
exists stopped refreshing that object while the database beside it kept
being overwritten. One reproduction reported malformed; the other
silently replayed stale content and turned 3000 rows into 2000.

The safety claim about archiving the database alone is stated
conditionally rather than absolutely -- SQLite's default
wal_autocheckpoint is 1000 pages, and a live writer crossing it mutates
the main file with no explicit checkpoint, measured going from 4096 to
860160 bytes.

Overlap against the four adjacent docs scored Moderate at highest, so
this is a new doc cross-linked in both directions rather than folded into
an existing one.

Refs #1407

* docs(solutions): add the backlinks the description claimed

The PR body said the new doc was cross-linked in both directions. It
wasn't -- only forward links existed, and no existing doc referenced it.
This repo does follow a backlink convention, so the claim was checkable
and wrong.

Adds entries from the two highest-overlap neighbours, which are where a
reader is most likely to be standing when this becomes relevant: the
cache-continuity doc (a save that failed and reported success, against
one that succeeded and persisted the wrong thing) and the S3 restore-scope
doc (the same restore-is-not-save asymmetry one layer down -- capability
there, atomicity here).

Also names the remedy in the regression section. These docs are retrieved
by section rather than read end to end, so a reader landing there from a
`malformed` search could have concluded the hazard was still live; the
fix was described in two other sections but never pointed at from the one
that describes the problem.

Drops related_components. Both values were novel to the corpus and the
field is unguarded, so they would have seeded near-synonyms for no
retrieval benefit -- tags already carry sqlite and session-cache.

* docs(solutions): fix inverted deixis in both backlinks

Both backlinks were written from the new doc's vantage point and pasted
into the neighbour docs without flipping "there" and "here", so each one
described the two documents backwards.

The convention is established three lines above the first one, in the
same bulleted list: there = the linked doc, here = the doc you are
reading. So that file simultaneously claimed "here = a write that failed
and reported success" and "here = the save genuinely succeeded", about
itself. The cache-continuity doc is the failed-save-reported-success
incident; the new doc is the succeeded-save-persisted-poison one.

The S3 backlink had the same inversion: that doc is the capability half
of the restore-is-not-save asymmetry, and the new one is the atomicity
half.

Both links resolved the whole time, which is all check-md-links can
observe -- the text they resolved to was the part that was wrong. Fitting
for a doc about claims that are checkable and unchecked.

* docs(solutions): correct the count and heading case

"Five plausible fixes" undercounted the seven that follow -- the two it
dropped are the zero-byte-log recreation and the restoreCache hoist, both
of which were refused with reasons worth keeping. Carried unresolved
through two reviews, in the section the doc itself calls the useful one.

Also aligns the regression heading with the title case the rest of the
file uses.
…it (#1521)

* fix(runtime): report an inconclusive port probe as unknown, not as exit

Two follow-ups from the #1519 review, both cases where an unknown answer
resolved to the optimistic one.

The quiescence probe's socket timeout called finish(false), and false
means "port closed", which waitForServerQuiescence reads as quiesced and
returns immediately. So a connect that neither completed nor was refused
-- the one case the timeout exists for -- reported that the OpenCode
child had exited. That suppressed the warning telling an operator their
checkpoint may have raced a live writer, which is the whole reason the
signal exists. Every other unknown in this design resolves the other way:
verifyDatabaseUsable returns usable for an unrecognized throw, and
isStructuralCorruptionError returns false unless SQLite positively says
otherwise. It now returns true, so an inconclusive attempt keeps polling
and only the real deadline produces quiesced: false.

Testing it needed the socket to be injectable, since a real one either
connects or is refused and never reaches the timeout. The probe now takes
a connect function, and a fake socket drives the branch directly rather
than depending on a black-holed address behaving consistently in CI.

Second: a mid-transfer failure in the S3 adapter's download left a
partial opencode.db on disk. mainDbRestored stays false, so restore
reports a miss and falls through to the Actions cache; if that also
misses -- the normal case for a repository leaning on S3 -- the repair
block is skipped entirely and a truncated database reaches bootstrap
unprobed.

The suggested condition for cleaning it up was mainDbRestored === false
&& failed > 0 at the restore layer. That is unsafe: syncSessionsFromStore
increments failed for any key in the batch, so an unrelated sidecar
failure satisfies it while opencode.db was never attempted -- and on a
self-hosted runner with a pre-existing local database, deleting it
destroys live session state the failed download never touched.

Fixed at the write site instead. download() unlinks localPath only when
the pipeline itself throws, meaning the stream was open and a partial
write happened. A rejection before the pipeline is reached never touches
the path, so an existing file is provably safe -- pinned by a test.

Both fixes were checked against their pre-fix shapes and fail there.

Refs #1407

* test(runtime): pin the quiescence invariant at the call site

The polarity fix was pinned inside isPortOpen but not where it is read.
waitForServerQuiescence called it without an injector, so nothing proved
that an inconclusive probe keeps the loop polling -- and inverting the
read at the call site to `if (stillOpen) return {quiesced: true}` passed
every test in this PR, reintroducing the bug one line from where it was
fixed.

The optional connect parameter now threads through, defaulted so the
production call site is unchanged, and a test drives several poll cycles
of nothing-but-timeouts and asserts the only reachable outcome is
quiesced: false at the deadline. Verified against the inverted read: it
fails there.

Also narrows the s3-adapter cleanup comment, which claimed the unlink can
only reach a file the pipeline just opened. True for a mid-transfer
failure, not for an open failure -- if createWriteStream cannot open the
destination at all, pipeline rejects before truncating and the unlink
still fires against a file this attempt never wrote to, since unlink
needs permission on the directory rather than the file. The comment now
separates what is guaranteed from what is merely rare, and the behavior
is unchanged.

And records what the polarity flip costs: an inconclusive probe now rides
out the full 5s budget instead of returning after one poll interval. One
server per run and bounded, but whoever next tunes those constants
shouldn't have to rediscover it.

* test(runtime): decouple the quiescence test from the wall clock

Two review follow-ups, both in test code.

The call-site test asserted connectCalls > 1 against a 60ms budget with a
10ms poll interval. The do/while checks the deadline only after the first
iteration, so a single stalled delay() on a contended runner exits after
one probe and fails that assertion. Widened to a 500ms budget with a 5ms
interval so a stall cannot end the loop before a second probe. The
quiesced: false assertion was never at risk either way -- a stalled loop
still cannot produce quiesced: true -- so this only removes a spurious
red, and the comment now says so.

The s3-adapter test title claimed the pre-existing file was "at a
different path", which would be trivially true and worth nothing. It is
at the same path the download targets, which is the entire point: a
pre-pipeline rejection leaves untouched the exact file the cleanup would
otherwise delete. Title now matches what the body actually pins.
fro-bot Bot added 2 commits September 2, 2026 13:27
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
@marcusrbrown
marcusrbrown merged commit e6c2fe5 into release Sep 2, 2026
1 check passed
@marcusrbrown
marcusrbrown deleted the next branch September 2, 2026 20:33
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