Preview e2e flake hunt
This document is the evidence log — every flake, root cause, and marathon run. The policy distilled from it (one retry layer, watchdog sizing, retry telemetry) lives in testing.md → Retries and timeouts.
Current goal: run the complete preview pipeline against a real preview environment 25 times in a row without a single failure, with every full-fleet deploy plus e2e run completing in under seven minutes and without an absorbed test retry. Fix and document every failure, retry, or tail encountered along the way.
Round 1 (PR #1644) found and fixed nine root causes and merged them to main.
Round 2 (PR #1653, merged) added flakes 16–17 and the preview.ts lease/retry
hardening, and merged main's worker-build pipeline (#1612) — whose #writeChain
write serialization supersedes round 2's standalone flake-15 fix. Later rounds
repeat the same proof after substantial platform changes and record their exact
base revision, run IDs, and findings below.
Method: scripts/preview/flake-hunt-loop.sh sequentially dispatches the
canonical Depot cloudflare-previews.yml workflow. Every iteration is a normal
fresh-runner preview check—full-fleet deploy, every e2e lane, artifact upload,
GitHub timing, and PostHog telemetry—not a second implementation hidden inside
one long-running job. It fails fast on the first functional failure, moved
head, absorbed retry, or run at or above seven minutes, and writes a
machine-readable ledger containing the immutable head plus Depot run/attempt
IDs, whole-run duration, and retry count. Every failure, retry, or tail gets a
root-cause diagnosis and the smallest reliable fix; any of them resets the
consecutive-clean counter.
This zero-retry acceptance rule applies to new proof runs from 2026-07-22 onward. Historical ledgers below retain the semantics and retry counts they recorded at the time; they are evidence, not retroactively relabelled runs.
Once a failing test is isolated, use pnpm preview test-target to run its
Vitest file or Playwright spec repeatedly against the PR's already-deployed
preview. It does not deploy, erase, or overwrite the full-suite result; see
Dev environments → Story 2
for exact commands. This focused loop is diagnostic only: the accepted streak
still consists of complete deploy-plus-e2e runs on Depot.
Run the orchestrator from an authenticated workstation:
PR_NUMBER=<pr> REF=<branch> RUNS=25 ./scripts/preview/flake-hunt-loop.shThe workstation only dispatches and observes. Every counted attempt executes independently in Depot on the normal preview job's 16-core runner and sends the normal telemetry; if the workstation sleeps or exits, no running test is misclassified and no later run is silently counted. Resume by explicitly starting a new proof—accepted streaks are never inferred across ledgers.
Round 15 (2026-07-23, post-#2284)#
This round starts from merged origin/main at
0d8f96f9298f46590f6a8f3bbae1825e03c9660a. PR #2284 aligned public project
creation with its real nested birth barriers and added one observable,
pre-session recovery for a spawned CLI's initial WebSocket upgrade. The
recovery boundary cannot replay authentication, capability lookup, or user
code, and application/RPC errors remain non-retryable.
The final #2284 exact-head preview was clean functional evidence: all six expected E2E sources finalized, all 245 runnable test records passed, and Depot artifacts plus PostHog recorded zero framework retries, zero passed-after-retry outcomes, and zero initial-connection recovery markers. It does not count toward this round's consecutive proof because it is a different immutable head. It took 359 seconds from aggregate dispatch (346 seconds for the preview check), which is inside the revised seven-minute stability ceiling but remains a performance tail to remove after the stability proof.
That run also isolated the leading time floor. OS deployment took 79.45 seconds and OS E2E took 171.99 seconds, while OS Vitest itself took 63.55 seconds. Fresh project creation was held until the deployment-wide Durable Object rollout clock reached 90 seconds, and Vitest began only after that boundary plus agent smoke settled. Round 15 begins at 0/25 and first measures unchanged warm-slot runs through the canonical Depot workflow before changing the rollout critical path.
Round 15 seven-minute proof and Project description reset#
Exact head a0706e02629ec20ca205fdf12596cbc2b6f0d7f5 produced five consecutive
clean canonical runs in 339, 333, 271, 251, and 411 seconds. The fifth run
(q5hjb85fr1) completed in 6 minutes 51 seconds with zero framework retries.
That is direct evidence for the 420-second proof ceiling: the old five-minute
limit would have rejected a complete, clean run, while the revised limit left
only nine seconds of headroom.
Run six (ngtx6pwk2h) finished in 241 seconds but is rejected because
repo-ide-markdown-preview.spec.ts passed only on Playwright's second attempt.
The first attempt created its project successfully, then the pipelined
Project.__describe() call was rejected with durableObjectReset: true and
Durable Object reset because its code was updated. Exact-version Cloudflare
trace 0bd2b7add32f43a5e61359103b532f4f places the Project Durable Object reset
about 140 seconds after the OS deployment was recorded—well after the existing
90-second deployment-age gate. Repo and Stream Durable Objects reset in the
same interval. This is therefore deployment-wide lifecycle behavior, not a
markdown-preview assertion failure or a project-creation collision.
PostHog confirms the scope: during the preceding seven days the same explicit code-update reset reached at least 20 different live test names across unrelated branches. Cloudflare documents Worker and Durable Object rollout as eventually consistent over seconds to minutes and provides no finite deployment-complete barrier for all Durable Object identities. Increasing a fixed pre-test sleep would add permanent critical-path latency without proving that a later identity cannot be reassigned.
The product boundary is instead made honest: the read-only Project description operation now uses the existing Durable Object availability helper for exactly one observable replay. The whole logical read is repeated so its Project and Capability Host snapshots remain from one attempt. Only workerd lifecycle flags or the explicit cross-RPC stream-unavailable contract qualify; application errors remain authoritative and no mutation can be replayed. The first reset is logged with project and scope context, and a second failure is returned. Focused tests cover resets from either Durable Object branch, and the shared classifier tests retain the no-application-error/no-loop guarantees.
The retry resets the accepted streak to 0/25. Merging current origin/main at
ecc8d1fa48d17eda5e12b9c7ff6f03b75d8d0358 also invalidates the earlier
immutable head, so the next accepted proof starts from the merged corrective
head with the same seven-minute ceiling.
Round 15 Secret create storage reset#
The automatic preview on exact head
52bfeb9c7d9857d2612ecbcdff294484651f4ba2 was clean in 324 seconds. PostHog
recorded 310 preview and 3,106 unit outcomes with zero failures or retries. It
is baseline evidence rather than a counted marathon iteration because it was
not dispatched into the immutable proof ledger.
The first ledgered run (bmd2z9ncsd, attempt hs0vk1fzrm) completed in 252
seconds but is rejected. waitrose-session strategy: username/password secret mints on first use, re-mints on 401, session works on the API passed only on
Vitest's second attempt after Cloudflare returned Durable Object storage operation exceeded timeout which caused object to be reset. The failure was
therefore unrelated to the seven-minute ceiling.
The raw Vitest artifact and exact-version Cloudflare events agree on the
boundary. Project prj_17684080138441f18be4338ddcb1be0e was created and
described successfully; its following Secret.create ran from
14:22:59.842Z to 14:23:34.111Z and failed after 34.269 seconds. The test
retry addressed the same project and secret, and the repeated create completed
in 449 milliseconds. At the same instant, a different project recorded a
36.611-second project-api-key Secret seed reset. The paired stalls rule out a
Waitrose assertion or credential failure and localize the incident to Secret
Durable Object storage availability.
PostHog supplies the recurrence bound. The named test ran 224 times in the available 30-day window: seven runs retried (3.12%) and two failed after their retry. Its prior errors include stream wait and Durable Object storage resets. The same storage-reset family reached unrelated stream lifecycle, dynamic worker, remote-app, and browser tests, so changing this test's timeout or quarantining it would only move the symptom.
Secret.create already documents and implements the necessary replay
semantics: the birth append is idempotency-keyed, an identical-policy duplicate
keeps the first material, and a different egress, refresh, or visibility
policy still fails. The missing piece was the outer Secret RPC boundary. It now
uses the existing availability helper for exactly one observable replay while
workerd's lifecycle flags are still present. Application rejections are never
replayed, the first reset is logged with project and path context, and a second
failure remains authoritative. Focused tests cover both lifecycle recovery and
the application-error exclusion; the full OS unit suite and typecheck pass.
The rejected run resets the consecutive counter to 0/25.
Round 15 shared Playwright admin connection#
Exact head eb887acb553a66eb5bfe7c4402807ff429457ea9 first passed two
canonical runs in 275 and 320 seconds with zero retries. Run three
(mv74spgz2j, attempt x6zqb8b8jj) completed in 344 seconds but is rejected:
stream-resume-after-suspend.spec.ts passed only on Playwright's second
attempt after its control case reported WebSocket connection failed. The
accepted streak therefore remains 0/25.
The retained Playwright trace localizes the first failure before navigation or
any project RPC. The fixture project
prj_9860ba1c2eb34ad39cd4bcc76663fbce was created, described, and made ready
successfully. Immediately afterward, connectAdminItx failed its initial
WebSocket dial. Exact-version Cloudflare traces contain the successful fixture
creation and simultaneous successful sessions from other tests, but no Worker
invocation matching this failed dial. The connection therefore died locally
or at the edge before the OS Worker accepted it; the named stream assertion
never ran.
PostHog confirms that the victim test is incidental. In the available history,
the exact generic connection error occurred three times across two unrelated
Playwright files: this stream control case and two REPL catalogue cases on
other branches. All three use the shared connectAdminItx helper and all three
passed on one framework retry. The stream control case itself had 196 recorded
attempts across 186 workflow runs; its three other historical failed attempts
were distinct stream-wait timeouts, not this transport signature.
The shared Playwright helper now uses the same bounded connectItxReady
boundary already proven for the CLI. It may make one fresh dial only before a
Cap'n Web session exists, so authentication and test operations cannot be
replayed. Project-fixture creation uses the same safe boundary before issuing
Project.create. Any recovery adds an itx: initial connection retry
Playwright step, a test annotation, and the structured
[itx-initial-connection-retry] diagnostic with the original error and
timings. Those records flow through the canonical artifact into PostHog, while
the marathon ledger explicitly counts the structured marker and rejects the
run. A second dial failure and every post-session failure remain authoritative.
Round 15 orphaned append, Artifacts 503, and signup recovery#
Canonical exact-head run hrtfl6plzr (attempt dhkmkf2p26) on
865060fa5644bf2703130611cb9a0d3bf6ee4431 completed in 302 seconds, but is
rejected with three framework retries. The accepted streak remains 0/25.
Artifacts and exact-version Cloudflare telemetry separate the three causes;
none was fixed by increasing a test or project-creation timeout.
The Streams Example relative-path case lost the native Durable Object RPC acknowledgement for its first append. The call stayed unresolved for the test's full 30-second budget, then completed after Vitest had already timed out and continued leaking the abandoned attempt's later appends. The append was not CPU-bound and the retry completed the complete case in about six seconds. Every path-resolution append now carries an idempotency key, and the shared Stream RPC target gives keyed native append attempts a 10-second acknowledgement deadline. A silent attempt is observed and disposed if it eventually returns, while the existing availability boundary performs exactly one replay with the same keys. Unkeyed appends are never deadline-replayed.
The clean-close Playwright case never reached its named assertion. Its fixture
project prj_e8fece330f4b46b49f812ab0ade21804 completed identity, root birth,
and every sibling birth barrier, then waited 99.201 seconds for
project/ready. The retained journal supplies the durable explanation:
/repos/config recorded repos/create-failed with
HTTP Error: 503 Service Unavailable at offset 9. The Artifacts client had
flattened the transient response into a plain Error, so the repo processor's
code/name-only classifier permanently poisoned the repository and no
repos/created certificate could make the project ready. The classifier now
recognizes only transient Artifacts HTTP statuses (including the live 503
shape) through wrapped causes and re-enters ordinary durable redelivery.
Domain HTTP failures such as 404 still settle fail-closed. Focused recovery
tests prove that the 503 writes no failure fact and a successor attempt writes
one terminal repos/created fact.
The new-user UI case exposed a separate navigation defect. The root
deployment-status probe returned unknown, so the root decision sent the user
to /projects. That page successfully created project
prj_445316f275f34223b49486ebf5f8346b and made it ready in about 4.9 seconds,
but remained on the list until the 60-second composer assertion expired. A
single unknown project now follows the same welcome destination as a missing
project. Server-side nonblocking birth is attempted first; if its probe or
birth call fails, an explicit ensureBirth handoff lets the authenticated
welcome page issue the same idempotent nonblocking create once. The user is no
longer stranded after successful recovery.
Focused tests (65 cases), both affected package typechecks, targeted lint and format checks, and the full OS unit suite are green locally. This is diagnostic and regression evidence only; a new immutable head must restart the canonical 25-run proof.
Round 15 orphaned wake settlement and false root-append deadline#
Canonical exact-head run mf4v6fm81q (attempt xswl4x4d5w) on
420eac47ba9c853f5cad15a429aa6a59eada4909 completed in 331 seconds and all
tests eventually passed, but the run is rejected because it used three
framework retries. The accepted streak remains 0/25.
The worker-alarm retry exposed a mistake in the preceding keyed-append hardening. Its first project continued through birth and became ready, while the public root append exhausted two artificial 10-second acknowledgement deadlines. Root birth is a larger durable saga whose callers already own explicit 90–100-second end-to-ready bounds; imposing the ordinary stream append deadline inside that saga creates a false failure under load. Keyed root appends are therefore excluded from deadline replay. Keyed non-root appends retain the bounded recovery, and unkeyed appends remain non-replayable.
The repo-commit retry was not a slow commit. On its first pooled project,
prj_66837522b9fd4a71bb551fb96a7c9205, durable execution
494d2b81-4750-4e59-b6d1-ae75209c6501 was requested at
16:52:08.683Z, but the capability-host processor did not record
script-run-started until 17:01:42.665Z; it settled successfully at
17:01:46.475Z. That is a 9-minute-34-second delivery gap before user work
began. The retry used replacement project
prj_f906e62453b949d6b669724b90addc45, started in 838 milliseconds, and
settled in about 4.4 seconds.
The gap came from an unbounded transport state in the wake lane. Wake delivery
is deliberately one-way and reports its acknowledgement through an independent
settleDelivery capability. A lost settlement callback that also failed to
raise onRpcBroken left the predecessor connection authoritative until the
roughly ten-minute idle teardown, so its durable cursor could not be
redelivered. Each pending settlement now arms the Stream Durable Object's
native alarm for 20 seconds. Expiry is an observable
StreamReceiverUnavailableError: the cursor stays put, bounded backoff is
recorded, the orphaned connection is closed, and a successor re-pokes from the
durable checkpoint. Late predecessor settlements are fenced. The deadline uses
the native alarm rather than an actor timer, so it does not retain the current
JS-RPC turn.
The third retry was a dynamic-worker runtime failure with platform reference
efqgrnq1347q3bs8in6q386a. Durable execution
9578a99f-b22b-42e5-bf7b-5ea8d9ec00dc started normally and settled failed
about 2.1 seconds later; its replacement test project passed. This is not
silently reclassified or generically replayed because runScript may contain
mutations. It remains a proof target: recurrence in the restarted marathon
must be localized below the user-code boundary before any recovery is added.
The two new regressions pass with the complete 33-file Streams unit set (381 passed, one expected failure), the affected app typecheck, targeted lint, and formatting checks. This rejected run resets the proof to 0/25; only a new immutable head can restart it.
Round 14 (2026-07-23, post-#2275)#
PR #2275 made preview worker builds fail closed when the dependency installer
returns warnings instead of an executable bundle, and made deployment wait in
parallel for the exact SHA-pinned pkg.pr.new SDK package. Its final exact-head
preview check ran every runnable suite in 240 seconds, and PostHog's finalizer
matched all 6/6 expected sources. One OS Vitest case nevertheless passed only
after retry, so the run is rejected and the strict counter remains 0/25.
The retry was Project worker processEventBatch receives events from every project stream and can cross-post. Its first project failed the public
15-second create deadline after 14.308 seconds of the remaining ready budget;
the whole-test retry created a different project and passed. This was not a
near-miss followed by harmless background completion. Exact-version trace
4d603e1afeb62f5f959255504b895244 shows the abandoned first project's config
repository processor still waiting at offset 7 and eventually reporting
waitUntilProcessed timed out after 59517ms. Its terminal stream appends
completed around 65 seconds after Project.create began.
PR #2273 had introduced an inconsistent timeout hierarchy: a 15-second public
project-create deadline wrapped a 60-second sibling-birth barrier inside a
75-second processor acknowledgement, and explicitly expected a whole-test
retry to redial. The corrective change removes that retry-dependent contract.
Sibling birth now owns one shared 75-second budget, the Project processor owns
90 seconds, and the public entry-to-ready operation owns 100 seconds. Healthy
creates still return immediately; the original caller now observes either the
original fully ready project or one bounded failure. The existing
os-cold-create-latency task retains the separate obligation to eliminate this
tail rather than treating the upper bounds as latency targets.
The corrective PR's first exact-head preview check was technically green but
is also rejected. It took 314 seconds and
catalogue example "provide-live-flattened" runs identically across runtimes
passed only on Vitest's second attempt. The first attempt's spawned CLI exited
with WebSocket connection failed; the exact-version Cloudflare trace set has
no corresponding accepted /api request or server error. That localizes the
failure before the server accepted the WebSocket upgrade, not in the named
catalogue example.
PostHog bounds both recurrence and scope. The named case ran 191 times in the
available window and retried once, but the matrix as a whole recorded 38
retries spread across unrelated examples. PR #2169 recorded the same spawned
CLI signature on repo-read-file. The example names are incidental: every CLI
case inherited one unobserved, one-shot initial transport dial.
The fix is below the test and above user code. The Node client can wait for the
initial WebSocket open event and, when explicitly requested by the CLI, make
exactly one fresh dial after a failed upgrade. This boundary ends before a
Cap'n Web session, authentication call, or user operation exists, so it cannot
replay side effects. The CLI emits a structured
[itx-initial-connection-retry] record with the original transport error and
timings; the matrix turns that into a PostHog e2e-phase retry annotation.
Application and RPC failures after connection are never retried.
Local transport tests prove first-upgrade failure then success, no retry after
an application failure, and a hard two-dial ceiling. Against the rejected
run's exact OS Worker version, all 27 catalogue examples passed with
--retry=0 in 67.68 seconds and emitted no connection-retry record. The
probe-based draft took 74.93 seconds; it was discarded because an extra RPC
round trip was unnecessary. The strict full-pipeline counter remains 0/25
until a new immutable head completes without any framework or transport retry
and below the seven-minute ceiling.
Round 13 (2026-07-23, post-#2271 and #2273)#
PR #2271 removed a cyclic Durable Object RPC lifetime from durable stream wake delivery. Wake batches are now one-way and carry an independent one-shot settlement capability. Missing settlement leaves the durable checkpoint authoritative, delivery failure and broken transport share the same bounded backoff/park machine, and late signals are fenced to the connection that created them.
Its final exact-head preview ran every canonical suite in 227 seconds with zero
failures or absorbed retries: Depot workflow 1w2np2p7w2, job 2xswh11402,
attempt 6kl9w6n7m7. PostHog recorded 3,342 passing final test records, seven
pre-existing explicit skips, and no failed or retried records. The formerly
65-second journal-is-the-record Playwright case completed in 11.232 seconds;
its Vitest equivalent completed in 7.639 seconds.
The first post-merge proof head 31f072d6… then produced two clean runs (285
and 230 seconds) before run 3 absorbed one retry in workspace.itx.e2e.test.ts
after Cloudflare returned opaque internal reference
jofnmc53etthqc74kfntuo36. The harness rejected that otherwise-green
234-second run. A fresh proof produced two more clean runs (245 and 231
seconds), then run 3 failed in 311 seconds with 13 retries and three hard
project-creation failures.
PostHog and exact-version Cloudflare traces localized that failure to the
Artifacts create/read consistency boundary. Three independent config
repositories reported an internal create error, then ALREADY_EXISTS, while
immediate reads still returned repository-not-found. Failed-run
artifact-get-or-create p95 was 39.54 seconds versus 2.59–3.16 seconds in the
adjacent clean deployments, and 30 downstream durable stream deliveries failed.
PR #2273 removed that race. The authoritative initial write token returned by
ARTIFACTS.create() now seeds the repository directly; the normal path no
longer performs an immediate get() plus createToken(). Only the ambiguous
ALREADY_EXISTS case uses one explicit, bounded recovery barrier. Public
project creation now has one 15-second entry-to-ready deadline, allowing the
framework's single test retry to redial while already-committed durable work
finishes.
The final #2273 exact-head preview was genuinely green in 247 seconds: all five
apps deployed, all 310 tests ran, and none failed. Two explicit
stream-wait-timeout outcomes recovered on the single test retry, so that run
accepted the product fix but does not count toward the zero-retry streak.
PostHog's finalizer matched all 9/9 expected sources. Exact-version Cloudflare
traces showed 20/20 successful artifact-get-or-create operations (p95 2.237
seconds, max 4.527 seconds), no normal-path artifact-token call, no durable
sink delivery failure, and no Artifacts error.
The strict consecutive counter now restarts at 0/25 on a fresh immutable proof
head based exactly on merged origin/main at
b5fbfdee44234c69226cad52b14fb0440c142e13.
Round 12 (2026-07-23, post-#2269)#
This round starts from origin/main at
c5d51cf02e0c1acf9a36bec903ecf956ae9469ae. PR #2269 paid off three
independent sources of preview tail latency and absorbed retries:
- project-directory registration now retries only the missing slug or project-index write, so a partial KV failure cannot replay a successful sibling write;
- agent smoke consumes the deployment-wide rollout deadline immediately before project creation instead of beginning a second rollout wait; and
- stream-backed read-your-writes paths no longer perform an unbounded, redundant processor catch-up before entering the existing offset wait that owns the explicit 15-second deadline.
The final exact-head check for #2269 ran all five app suites in 231 seconds with
zero retries: Depot run b2jwhtx6q4, attempt nvg49cr0jp. OS Playwright passed
63/63 and the formerly retrying repository-edit and secret write-fence cases
both passed on their first attempts. PostHog recorded 310 preview logical-test
outcomes and 3,046 unit outcomes with zero failures or retries, and both
telemetry finalizers reported complete expected source coverage. Cloudflare
traces showed no wall-time exhaustion, resource exhaustion, or unexplained
Durable Object reset in the test window.
That run was PR acceptance evidence before the squash merge, not part of this post-merge consecutive proof. The round-12 counter therefore starts at 0/25 on this new immutable PR head.
Round 12 rejected attempt: self-alarm readback races a due alarm#
Run 1 on the initial proof head was clean in 281 seconds (Depot run
zq6s3vngt8, attempt sslv41jkrl). Run 2 completed in 250 seconds but
stateful-worker-alarm.e2e.test.ts failed once and passed on Vitest's single
retry (Depot run fgl4xv672n, attempt pf1mx7sj0s). The harness rejected the
run and reset the streak to 0/25 even though GitHub's ordinary check was green.
The failing assertion expected armSelf(3_000) to return an armed timestamp
but received null. This was a test race, not a missing alarm: the worker
computes the due time before ctx.storage.setAlarm crosses the facet, project
itx, and outer StatefulWorkerDurableObject; its following
ctx.storage.getAlarm is another traversal. Cloudflare traces show the
self-arm invocation already in flight by 04:09:16.843Z, while the outer
Durable Object did not receive setAlarm until 04:09:19.318Z. About 2.5 of
the test's 3 seconds had therefore elapsed before the real alarm was armed,
leaving its readback free to race normal alarm consumption.
PostHog shows 131 executions of this test since 2026-07-22: 129 passed first try and 2 absorbed a retry. The normal duration was 38.7 seconds at p50 and 45.8 seconds at p95; the earlier retried execution took 142.4 seconds and this one took 75.3 seconds. The fix separates the two contracts already present in the test. The first alarm continues to prove delivery, deliberate handler failure, and native retry. Self-addressed arming now uses a safely non-due alarm, proves the worker-side and host-side readbacks equal the exact scheduled time, and immediately disarms it. A second near-term fire added no distinct platform coverage and made a due timer the synchronization primitive.
Round 12 proof restart after #2270#
PR #2270 merged that alarm-test fix to main at
78ba8ad61b3eeeba0cbdd77193510061f8466865. Its final exact-head preview check
ran all five app suites with zero failures and zero retries in a 254-second
Depot run envelope: run gdkz9dw86w, attempt c7zv439zvz. The changed alarm
test passed first try in 33.4 seconds.
PostHog recorded 310 preview outcomes and 3,046 unit outcomes on that head with
zero failures or retries. Both finalizers were complete: the preview job found
all 9 expected artifact sources, and the unit job found all 10 expected
workspaces. The depot, github-actions, and github-reviews source syncs
were all fresh and healthy.
That run accepted the fix before its squash merge; it is not part of the
post-merge consecutive proof. The strict counter restarts at 0/25 on a fresh
PR head based exactly on 78ba8ad61b3eeeba0cbdd77193510061f8466865.
Round 11 (2026-07-23, post-#2265)#
This round starts from origin/main at
56fdbd4e447ab53f3011a60417349f76223db30d. PR #2265's final exact-head
canonical check ran all five suites successfully in 291 seconds from pickup to
finish (the GitHub check itself reported 4m40s): Depot run
79jc4l57lt,
attempt c2mnb0ph28. Playwright had zero retries, but OS Vitest absorbed one,
so this is a useful ordinary green and a rejected formal proof; the accepted
counter remains 0/25.
The retry was itx expression replacement records the recipe without evaluating it. Its first attempt failed during fresh-project creation with
stream-unavailable: Durable Object is overloaded. Requests queued for too long.; the full-test retry created a second project and passed. PostHog records
153 executions of this exact case over the preceding 30 days and only this one
retry (0.65%). Duration was normally 9.0s at p50 and 14.9s at p95; the retried
run took 19.3s. That history does not justify quarantine, serialization, or
project reuse.
Cloudflare traces locate the first failure in project
prj_d2cb39d19139488fba1c5236b17d61da, not in deployment-global state. Its
root Project Durable Object repeatedly rejected delivery while
ProjectProcessor.#createSiblingProcessors waited for the root capability-host
birth. The stream sink durably backed off and re-poked the subscription, and
the same project subsequently reached ready about nine seconds after
registration. The caller nevertheless failed earlier: the processor relay
performed its one immediate lifecycle retry, received another availability
rejection, and threw even though waitUntilProcessed still had most of its
75-second public deadline available.
The round-11 fix makes only that idempotent wait operation reacquire a fresh processor facade after repeated availability failures. It retains one caller deadline, applies exponential backoff capped at one second, disposes every transient facade, and fails when the deadline expires. Application errors still propagate immediately, and non-wait processor calls keep their existing single retry. Focused tests cover recovery after more than two availability failures, decreasing timeout propagation, application-error fail-fast behavior, and bounded exhaustion. This pays off the product-layer wait contract instead of adding a test retry, longer timeout, quarantine, or global Vitest throttle.
Round 10 (2026-07-23, post-#2263)#
This round starts from origin/main at
7b41300708f49146603fea09766fca64a07f1eb8, including the lazy repository lane
from PR #2262. PR #2263 fixed the false-green
boundary exposed by PR #2262: a head that selected no deployment work reused an
older head's test result and returned success in six seconds. Deployment reuse
is still allowed, but every triggered PR head now executes every runnable app
suite against the exact recorded Worker versions. An empty runnable set fails
loudly with E2e was NOT run instead of becoming a skipped success.
The repaired #2262 check at head 1ef0f722… performed all five app suites and
passed in 3m37s with zero retries. Its later head 6f1d610f… then provided an
important real failure rather than another false green: Playwright and all
non-OS suites passed, while six OS Vitest cases retried and one still failed.
The failures spanned unrelated project, agent, MCP, egress, worker-readiness,
and sandbox cases. One error explicitly said Durable Object reset because its code was updated; another script execution was orphaned because its
incarnation disappeared. All affected tests owned distinct projects.
PostHog places the OS deploy-phase finish at 23:05:30.758Z and the Vitest
start at 23:05:51.367Z: only 20.6 seconds separated edge readiness from the
full fan-out. Exact-version readiness itself took 58ms. The explicit reset
arrived about 51 seconds after Wrangler uploaded the version. Cloudflare's
known-issues documentation
says Worker/DO code propagation is globally eventually consistent for seconds
to minutes, while the
Durable Object lifecycle
documents that a code update shuts an object down and can terminate in-flight
RPC. Exact edge-version health is therefore necessary but cannot certify every
future Durable Object placement.
A control on the next #2262 head, 410c1774…, was genuinely green in 3m48s:
all five suites ran, OS Playwright passed 63/63, OS Vitest passed all 170
reported cases, and raw telemetry recorded zero retries. On that run OS
readiness naturally consumed 31.1 seconds before the smoke's 22.6-second path
to Vitest, giving the deployment about 53.7 seconds of age before fan-out. That
does not prove 54 seconds is a safe boundary, but the contrast localizes the
failure cluster to the fresh global-rollout window rather than shared project
state or one victim test.
Round-10 preflight ledger:
| Proof | Revision | Accepted runs | Retries | Outcome |
|---|---|---|---|---|
| Repaired #2262 full check | 1ef0f722… |
0/25 | 0 | Real five-suite pass in 3m37s |
| Fresh-rollout failure | 6f1d610fdea1521ca93a6e5c4a3bcdee203e5dc0 |
0/25 | 6 | OS Vitest failed after rollout-reset cluster |
| Natural-readiness control | 410c1774d8b0a614096d89dcfe39c1e44359a9b6 |
0/25 | 0 | Real five-suite pass in 3m48s |
The smallest deterministic CI boundary is a 90-second minimum age from the successful OS deploy command to high-fanout Vitest. The clock starts before exact-version readiness and runs concurrently with readiness, agent smoke, Chromium installation, Playwright, TUI, and every other app suite; reused older deployments wait zero seconds. Playwright remains on the critical path in the observed runs, so the clock should add little or no wall time. This is a visible bounded lifecycle gate, not a retry, test quarantine, synthetic Durable Object sampler, or serial deployment barrier. The accepted counter remains 0/25 until the new immutable PR head completes the canonical marathon.
The first formal head then passed four consecutive full runs in 231–241
seconds with zero retries. Run 5 took 361 seconds and was rejected because two
Playwright cases needed their permitted retry. Both failed before their named
stimulus: one fresh agent feed never painted its reply, and one REPL
case timed out inside project creation. Cloudflare telemetry for the first
project records successful birth/readiness followed by durableObjectReset
warnings on its stream sink at 00:05:00Z and 00:05:04Z. Its creation began
before the 90-second clock expired, because only Vitest consumed that boundary.
The follow-up keeps the Playwright process parallel but passes the absolute rollout deadline into its project helpers. Forged-session creation and the real email-signup form wait only immediately before creating a project; Chromium, page setup, authentication, browser-only specs, smoke, TUI, and other app suites continue to overlap the rollout. The rejected 4/25 streak remains diagnostic evidence only; the accepted counter restarts at 0/25 on the new head.
PR #2265's first formal head, e5e9990986834b9c3c340e746b626ba0974e96bb,
then passed one complete run in 285 seconds with zero retries. The next run was
rejected at 372 seconds after three permitted test retries exposed two distinct
defects:
- Semaphore and Streams addressed old Durable Object versions after their new edge Workers were ready. Semaphore traces contained both the prior and new code versions; Streams reported an explicit code-update reset. The 90-second gate had covered only OS, so the fix applies the same per-deployment boundary to every live suite that calls Durable Objects. The short app commands wait at their own boundaries while all app lanes remain concurrent; OS still overlaps browser/auth setup internally.
- Playwright's empty-agent-feed case timed out while creating project
prj_ddc2a50bba2941878bed307b207417ea. Traceeb26b8dc5ae96c5da9373abef6e8606dshowsProject.createalive until the 90-second caller watchdog canceled it. The durable event history is conclusive: config-repo creation began, Cloudflare Artifacts returnedINTERNAL_ERRORafter 32.7 seconds, and the repo processor journaledrepos/create-failed. Norepos/createdor terminal project creation fact followed, even after later wakes. The Playwright retry created a different project and passed, masking the permanently poisoned first project.
INTERNAL_ERROR and UPSTREAM_UNAVAILABLE are Artifacts service-availability
outcomes, not invalid repo requests. Repo creation now leaves its existing
idempotent durable obligation open for redelivery on those two codes, exactly
as it already does for a Durable Object lifecycle reset or an Artifact that is
still materializing. Input/domain errors still append terminal
repos/create-failed and remain fail-closed. Focused processor tests prove both
classifications and the failed-attempt → redelivery → one repos/created
recovery path. These fixes change the immutable head, so neither formal run is
counted and the accepted streak restarts at 0/25.
Round 9 (2026-07-22, post-#2260)#
This proof starts from origin/main at
7ad6d92037663ac2aba0d0b15ba9550c2a0f685b, after deploy-phase telemetry
(#2254), the zero-budget worker-build cache race fix (#2256), sandbox RPC
settlement lifetime hardening (#2257), and the bundled REPL TypeScript runtime
(#2260). PR #2261 carries the immutable proof head. The accepted counter starts
at zero.
Round-9 run ledger:
| Proof | Revision | Accepted runs | Retries | Outcome |
|---|---|---|---|---|
| Round-9 marathon 1 | 6a7e1d0956e0fc64c8913167318442752df975ba |
0/25 | 4 | Functional pass; rejected at 363s |
| Round-9 marathon 2 | 6a7e1d0956e0fc64c8913167318442752df975ba |
0/25 | 0 | Clean functional pass; rejected at 350s |
| Round-9 marathon 3 | 64bc519b4d68d3d8fad6c4337d0ad8a137aa2332 |
0/25 | 9 | Failed OS Vitest during rollout at 270s |
The first attempt was Depot run
nfwh10j558.
All apps eventually passed, but four unrelated cases retried: agent delegation
lost its CapabilityHost incarnation, direct MCP lost its connection, a project
stream subscription exhausted its 120-second outer watchdog, and the browser
half-open test did not paint an already-durable assistant event before its
30-second UI wait. Running each exact case five more times against the unchanged
warm deployment produced 20/20 first-attempt passes. That control does not count
toward the streak; it localizes the shared cluster to the fresh-deployment
window rather than four deterministic victim-test defects.
The second attempt was Depot run
bj3zqplkn6.
Every test passed on its first attempt, but the strict loop rejected the
350-second wall time. The normalized phase evidence accounts for the whole
critical path: roughly 40 seconds of pickup/setup, 158 seconds to the global
deployment barrier, 143 seconds of tests, and 7 seconds of finalization. OS
readiness alone took 72.3 seconds; streams readiness took 136.6 seconds. During
those waits the active probes received Cloudflare 1101/HTTP 500 responses and
later 503 probe-timeout responses even after all ten waves had previously
reported ready.
The readiness implementation was stale relative to the contract already
documented in ci-preview-performance.md. OS created 25 synthetic Durable
Object identities per wave across ten waves, then slept ten seconds and checked
the complete set again: up to 500 Durable Object RPCs per deploy. Streams did
the same with eight identities per wave: up to 160 more RPCs. Besides dominating
the deploy tail, this finite placement sample cannot prove the rest of the fleet
and did not prevent attempt 1's real project identities from encountering
lifecycle transitions.
PostHog confirms this was a fleet-wide tail rather than one bad runner. Across the preceding 24 hours, 30 OS deploy lanes spent a median 61.3 seconds in readiness (p90 93.9 seconds; max 118.8 seconds), while 22 streams deploy lanes spent a median 56.0 seconds (p90 93.5 seconds; max 136.6 seconds).
PR #2261 therefore removes the synthetic Durable Object gate, its authenticated
probe protocol, and its orchestration/test machinery. Readiness is once again a
cheap public request that must report wrangler's exact CF_VERSION_METADATA
version; the first exact match releases the test barrier with no artificial
dwell. Product operations remain responsible for explicit Durable Object
lifecycle outcomes, as the existing performance contract requires. This change
resets the accepted streak to 0/25; only a canonical Depot run on its new
immutable head can restart it.
The first canonical run after removing the probes, Depot run
69ccf2rkj7,
made the remaining boundary precise. All five deploys passed and every non-OS
suite passed. OS Playwright passed 63/63 and the agent smoke completed on
its first attempt in 26 seconds, but nine OS Vitest cases entered retry; eight
recovered and the preview-smoke case failed both attempts. Every affected test
was scheduled within the first second of Vitest. The errors were rollout-wide
(Durable Object reset because its code was updated, failed WebSocket
connections, and internal references), while files scheduled later had no
retry. This is a fresh-deployment fan-out race, not nine independent test
defects.
The minimal boundary is now the existing production-shaped agent smoke, not a synthetic fleet sampler. Chromium installation, the smoke, and TUI start immediately; Playwright starts as soon as Chromium is ready; high-fanout Vitest starts only after the smoke has successfully created a real project and served its project/agent/stream flow. The 26-second propagation window remains hidden under the 139-second Playwright critical path, adds no arbitrary sleep or retry, and fails fast if the real canary cannot complete.
Round 8 (2026-07-22, post-#2251)#
This is a fresh proof from origin/main at
cd5f4a67b9df5bc05fd2e38fe5ea9eda63bd6e7b, including the cold worker-build
handoff from #2251 and the fixed durable-stream telemetry from #2249. It
inherits no streak: the accepted-run counter starts at zero.
The final #2251 head, a0380f8d078ddab6a825f9a070b2a8f433d5e7e4,
passed its normal preview check on the first workflow attempt. The complete
check took 5m20s. OS deployment was the critical deployment lane at 148.9s,
while OS e2e took 133.7s: Playwright passed 63/63 tests in 2.1m and Vitest
passed 48 files, with 2 skipped, in 78.98s. This is useful green baseline
evidence but not an accepted marathon run because it exceeded five minutes
and absorbed one Vitest retry.
The retried case was Project worker processEventBatch receives events from every project stream and can cross-post. Its first attempt reached the
30-second stream-wait watchdog and its retry passed, bringing the case to
59.18s. This first occurrence is recorded rather than quarantining a
substantial concurrency test from one observation. Any recurrence in this
round rejects the run and triggers diagnosis; a test shown to be independently
flaky or pathologically slow will be fixed or quarantined with a tracking task
under the normal testing policy.
Round-8 run ledger:
| Proof | Revision | Accepted runs | Retries | Outcome |
|---|---|---|---|---|
| Pre-round normal preview | a0380f8d078ddab6a825f9a070b2a8f433d5e7e4 |
0/25 | 1 | Passed in 5m20s; cross-project stream retry |
| Round-8 marathon 1 | e301520145a6259a743a3e300366a0a53689a009 |
0/25 | 2 | Functional pass; rejected at 311s |
| Round-8 marathon 2 | c2a695fac7279da1b3b9bb64512a2d08d20aa576 |
0/25 | 0 | Clean pass; rejected at 311s |
| Round-8 marathon 3 | 723f73067c13fa52512593f9e2952ffc91618ef2 |
0/25 | 19 | Shared-isolate cancellation; rejected |
The first round-8 attempt was Depot run
3jp43c0dbg.
Every test eventually passed, but the proof stopped because the workflow took
311 seconds and absorbed two retries. OS deployed in 101.7 seconds and its e2e
lane took 152.6 seconds. Vitest passed 48 files, with 2 skipped, in 86.15
seconds; Playwright passed 63 cases in 146 seconds.
The project-worker cross-post case again exhausted its 30-second stream wait
before passing on retry, reaching 62.79 seconds across both attempts. This
second consecutive occurrence establishes a cold-build delivery tail rather
than a one-off test failure. The test still exercises a fresh worker build and
is not quarantined; its public delivery budget is temporarily 100 seconds, and
tasks/reduce-project-worker-cross-post-tail.md tracks instrumentation,
latency reduction, and restoration of the tighter budget.
The other retry was the markdown-preview Playwright spec. The network trace
showed the parent project route's identity beforeLoad taking 1.79 seconds
after the Preview search-param navigation. The test selected Code while that
first transition was still settling; .cm-content remained absent for roughly
1.3 seconds, then the editor was fully rendered in the failure screenshot
about 100 milliseconds after the action deadline. The IDE now renders a
visible, data-spinner-marked status while TanStack Router is loading. This
exposes the real product wait and lets the normal spinner-aware action deadline
cover it; the substantial markdown editing and sanitization spec remains
active.
This head therefore contributes zero accepted runs. The next immutable head starts a new 0/25 streak through the same canonical Depot workflow.
The next canonical attempt on that head was Depot run
w9sc1f40ds.
It was the first complete zero-retry pass in this round: all 48 runnable OS
Vitest files and all 63 Playwright cases passed on their first attempt. OS
deployed in 116.2 seconds and its e2e lane passed in 146.9 seconds (Vitest
113.08 seconds; Playwright 2.3 minutes). The strict proof still rejected it
because dispatch creation through completion took 311 seconds.
Reporting accounted for the avoidable final tail: normalization produced
6,865 PostHog events, then the uploader sent 69 fixed 100-event batches
strictly sequentially, consuming 12.9 seconds before artifact collection.
The uploader now packs events by encoded size, with a conservative 5 MB event
payload budget beneath PostHog's 20 MB batch-request limit. The preceding
5,906-event artifact packs into three requests instead of 60 while preserving
every deterministic event UUID and the existing per-batch bounded retry. The
next canonical run, Depot run
h95t4wvm5n,
confirmed the real upload tail fell by roughly 10 seconds. The complete check
still took 312 seconds and absorbed 19 Vitest retries, all from one synchronized
Peer closed WebSocket: 1006 wave; every retry passed.
Cloudflare traces identified one causal test rather than 19 independent
flakes. The live-capability WebSocket boundary probe caught its expected
serialization error and reported a pass, then the runtime canceled that
session's root GET /api because the Worker had hung and would never generate
a response. Three isolated apparent passes reproduced the hidden runtime
cancellation three times out of three. Because every Vitest file correctly
runs in parallel against the same OS deployment, that isolate cancellation
severed unrelated sessions across otherwise-independent projects.
Both cases in live-capability-websocket.e2e.test.ts are explicitly
quarantined under
tasks/quarantined-live-capability-websocket-e2e.md. The causal boundary case
is deterministically runtime-fatal; the second was a test.fails that stopped
on an unrelated stale-template assertion and therefore provided no active
coverage. Ordinary live capabilities, project-app WebSockets, and all other OS
Vitest files remain enabled. This quarantine resets the immutable head and the
accepted streak to 0/25; only a canonical Depot run can restart it.
Round 7 (2026-07-21, post-#2226 and #2227)#
This is a fresh proof from origin/main at
767baafc35c774f48916a15278659e61dd8c9670, after unified CI/test telemetry
(#2226) and immediate OS Vitest file scheduling (#2227) merged. It inherits no
streak: the accepted-run counter starts at zero.
The exact #2227 head, 220ab10c9fd2ea51d4cd15ac4a0659fdf7562ed1,
passed its normal preview check on the first workflow attempt. The complete
GitHub check took 4m59s, including pickup, setup, deployment, tests, and final
bookkeeping. OS deployed in 115.5s and its e2e lane passed in 158.9s. Within
that lane, all 48 runnable Vitest files were scheduled immediately and Vitest
finished in 78.28s, down from 181.77s with the former seven-worker queue.
One matrix case, run-script, passed on its permitted single Vitest retry
after the separate Node runtime reported WebSocket connection failed. The
run stayed green, but the retry is part of this round's evidence and will be
compared with subsequent telemetry before any harness change. No extra retry
layer or test-specific exception has been added.
Round-7 run ledger:
| Proof | Revision | Accepted runs | Retries | Outcome |
|---|---|---|---|---|
| Pre-round normal preview | 220ab10c9fd2ea51d4cd15ac4a0659fdf7562ed1 |
1 | 1 | Passed in 4m59s; Node transport-open retry |
| Round-7 marathon 1 | 210f7ef88d6d8170daaf893380bd54f0da0cb8c2 |
0/25 | 1 | Functional pass; rejected at 318s |
| Round-7 exact-head preview 1 | 210f7ef88d6d8170daaf893380bd54f0da0cb8c2 |
1 | 1 | Passed in 4m34s; example-app event-count retry |
The first round-7 marathon was Depot run
glfbndtvnx.
Every test passed, but the proof correctly stopped because deploy plus tests
took 318 seconds. OS was the critical app: deployment took 127.6 seconds and
tests took 162.2 seconds. Playwright dominated the test lane at 155 seconds;
Vitest took 77 seconds. The eight Playwright workers each performed between
133.6 and 150.0 seconds of work, so the queue was already balanced: merely
reordering files cannot materially shorten this lane. Depot's 16-core runner
also peaked at only 41.7% CPU and 15.0% memory, which does not support a larger
runner as the first optimization.
One Vitest case, MCP built-in connects directly and mounts as a described capability, passed on retry after Cloudflare reported Durable Object storage operation exceeded timeout which caused object to be reset. The same Durable
Object startup-reset class occurred on unrelated PRs #2224 and #2231, so this
is classified as a shared Cloudflare transient rather than a defect in the
victim test. Its one repository-level retry remains bounded and visible.
The unchanged head then passed its normal preview check on Depot run
r62fmqfqp5 in
4m34s. OS deployed in 94.6 seconds and its tests took 154.6 seconds, confirming
that rollout/readiness variance accounted for most of the rejected marathon's
33-second overrun. The streams example app absorbed one Playwright retry after
its scroll-away counter expected 83 events but observed 84; the retry passed.
This first occurrence is recorded rather than quarantined. Recurrence will be
fixed or quarantined with a tracking task under the normal testing policy.
Round 6 (2026-07-21, post-#2169)#
This is a fresh proof from origin/main at
a999a11a00987bd59694d79f20949353644f29f6, after PR #2169 merged. It does not
inherit a streak from that PR: the counter begins at zero and the complete
deploy-plus-test critical path is run 25 more times.
The exact final tested head before #2169's squash merge was
950a4f01726f52ec2fb185305dc4dcf9baa7745d. Its normal preview check passed.
Schema-v2 PostHog telemetry recorded a 187,448ms test operation, 306 logical
test results, 95 Playwright attempt results, 208 named test phases, and 57
module results. It also exposed one absorbed Vitest retry:
- lane: OS Vitest
- module:
apps/os/e2e/examples/examples-matrix.e2e.test.ts - test:
catalogue example "repo-read-file" runs identically across runtimes - first failure: the spawned CLI runtime lost its initial Cap'n Web connection
with
WebSocket connection failed. - retry: the complete matrix case passed in 30,008ms
The failure is not evidence against repo-read-file: the other runtimes
completed and the retry passed. It is a transport-open failure in the genuinely
separate CLI process. The CLI currently makes one connection attempt and the
single Vitest retry reruns the whole isolated case, which is the repository's
intended one retry layer. No nested transport or harness retry has been added.
The CI diagnostic already included the enriched
cli process failed — stderr: output; Node's execFile defaults to UTF-8
strings, so PR #2169's Buffer-output bot comment does not describe the observed
runtime and needs no speculative conversion.
An absorbed retry remains a green run under the testing policy, but it is never silent: every retry in this round is classified from telemetry plus CI logs and artifacts. Any reproducible product or harness defect gets a minimal fix on its own reviewed green PR before the streak resumes. The catalogue runtime matrix remains intact.
Round-6 run ledger (filled from the marathon's machine-readable summary):
| Proof | Revision | Accepted runs | Retries | Outcome |
|---|---|---|---|---|
| Pre-round normal preview | 950a4f01726f52ec2fb185305dc4dcf9baa7745d |
1 | 1 | Passed; CLI transport-open retry classified above |
| Round-6 marathon 1 | 443d7a49a6da759842248ce8b284c820db2a41ab |
0/25 | 0 | Functional pass; rejected at 312s |
The first round-6 marathon was Depot run
2b0d59sw92.
Every test passed without retry, but the proof correctly stopped because the
deploy-plus-test critical path took 312 seconds: 123.9 seconds deploying, then
188.4 seconds testing. OS Vitest was the test pole at 181.8 seconds.
Schema-v2 module timing showed why. The seven-worker cap made the final
33.7-second itx-egress.e2e.test.ts file wait 147.6 seconds before it could
start. The 48 OS module executions contained 1,112.7 seconds of remote work;
even perfect seven-worker scheduling has a 159.0-second lower bound. These
files create isolated projects and spend nearly all their time awaiting remote
operations, so CI now gives every file a worker immediately. The in-file limit
of two remains for the few cases that deliberately share a bounded project
pool. The next marathon measures whether the deployed platform has a real
capacity limit instead of encoding an assumed one in the local scheduler.
Round 5 (2026-07-21)#
This round starts from current main. Its baseline is PR #2140's exact tested
head 2d156e0c3: the complete preview job passed in 4m05s with zero test
retries. Deploys already start together, app e2e lanes start together, and OS
starts smoke, TUI, Vitest, and Playwright together. The remaining proof is
distributional: 25 consecutive full deploy-and-test runs, each under five
minutes, with every absorbed retry visible in the ledger and investigated.
Progress and failure diagnoses live in the active PR's comments; this section is updated with the final run IDs and outcome once the proof completes.
The first exact-head normal preview after the TUI fix was green but took 5m01s.
Its OS Vitest lane was the critical path at 187s: the suite still capped 48
isolated files at seven workers, and a stale 8s scheduling estimate started the
admin-project file late when it actually took 83s. The cap and estimated-time
sequencer are now gone. CI gives every file a worker immediately on a 64-core
runner, so the intended bound is the slowest individual file (plus its one
allowed retry), not several scheduling waves.
The first marathon attempt (wzg4nbj1j7) stopped on run 1 after both TUI
workflows hit their 45s watchdog. TUI Test 0.0.4 then reported an immediate
Worker terminated for each retry and wrote no trace. Source inspection found
two deterministic test-harness defects. The dynamic agent-path suffix made the
header long enough to clip the literal live label while the terminal was in
fact connected and fully rendered, so both tests waited on absent layout text.
Then the framework timeout terminated the per-file worker, but its retry loop
reused that same dead worker; trace persistence also runs inside the worker
after the test returns. The agent path is now a short constant within each
fresh project. The TUI lane launches its two independent workflows concurrently
in separate processes/projects, retries only the failed workflow at the wrapper
boundary with a new process/project, and uses assertion deadlines below the 55s
hard watchdog so ordinary failures persist terminal diagnostics and
per-attempt traces before process teardown. A direct preview-7 proof completed
both workflows on their first attempt in 8.6s max (19s including the one-time
package build and project setup).
Update (2026-07-21): the TUI lane was subsequently skipped entirely rather
than hardened further. A cleanup pass found the isolation harness still
fighting more tui-test 0.0.4 shared-global defects (the cwd transform cache
and the tmpdir zsh dotfiles folder both race across concurrent invocations),
and the TUI has known bugs and no users yet — so e2e/tui-test/run.ts is now
a no-op stub that reports an empty retry ledger, and the specs stay on disk
for a future revival. See the stub's header and
apps/os/e2e/tui-test/README.md.
Round 4 (2026-07-13/14, PR #1938)#
Goal: 25 consecutive green runs on Depot, re-validating the lane after a week
of heavy merging (subagents/unified messaging, stream metrics,
MCP OAuth, sandbox AI-gateway egress, …). The then-current method used the
since-retired nested preview-e2e-marathon.yml workflow against this PR's
leased slot, failing fast and fixing every root cause before resuming.
Result: 96 consecutive green runs in one night, zero test failures, zero flakes found — the goal met on the first marathon and re-proven three more times across six merged main heads. Detailed per-run log in the PR comments.
- marathon r4-1
r59ccf138r(head = main 0d53cbc7e): 25/25 green, 21:51–22:53 UTC, ~2.2–2.9 min/run. - marathon r4-2
dgnsqq9jng(merged main 9b9f3404d): 21 clean, then run 22 stopped in 4s — not a flake: the slot's semaphore lease had been claimed bymain-auth-rpc-security-cutover(the #1940 validation), whose deploy replaced the PR's apps on preview-3. The ownership guard inpreview testfired exactly as designed. The marathon claimed a fresh slot (preview-1) and moved on. - marathon r4-3
4xg33k0bf5(merged main ce18a7d79): 25/25 green, 01:09–02:17 UTC. Runs 24–25 slowed to ~6 min (os lane 372s/355s) with recovered agent-streamliveness probeWebSocket reconnects — the flake-23 pool-saturation tail signature (the pool rides at the per-typeSANDBOX_MAX_INSTANCESpreview cap; #1747 replaced the flat cap-150 with that table), fully absorbed by the reconnect/retry machinery. The next marathon's preflight redeploy flushed the pool and restored ~2.2 min/run pace from run 1, confirming the mechanism (a rollout resets assigned instances). - marathon r4-4
6q6j7wlz3z(merged main d36c2f38d): 25/25 green, 02:24–03:24 UTC, no tail slowdown.
Cost (measured; full breakdown in the PR): $54 for 101 lane executions ≈
$0.53/run — 92% LLM tokens (gpt-5.6-sol BYOK, $49.36 uncached), with the
AIG response cache absorbing 46.4% of requests ($42 saved); Depot 8-core
compute ~275 min ≈ $4.40.
Round-4 lessons (no test-failure fixes needed; the PR carries only the slot re-claim above plus a dead-code/doc-drift sweep from the round's follow-up reviews):
- The round-1..3 fixes have held through a week of heavy platform churn; the lane's stability is structural, not a lucky streak.
- A marathon can lose its slot mid-flight to a legitimate external claim;
the guard converts that into a clean fast stop. The loop now re-claims a
slot (full-fleet redeploy) and re-runs the interrupted run number uncounted,
capped at
MAX_SLOT_RECLAIMS(2) per marathon — no tests ran under the refusal, so this is environment re-establishment, not a retry layer. - Sandbox-pool tail pressure at the per-type preview caps is visible (slower runs, recovered liveness reconnects) after ~25 runs on one deploy but self-heals on redeploy and never failed a run; watch it if marathons grow past ~30 runs per deploy.
Run log#
Depot marathons (the counted lane), 2026-07-05:
- marathon1: 17 clean, run 18 failed — sandbox cold boot ~132s vs 120s budget (fixed: 180s, flake 20).
- marathon2
0rm2n02tk2: 4 clean, run 5 failed — flake 21 (blank route-pending panel). - marathon3
nqchbzzr63: 11 clean, run 12 wedged at the container instance cap — flake 23 (stopped containers never release their slots). - marathon4
xw5qzkt05d: 16 clean at ~65-90s/run (no cap slowdown — the flake-23 fix held; slot recycled at active:3-5 throughout), run 17 failed —stream-lifecyclehit a Cloudflare DO-storage fault TWICE (attempt 1 timed out, the retry gotInternal error in Durable Object storage caused object to be reset; reference = v6frpcasd5hp70rrhv37kmr4) during an active Cloudflare "network performance in North America" incident (preview DOs are ENAM). Fresh project per attempt, so two independent draws both faulted — platform weather, nothing app-side to fix; bumped CI retries 1→2 (vitest + Playwright) so a burst needs three consecutive faults to fail a run. - marathon5
wdq4c1mv2q: 32 clean (new record), run 33 wedged at the 600s watchdog — sandbox starts degraded as the instance pool saturated atassigned == 100even with destroy-on-idle (see the marathon5 addendum under flake 23). Preview cap raised 100 → 500 for the marathon, then reset to 150 after Cloudflare rejected 500 against the preview account memory quota. - marathon6
gb1g4sg7rs: 25 clean at a steady ~60-90s/run — cap 500 eliminated the saturation slowdown entirely (pool rode atassigned: 491with NO latency growth, where cap-100 marathons were at 3-5min/run by run 20). Run 26 failed on the lane's one retry-less gate:agent-smoke.ts— the agent didn't reply within 90s ("saw 0 events") and the remote timeout crashed the bare tsx process. Fix: the smoke now makes 3 attempts, each with a fresh session + project, matching the vitest lane'sretry: 2policy; a broken slot still fails all three inside ~5min. - 2026-07-07 quota correction: the preview cap was reduced 500 → 150.
Cap 500 helped a single marathon slot, but multiple preview slots at
standard-1exceeded the dev/preview account memory quota and blocked new deploys. Cap 200 fits a partially populated preview fleet, but not all nine preview slots once each carries the OS sandbox app. Cap 100 previously wedged atassigned == max_instances; 150 is the fleet-wide compromise until Cloudflare changes assigned-slot accounting. - marathon7
pvtfkq146g: 21 clean, run 22 failed instreams-example-app's vitest lane:Network connection lost.on a 392ms-old fresh WebSocket (edge blip) — and that suite had NO retry config at all. The os lane'sreactivity.spec.tsflaked the same run and recovered via its newretries: 2, proving the policy works where it exists. Fix: fleet-wide retry parity —retry/retries: CI ? 2 : 0added toapps/streams-example-app(vitest + playwright) andapps/semaphore(vitest), the last lanes without it. - marathon8
8vl4d0479f: 🏁 ALL 50 RUNS GREEN (14:57–16:31 UTC, ~1h34m, ~60-190s/run, zero failures, zero watchdog kills). The goal — 50 consecutive green full-fleet preview e2e runs on Depot CI — is met, on the same afternoon and preview slot where the lane could not string together more than a handful of runs when this hunt began.
Flakes found and fixed#
1. Leaked semaphore leases starve the slot fleet#
Found before the first e2e run: every slot was leased, but pr-1634 and
pr-1636 each held two slots while their PR bodies recorded only one. A
deploy run that is cancelled (cancel-in-progress on a rapid push) between
the semaphore acquire and the PR-body write leaves a lease no later run knows
about; the next run sees "no lease recorded" and leases a second slot. The
leaked lease blocks other PRs for up to the full lease duration, and their
deploys queue for 20 minutes then fail.
Fix: claimEnvironmentConfigLease now adopts any lease the semaphore already
attributes to the holder (re-issued under a fresh leaseId, same pattern as
lease repair) before acquiring a fresh slot. Guard test in
scripts/preview/preview.test.ts.
2. "The weird JWKS issue": OS teardown bakes JWKS against a parked auth#
Failure signature (Depot cleanup jobs, and any run sharing the window):
JWKS fetch attempt N failed, retrying: Error: HTTP 503 (x60)
Error: Forge key is set but the deploy-time JWKS fetch from
https://auth.iterate-preview-N.com/api/auth failed (HTTP 503). ... Aborting
Preview cleanup destroys all apps in one parallel batch. Auth's teardown
usually finishes first and parks its routes (#1622) — parked routes serve
503. The OS teardown then ran the deploy-time JWKS bake, whose
resolveStaticAuthJwks polls the slot's auth /jwks for 120 s before the
forge check aborts the process. Deterministic whenever auth's teardown wins
the race; audited 2026-07-03 Depot runs show it failing exactly that way
(e.g. run kq6qlp02c0, preview-4). The same poll-503-then-abort also shows up
on deploys when the slot's auth is genuinely broken — there it is a symptom,
not the disease.
Fix: the teardown path skips the JWKS bake entirely — a teardown has no worker to bake a key set into.
3. Signup/create-project specs: double navigation redeems the OAuth code twice#
The dominant fleet-wide Playwright flake ("locator.fill/waitFor timeout" on
signup.spec.ts and create-project.spec.ts) renders as a bare page reading
OAuth callback exchange failed: server responded with an error in the response body — after instrumentation (fix below):
[invalid_verification: Invalid code] (token endpoint HTTP 401).
Live worker tails showed the smoking gun: every UI login issued TWO
browser navigations to /api/iterate-auth/callback?code=… 1–2 ms apart
(adjacent cf-rays, both sec-fetch-mode: navigate), producing two
simultaneous token exchanges for a single-use code. better-auth's client ships
a default redirectPlugin that auto-navigates whenever a response carries
{redirect: true, url} — which oauth2.continue/oauth2.consent responses
do — while the auth SPA's mutation handlers ALSO window.location.href = result.url. Which exchange wins the D1 row delete and which navigation the
browser commits are independent races, so most runs pass and some render the
loser's 502.
Fixes:
apps/auth/src/utils/auth-client.ts:disableDefaultFetchPlugins: true— navigation after auth-client calls is now always explicit (the only caller that relied on the plugin, Google social sign-in, navigates manually now).apps/auth/src/lib/server.ts: the callback's 502 now includes the OAuth error code + token-endpoint status, so the next exchange failure is diagnosable from the Playwright screenshot alone.
4. Preview auth signing key changed post-bake (OS static JWKS went stale)#
auth.iterate-preview-2.com/api/auth/jwks served kid 884xFI… at 23:12Z and
kid YDmMHW… (created 23:22:41Z, mid-e2e, no deploy in flight) later the same
hour — the old row was gone from the jwks table while user rows survived.
better-auth never deletes jwks rows and no first-party code touches that
table; the strongest correlate is a Depot CI preview deploy that was cancelled
mid-auth-deploy in exactly that window. Root cause unproven; the blast radius
was total (OS verifies with a deploy-time-baked static JWKS, so a post-bake
rotation fails every verification until the next OS deploy).
Mitigation: createIterateAuth now falls back to the issuer's live /jwks
when the baked set has no matching kid (ERR_JWKS_NO_MATCHING_KEY), keeping
the baked set's zero-roundtrip fast path and the forge key intact.
Lab note: a sleeping laptop perfectly impersonates a broken slot#
Two full marathon runs "degraded" for 45–56 minutes with rotating 90 s
timeouts, 14–16 minute per-spec hangs, capnweb WebSocket connection failed,
and stream waits that saw events stop mid-flow — while every interactive probe
between runs was healthy. pmset -g log showed the Mac cycling through
13–17 minute Deep Idle sleeps exactly matching the hang durations: the loop
was running on a sleeping machine. The loop script now re-execs itself under
caffeinate -dims on Darwin. Lesson for anyone chasing "slot degradation" from
a laptop: check pmset -g log before blaming the server.
Recurred round-3 (r3d), new symptom: an overnight ~5.5h gap between the
warmup and run 1 (the machine slept despite caffeinate — a 43-minute
assertion had died) left the preview slot idle long enough that Cloudflare
de-provisioned the sandbox container image. When the marathon resumed, a
later run's sandbox-exec hit Container is currently provisioning. This can take several minutes on first deployment. (SDK 503, phase: provisioning) —
image provisioning exceeded the sandbox test budgets (Playwright
completionTimeoutMs 120s, vitest testTimeout 240s), failing both attempts.
This is distinct from flake 19's instance-cap "Container is starting": that was
too few slots; this is a cold IMAGE that must be re-pulled after a long idle.
The continuously-running r3c marathon (22 clean) never hit it — keep the
marathon continuous (machine awake, no multi-hour gaps) so the image stays
warm. If provisioning latency ever bites a genuinely continuous run, the fix is
to raise the sandbox-exec budgets to tolerate a cold pull, not more warmups.
6. Repo reads lose the read-your-write race against the Artifacts remote#
examples-matrix › repo-edit-file failed with the edit reporting success
(occurrenceCount: 1, changed path recorded) while the immediately following
readFile returned the pre-edit content. The Repo DO clones the
Artifacts git remote fresh for every read, and that endpoint is eventually
consistent after a push — a clone issued milliseconds later can serve the
previous HEAD. Same hazard applied to the worker-source projection refresh,
which could silently bake pre-push worker code even though the commit RPC
already resolved (the DO comments promise "commitFiles() is our
read-your-write boundary").
Fix: the Repo DO records each pushed commit oid per branch
(repo-pushed-head:<branch> in DO storage); read clones retry briefly until the
snapshot observes at least that head. (After the #1612 repo refactor this guard
lives in getFilesSnapshot, the single clone-and-read pathway every read goes
through; the old per-projection materialization is gone.) See also flake 15 —
serialized writes mean the recorded head can only move forward, never behind our
last push.
2026-07-17, explicit-births follow-up: requiring a mutation clone to see
the exact advertised head closed the replica-lag case, but a retries-disabled
preview run exposed a distinct writer. The failed repo stream had no user
commit fact, while its Artifact history contained two consecutive
Seed minimal itx project worker commits. An at-head creation obligation had
been driven twice; the old seed path always made and force-pushed a commit even
after cloning an existing branch, so it moved main between the mutation's
checked clone and push.
The creation call now shares the Repo DO's write serializer with mutations. Seeding is strictly create-if-absent: cloning any existing branch returns it untouched, including user commits. Two genuine first drives that both observe an empty Artifact produce the same root oid from a fixed synthetic seed identity and timestamp, making their pushes equivalent. Initial publication is also a normal fast-forward push, never forced: if a different head wins the race, creation fails instead of replacing it.
The same retries-disabled run found an independent first-use secret race. A
GitHub App token mint read secret state at offset 5; a
connection-opened telemetry fact advanced the raw stream to offset 6;
the material append's explicit offset then conflicted and was incorrectly
reported as secret_not_found. Refreshed material now retries bounded offset
conflicts while the reduced secret's updatedOffset and refresh policy remain
unchanged. Any actual secret update still invalidates the in-flight mint.
7. Local harness must match the CI contract (vitest retry)#
The e2e vitest lane configures retry: ci ? 1 : 0 — Depot CI absorbs one
platform blip per test (Cloudflare's DO resets surface as
internal error; reference = … / Durable Object storage caused object to be reset, which Cloudflare marks retryable) while the local loop ran with CI
unset, i.e. a stricter config than the pipeline being de-flaked. The loop now
exports CI=true so a run means exactly what a Depot run means; retried
tests remain visible in the run log.
8. CI's vitest retry never actually applied (root config not inherited)#
The e2e config declared retry: ci ? 1 : 0 at the ROOT test level, but
vitest does not inherit retry into projects configs — a CI-profile run
showed a failed test with zero retry attempts. Every "one retry absorbs a
rare blip" assumption in the preview lane has been a no-op since the config
was split into projects. Fix: retry now lives on each project's test block.
9. Idle-teardown severance asserted with a 1.5 s deadline#
stream-lifecycle › append after idle teardown re-wakes configured subscriber
gave the Stream DO 1.5 s to sever three cross-script processor connections
after runIdleTeardownNow(). Under the CI-parallel lane profile that
intermittently takes longer; the assertion is about eventual severance, not a
latency SLA. Both severance waits now allow 10 s.
10. sandbox-exec REPL spec undercuts the container cold-boot tail#
The Playwright REPL spec provisions a fresh project (fresh sandbox container)
per run and middlewright's spinner-waiter caps "spinner still visible" waits
at 30 s — but a cold container boot + repo clone legitimately exceeds that
(observed >70 s across two attempts on preview). ExampleCase now carries
completionTimeoutMs; sandbox-exec declares 120 s and the spec bypasses the
spinner cap for examples that declare a budget. Expected latency is not a
hang.
11. Container-instance cap wedges sandbox starts after ~5 runs#
With the fixes above, back-to-back full runs complete in ~1 minute — and both
marathon attempts then failed on exactly run 5, with sandbox-exec consuming
its entire (now 120 s) budget twice: the container never started. Mechanism:
every run provisions fresh fixture projects whose sandbox containers idle for
the SDK-default sleepAfter = "10m", and the sandbox container app is capped
at maxInstances: 10 — roughly two sandbox containers per run × 5 runs
exhausts the cap, and every later start queues until an old container times
out. Fix: sleepAfter = "3m" on the sandbox DO (reclaims capacity ~3× faster;
idle restart costs one cold boot + clone) and maxInstances: 40 (lite
instances bill on usage, not reservation).
12. Green check on a wedged slot: stale failed deploys are never retried#
Round 2 opening move. preview_7's D1/KV had been deleted out from under
envs.ts, so the first deploy failed (D1 7404). The fix push touched only
envs.ts — which was in NO preview paths list — so app selection chose
nothing, the stale deploy-failed entries (old head) were excluded by the
retry selector's same-head guard, deploy skipped ("nothing to deploy"), the
test lane skipped its stale recorded apps, and the whole check went green
with three apps deploy-failed on the slot. Two fixes:
envs.ts+scripts/lib/**joined the preview shared paths (and the Depot workflow'spaths): every app's wrangler config derives from envs.ts.- Failed states (
deploy-failed,claim-failed,tests-failed) now retry regardless of which head recorded them. (awaiting-testsinitially kept a same-head guard; it too retries at any head since thepreview runone-step refactor — an awaiting-tests entry at any head is a deploy whose e2e never ran.)
13. Fresh-worker bring-up: module-scope config parse + orphaned container app#
Recreating preview_7's deleted workers surfaced two first-deploy failures:
- semaphore:
parseConfig(workerEnv)ran at module scope, so Cloudflare's upload-time script validation threw ZodError on a worker with no secrets yet — rejecting exactly the classless bootstrap deploy a fresh worker needs before its first code+secrets version (deploy-helpers.ts). The parse is now lazy (memoized on first request). - os: the Cloudflare Containers application
os-preview-7-cloudflaresandboxdurableobject-preview_7survived the old worker's deletion and stayed bound to the dead DO namespace; redeploying created a new namespace and Cloudflare refused the collision ("already an application with the name … associated with a different durable object namespace"). Fixed operationally withwrangler containers delete <id>; if a slot's os worker is ever deleted again, expect this and delete the orphaned application before redeploying.
14. Vitest wedged at startup for 9+ hours (loop watchdog added)#
Round-2 run 14: the Playwright specs passed, then pnpm e2e --project node
printed vitest's header and nothing else for 9h23m. The vitest main process
sat with an idle event loop (kevent wait), zero CPU, and no worker
children — it hung before running a single test, machine awake the whole
time (pmset clean). Root cause unknown (one occurrence in ~20 runs;
plausibly a wedge in vitest's startup/fork-pool against this Node version).
Mitigation: the loop now runs each attempt under a watchdog
(RUN_TIMEOUT_SECS, default 30 min) that kills the run tree and counts it as
a failure instead of silently freezing the marathon.
Recurred round-3 (r3c run 23), and exposed two gaps — both now fixed:
- The watchdog fired at 30 min but its
SIGTERMdid not propagate down the deepdoppler → pnpm → trpc-cli → inner doppler → bash → vitesttree, so the wedged run hung ~58 min past the timeout, still holding the loop'swait. Historical fix: the then-current in-process loop walked the whole descendant tree andSIGKILLed it leaf-first. Thatkill_treeimplementation was retired when the marathon became an observer of canonical Depot runs. - More importantly, the wedge is now self-healing at the source instead of
costing a whole run: the preview test orchestration (
previewTestCommandArgsinpreview.ts) wraps the vitest node lane intimeoutand retries it once on a timeout. A rare fork-pool wedge is a fresh restart, not a dead lane — and this fixes it for Depot CI too, where the same wedge would otherwise hang the job until its timeout. Root cause (why vitest's pool occasionally hangs pre-RUN) is still unknown; this makes it a non-event either way.
Correction (first Depot marathon, run df87f12sz3): the self-heal was firing
on EVERY run. The retry condition also fired when the lane "never printed
RUN v<version>" — but that grep is defeated by vitest's ANSI colour codes,
which sit between RUN and the version (RUN␛[…m␛[…mv4.1.8), so it matched
nothing and re-ran the entire vitest node lane a second time on every single
run (all 18 of them). That silently doubled test load and sandbox-container
churn — very likely a hidden aggravator of flakes 19/20 (cap pressure and
provisioning latency) throughout round 3. Fix: retry only on rc=124 (the
timeout — the actual wedge signature); rc=0 means it ran, and a non-124
non-zero is a real failure we must not paper over. Also cut the lane timeout
to a fail-fast 360s (was briefly 600/900).
15. Concurrent repo writers lose the compare-and-swap race on refs/heads/main#
Round-2 run 44 (43 consecutive greens, then this): examples-matrix › repo-edit-file failed two different ways across its retry — first a
git.push GitPushError: refs/heads/main: stale ref, then (on the retry)
the final readFile returning status: draft after the edit had reported
success. Both are the same root cause.
The itx examples matrix runs many repo-mutating examples
(repo-commit-files, repo-edit-file, …) concurrently against ONE shared
project repo, all pushing to refs/heads/main. Each mutation clones HEAD,
commits on top, and pushes. The Artifacts git server enforces optimistic
concurrency (compare-and-swap on the ref): when a second writer's push carries
a parent that is no longer the server's current HEAD — because the first
writer landed in between — the server rejects it with stale ref /
not-fast-forward, and isomorphic-git surfaces that as a thrown GitPushError.
force: true (which every mutation used) does not help and actively hurt:
- It only skips isomorphic-git's client-side fast-forward check, not the
server's compare-and-swap — so the
stale refrejection still happens. - A force-push that did land would clobber the concurrent writer's commit by
resetting HEAD to a parent that predates it. That is exactly how the second
failure arose: our
editcommitted and pushed, then a racing writer's force-push resetmainto a commit that predated our edit, and the read-your-write clone (flake 6) faithfully returned the reverted content.
Fix: already solved on main by the #1612 repo refactor, which this branch
merges — so the standalone fix this branch first carried (a compare-and-swap
retry loop in mutateArtifactRepo) was dropped in favour of main's cleaner,
structural one. The Repo DO now serializes every write through a #writeChain
(commitFiles/edit each run inside #serializeWrite), so two mutations to one
repo can never be in flight at once: each clones the latest HEAD, commits, and
fast-forward pushes with no force. With a single writer at a time there is
no compare-and-swap race to lose and no force-push to clobber a concurrent commit
— the two failure modes above are structurally impossible. Repo seeding now
uses the same non-forced publication rule.
The diagnosis is retained here because it explains why main dropped force and
serialized writes; the marathon re-verifies it end-to-end.
2026-07-17 correction: serialization prevents two calls in one Repo DO
from overlapping, but it does not make the Artifacts clone endpoint strongly
consistent. A second serialized write can clone a replica that still advertises
the pre-first-write head and then lose the server's ref compare-and-swap with
stale ref. Mutations now record the seed and every successful push, and wait
boundedly until their clone's branch HEAD equals that last pushed commit before
changing anything. Merely finding the commit object in the clone is not enough:
Artifacts can supply the object while its advertised ref still lags. A later
rejection therefore really does identify an out-of-band writer that moved the
ref after the checked clone.
16. Marathon methodology: an incremental deploy splits the fleet head#
Not a product flake — a hole in the flake-hunt harness, surfaced while shipping
the flake-15 fix. preview deploy selects apps by diffing the PR head against
the last deployed head, not the PR base. So a mid-branch commit that touches
only one app (the flake-15 fix was apps/os-only) redeploys just that app and
leaves the others at the previous head. The test lane only tests apps whose
recorded head equals the PR head, so the very next marathon run tested os, auth
but not semaphore, streams-example-app — tripping the full-fleet guard
(exit 3) at run 1. This is the deploy-side twin of flake 12: a fleet can silently
shrink to the changed apps, and without the guard a partial lane would count as
green.
Historical fix in the then-current loop: a fresh marathon ran a full-fleet
deploy preflight before counting. The current orchestrator instead dispatches
the canonical workflow, whose manual-dispatch path passes --all-apps; every
counted Depot run therefore deploys and tests the complete fleet directly.
Preflight hardening (round 3): the preflight originally relied on the
marathon commit happening to touch a fleet-shared path to force a full-fleet
deploy — an apps/os-only commit would have deployed just os and tripped the
guard at run 1. preview deploy --all-apps now forces the full fleet
explicitly and the preflight uses it.
Second sub-cause (round 3): a commit touching only dependency manifests
(patches/** + pnpm-lock.yaml + pnpm-workspace.yaml, from the flake-22
middlewright patch) selected no apps at all — "nothing to deploy" left
every recorded head stale behind the PR head and the test lane skipped every
app. Dependency manifests can change any app's build output, so they are now
cloudflareAppSharedPaths (full-fleet deploy), mirrored in
cloudflare-previews.yml's paths filter and asserted in preview.test.ts.
17. A second browser tab has no spinner-waiter, so it dies on the 750ms actionTimeout#
Round-2 (r2e) run 3: reactivity.spec.ts › delivers an appended event to another open tab failed on the initial attempt and retry #1 —
TimeoutError: locator.waitFor: Timeout 750ms exceeded waiting for the second
tab's reactivity-stream-status to read live. The first tab (line 73) went
live fine; only the second tab (line 74) timed out.
playwright.config.ts sets a deliberately tight actionTimeout: 750 (non-video)
so bare waitFor()s fail fast; the safety net is middlewright's spinner-waiter,
which extends a wait up to ~28s while a data-spinner="true" element is
visible. The reactivity page shows exactly one such element (the "connecting…"
badge) while a subscription connects, so the primary page reliably reaches
live. But the second tab is created with context.newPage(), which returns a
raw page without our middlewright plugins — so its waitFor() gets only the
raw 750ms, too short to open a second concurrent stream subscription (a second
WebSocket to the same Stream DO). Runs 1–2 passed because the second tab
happened to connect in <750ms; run 3 it didn't, twice.
Fix (specs/test-support/test.ts, not the spec — specs stay verbatim): the
page fixture now patches context.newPage to wrap every subsequently-opened
page with the same plugins as the primary. basePage already exists when the
fixture runs (Playwright's built-in page fixture created it via
context.newPage() first), so the primary page is never double-wrapped; only
extra tabs the spec opens later get wrapped. Any multi-tab spec now benefits.
18. A dangling stream-wait rejection crashes the vitest runner, bypassing retry: 1#
Fleet survey (2026-07-04, across all of today's PRs) found the stream-event
delivery flake was the dominant preview e2e failure (4 of 5 failing PRs), and
uncovered a distinct, higher-leverage bug in how it fails. On PRs #1664 and
#1665 the Timed out waiting for stream event … saw 0 events error came back
over capnweb's read loop (serialize.ts → rpc.ts readLoop) as an
unhandled promise rejection — not inside a test's await — and Node's
default crashed the whole vitest worker (Node.js v24…, exit 1, zero test
output). Because the process died before vitest could mark the test failed,
the CI retry: 1 safety net never engaged. The same flake on #1666, where it
surfaced inside an await, recovered on retry and went green.
Why it dangles: under sequence.concurrent (maxConcurrency 2) several tests'
waitForEvent RPCs are in flight at once. When the slot's stream delivery
stalls under load, a sibling test's — or a background subscription's — wait
rejects after its owning test has already moved on, so nothing is awaiting it.
Fix (apps/os/e2e/vitest/setup.ts): a scoped unhandledRejection handler that
swallows only the known-transient stream-wait signature
(Timed out waiting for stream event / waitUntilEvent timed out) so the
worker survives — the test that actually awaited the wait still fails normally
and retry: 1 re-runs it — and re-throws every other rejection (Node escalates
that to an uncaughtException, preserving crash-on-real-bug). This converts a
suite-killing crash into an ordinary, usually retry-absorbed, failure.
The survey also refuted the "unhealthy preview slot" hypothesis: today's 5
failures spread evenly across preview-2/-3/-4/-7/-9 with no repeated or
chronically-bad slot and zero JWKS/503/workers.dev-edge-drift; the only
environmental correlate is cold-slot / cold WORKER_BUILD_CACHE on the first
run after a deploy. And it confirmed maxConcurrency: 2 still flakes, so the
real fixes remain the tracked delivery race
(tasks/streams-event-delivery-flake-under-concurrent-load.md) and splitting
the 39-test itx.e2e.test.ts monolith — not raising concurrency.
19. Sandbox container instance-cap wedge (Container is starting)#
Round-3 (r3b) run 4: sandbox-egress.e2e.test.ts › is MITM-intercepted and routed through project egress failed both attempts (~292s) with
Error: Container is starting. Please retry in a moment. — the SDK's own
transient-startup 503, which it auto-retries, but the container never became
ready inside the budget, twice. This is the flake-11 family resurfacing: the
preview slots capped sandbox containers at max_instances: 20, and
sandbox-heavy e2e churns several fresh containers per run (the REPL
sandbox-exec spec, #1654's new sandbox-egress vitest test, the examples
matrix — each a fresh project + container), so under a marathon the cap is
reached and a new container wedges in "starting" until a slot frees.
Cleanup was not the problem: the @cloudflare/containers base sets a
durable idle alarm from sleepAfter (3m) and CloudflareSandboxDurableObject
does not override alarm(), so every idle container is reaped and its slot
freed within ~3m (verified in the SDK: renewActivityTimeout → sleepAfterMs
→ alarm() stop). The containers were being cleaned; there just weren't enough
slots for the churn rate.
Correction (flake 23): the SDK verification above proved the container
stops — not that its instance slot is released. It isn't: a stopped
instance stays ASSIGNED to its Durable Object and assignments are what count
against max_instances. The cap raise bought headroom but the leak remained;
see flake 23 for the real fix (destroy on idle).
Fix (apps/os/scripts/generate-wrangler-config.ts): raise the cap to 100
for previews and 50 for prd (lite instances bill on usage, not
reservation, so a high cap is free headroom). The durable idle reaper keeps
cleanup reliable at any cap. That round also briefly added uncounted warmups;
they were later removed because cold-start behaviour is part of the production
path and every proof run must count.
20. Sandbox tests must budget for cold container image provisioning#
Distinct from flake 19's instance-cap "Container is starting": Cloudflare
intermittently RE-PROVISIONS the sandbox container image
(Container is currently provisioning. This can take several minutes on first deployment., SDK 503 phase: provisioning). It hit r3d run 7 (after an
overnight idle) and r3e run 17 — the latter on a continuously-running
marathon ~40 min / 16 clean runs in, so it is NOT just a post-idle artifact: the
image gets re-pulled to a node every ~15-20 runs regardless. When it fires it
hits every sandbox test at once (sandbox-exec in both the vitest matrix and
the Playwright REPL lane, plus sandbox-egress), and the SDK's automatic retry
takes longer than the old 120-240s test budgets, so they time out mid-provision.
The first instinct was to raise the budgets to ride provisioning out (480s
tests, 900s lane). That was wrong and has been reverted. Sitting for 8-15
minutes to mask a Cloudflare infra transient is worse than failing: it hides a
real issue behind a huge wall-clock, and the point of the e2e lane is to
fail fast when something is actually wrong. The provisioning latency is a
Cloudflare-side transient (a direct itx run sandbox probe confirmed it clears
on its own — a fresh sandbox exec returned in seconds once the window passed),
not something the test should absorb.
Decision: keep fail-fast budgets — sandbox-exec completionTimeoutMs
180s (120s undercut a genuine ~132s cold boot observed on Depot), the vitest
matrix/sandbox-egress tests 240s, and the preview.ts vitest-lane guard 360s
(was briefly 600/900). A container stuck provisioning
surfaces in ~2 min and the run fails; we re-run rather than wait. Reaching 50
consecutive green therefore depends on a healthy provisioning window (or a
Cloudflare-side fix), not on masking the latency — and CI’s own retry absorbs a
lone transient. If provisioning proves frequent enough to block 50-in-a-row,
the right lever is reducing sandbox-container churn or a Cloudflare escalation,
not a bigger timeout.
23. Stopped sandbox containers never release their instance slots#
Signature (Depot marathon3 run 12, and retroactively flakes 19/20): runs slow down progressively (sandbox-exec 22.7s → 33.8s → stuck at 180s×2), the vitest lane trips its 360s guard, the loop watchdog SIGKILLs the run at 600s. The REPL page shows the run submitted but the entry never lands — the server-side sandbox exec is silently waiting for a container that never starts. No error anywhere.
Root cause: wrangler containers info on preview-3 fifteen minutes after
the marathon died: instances: {active: 0, assigned: 99, healthy: 1} — at the
max_instances: 100 cap — while idle slots hold 7-10 assignments for days.
The SDK's sleepAfter idle alarm does run (active: 0 — the containers
stopped), but a stopped instance stays assigned to its Durable Object, and
assignments (a) count against max_instances and (b) never expire on their
own — only destroy() or an app rollout releases them. Every e2e fixture
creates a fresh sandbox DO, so each preview e2e run leaks ~7-8 assignments;
the cap wedges after ~a dozen runs; a fleet deploy resets the count (which is
why every marathon ran clean for its first ~dozen runs and why flake 20's
"provisioning windows every 15-20 runs" pattern fit so well — it was this
leak, not image re-pulls, at least in the continuously-running cases).
Fix (cloudflare-sandbox-durable-object.ts): override
onActivityExpired to destroy() (SIGKILL + full teardown, releases the
assignment) instead of the SDK's stop() (SIGTERM, keeps the assignment).
For our sandboxes destroy-on-idle costs nothing: the container filesystem is
ephemeral across sleep anyway, so waking from stop and waking from destroy are
the same cold boot + repo re-clone. The SDK's keepAlive escape hatch is
preserved. Verified on preview-3 with a probe sandbox (itx.sandboxes.get +
one exec): its instance showed active:1, assigned:0 while running and went
active:0, assigned:0 exactly 3m after the last command — the slot fully
released, where the old behavior left it assigned indefinitely. (The fix's
deploy also confirmed a rollout flushes the leaked pool: the app dropped from
100 stuck instances to a handful once the new version finished provisioning.)
Marathon5 addendum — destroy is necessary but the cap must still exceed
cumulative churn. With destroy-on-idle deployed, marathon5 still wedged at
run 33 with active:0, assigned:100: under sustained load the platform
BACKFILLS released slots into an "assigned" warm pool that rides at
max_instances (the probe above released to 0 only because demand was zero
at that moment), and sandbox start latency grows as the pool saturates —
sandbox-exec went ~20-40s (runs 1-10) → 2.1-2.8min (runs 30-32, shaving the
180s budget) → 3.2m×2 attempts (run 33, dead). Every marathon wedge to date
happened at exactly assigned == max_instances (20, then 100). Response:
preview cap raised 100 → 500 (generate-wrangler-config.ts) so a whole
50-run marathon's cumulative creations (~3-8 sandboxes/run) never saturate
the pool. On 2026-07-07 this was reduced to 150 because several preview
slots at 500 standard-1 instances exceeded the dev/preview account memory
quota and blocked deploys; 200 fit the currently populated fleet but did not
leave room for all nine preview slots to carry OS sandbox apps. Destroy
remains correct — it is what lets an idle fleet drain back to zero instead of
holding slots forever. If sustained-churn claim latency reproduces at the
cap, this becomes a Cloudflare Containers escalation (pool-manager
degradation under DO-binding churn), not an app-side fix. (Update 2026-07-08:
#1747 replaced the flat cap with per-instance-type caps —
SANDBOX_MAX_INSTANCES in apps/os/scripts/generate-wrangler-config.ts is
now the source of truth; the saturation mechanics above are unchanged, per DO
class.)
21. Blank route-pending panel fast-fails the first wait after goto#
Signature (Depot marathon2 run 5; also the round-2 merge's own preview
e2e — the "REPL Run button fast-fail" below): repl-examples.spec.ts
repo-read-file / repo-edit-file fail in ~6s with
TimeoutError: locator.waitFor: Timeout 1ms exceeded waiting for the "Run"
button right after page.goto(/projects/<slug>/repl). The failure screenshot
shows the app shell (sidebar, breadcrumb) with a completely empty main
panel — no REPL, and critically no spinner.
Root cause — a product gap, not a slow test. The project layout
(routes/_app/projects/$projectSlug/route.tsx) is ssr: false, so a direct
hit SSRs only the shell; TanStack renders the client-only match as
<ClientOnly fallback={pendingElement}> and shows pendingElement again
while beforeLoad (the getProjectBySlugServerFn HTTP roundtrip) runs after
hydration. The router configured no defaultPendingComponent, so
pendingElement was null → the outlet rendered literally nothing for the
whole hydrate + project-fetch window. The failing trace shows that server-fn
taking 1037ms (a normal cold-read tail); with no spinner visible the
spec-side spinner-waiter correctly refused to extend the deliberately-tight
750ms actionTimeout and the wait died with 1ms left. All the route-level
fallbacks further down (ItxResourceLoading, ItxPending) never got a chance
to render — the match itself hadn't mounted.
Fix (apps/os/src/router.tsx): wire defaultPendingComponent (muted
"Loading…" with data-spinner="true", same idiom as every route-level pending
fallback) plus defaultPendingMs: 300 / defaultPendingMinMs: 200 (library
defaults leave a 1s blank window on client-side loads). The spinner now sits in
the SSR HTML itself for ssr: false subtrees, so from first paint to REPL
mount the app continuously reports progress and the spinner-waiter extends
exactly as designed — no spec-side timeout was touched. This is the correct
fail-fast shape: the wait budget grows only while the app visibly claims to be
working, and a genuinely wedged page still dies at the spinner-waiter's 30s
cap.
22. spinner-waiter dies on TWO visible spinners (strict-mode violation)#
Signature (the push-triggered preview run for the flake-21 fix):
dashboard.spec.ts fails with locator.isVisible: Error: strict mode violation: locator('[aria-label="Loading"],[data-spinner=…]…') resolved to 2 elements.
Root cause: middlewright's spinner-waiter checks "is a spinner visible?"
with spinnerLocator.isVisible() on the UNION selector — and Playwright's
isVisible() throws when a locator resolves to more than one element. Two
loading indicators visible at once is a perfectly legitimate app state (e.g.
the REPL panel's "Connecting to itx…" next to the activity tail's "Connecting
itx activity…"). The flake-21 fix UNMASKED this: before it, the blank pending
panel meant those sibling fallbacks never got to render together during the
window the spinner check runs in.
Fix: patches/middlewright@0.1.1.patch — both spinner checks
(spinnerVisible and the bail-early check in waitForReadyWhileSpinning) go
through a multi-element-safe anySpinnerVisible() using
filter({ visible: true }).count() > 0, which needs no strictness. "Any
visible spinner counts as progress" is the plugin's intended semantic. Worth
upstreaming to the middlewright package.
23. spinner-to-content handoff can false-fail with a 1ms timeout#
Signature: dashboard.spec.ts reports Timeout 1ms exceeded waiting for
the project link, while the failure screenshot and accessibility snapshot
already contain that exact visible link. The page showed its route/query
"Loading projects..." indicator continuously until the table replaced it.
Root cause: middlewright first polls target readiness, then separately checks whether any spinner is visible. React can atomically commit the spinner-to-content replacement between those browser reads: the target was absent at the last poll, then the spinner was absent at its check. The waiter therefore took its no-spinner fast-fail path and gave the now-appearing target only 1ms. This is a test-waiter TOCTOU race, not missing product progress UI.
Fix: patches/middlewright@0.1.2.patch gives the target one 100ms polling
interval after observing no spinner. A genuine no-spinner failure still fails
in about a second; an atomic loading-to-ready handoff can complete without a
false failure.
Round 3 targets#
The round-2 merge commit's own preview e2e (Depot, two attempts) failed on two pre-existing flakes that round 3 fixes:
- REPL "Run" button fast-fail.
forged-session-repl.spec.tsand severalrepl-examples.spec.tscases fail withTimeout 1ms exceededwaiting forgetByRole("button", { name: "Run" })after/replnavigation. Root-caused as flake 21 (blank route-pending panel — nodefaultPendingComponenton anssr: falsesubtree), fixed inrouter.tsx. - Stream-event delivery timeout under concurrent load / cold build cache.
Timed out waiting for stream event after 60–90s (saw 0 events)across reactivity, repl-examples, and a vitest e2e test. Known/tracked (tasks/streams-event-delivery-flake-under-concurrent-load.md,tasks/raise-e2e-maxconcurrency.md); the vitest lane already runs atmaxConcurrency: 2. The round-2 merge added #1612's worker-build pipeline, whose first dynamic-worker build on a coldWORKER_BUILD_CACHEKV (freshly created per slot) adds latency that widens this window on the first run.
Push-lane deployment-version barrier#
- Guarded: deploy→test race:
Durable Object reset because its code was updated.wrangler deploycan return while Cloudflare is still propagating the new code. This produced a rollout-wide failure on commit1796831c: the agent smoke reset on its first attempt, 30 Vitest tests retried, and 19 still failed. A retry was not a sufficient boundary. OS/api/healthnow reports itsCF_VERSION_METADATAid, and the preview orchestrator parses the finalCurrent Version IDfrom the deploy (the main Worker follows its two sidecars) and requires that exact version on the health probe before the test phase can create a project (first match; no multi-second dwell). A plain 2xx no longer counts as post-deploy readiness. The next fully settled run then exposed a separate deterministic problem: E2E fixtures still relying on implicit processor births. Those fixtures now create their agents, repos, secrets, and integration routers explicitly.
Observed, not yet fixed#
packages/mock-http-proxyunit testmsw-server-adapter.http-parity › does not mark non-matching one-time handlers as usedfailed once in the DepotTest / testlane withfetch failed: bad port— the listen(0) helper appears to have produced port 0 despite waiting for 'listening'. Unit lane, outside preview e2e; needs its own repro.- One Depot push (
346bcebdb) produced a run with zero scheduled workflows (depot ci statusshows"workflows": []), so no checks were created for that head at all; a manualdepot ci dispatchof cloudflare-previews.yml covered the gap. Worth watching for recurrence.
5. Sandbox repo clone dies on a transient Artifacts 503#
repl-examples.spec.ts › sandbox-exec failed with Failed to clone repository 'https://…artifacts.cloudflare.net/git/…': error: 503 — the Artifacts git
endpoint intermittently 503s on cold repos and the sandbox clone ran exactly
once. Fix: cloudflare-sandbox-durable-object.ts#cloneProjectRepo retries the
clone (3 attempts, backoff, fresh target dir each try).