# Cameron Rye - Complete Content Archive > This file contains the complete text of all blog posts and projects from rye.dev. > Generated for LLMs with large context windows per the llms.txt specification. > See also: https://rye.dev/llms.txt for a concise overview with links. Total blog posts: 27 Total projects: 15 --- # Blog Posts ## The Test Wasn't Flaky. The Server Was Quitting. > A prod-smoke suite failed for weeks and the error I spent that time chasing was never the cause. The real one was an upgrade that made a dropped connection fatal. Date: 2026-08-06 Tags: cloudflare, wrangler, testing, debugging, ci URL: https://rye.dev/blog/the-test-wasnt-flaky-the-server-was-quitting/ For most of July, the last gate before production on this site failed about four times in ten. It is a Playwright suite I call the prod smoke: it builds the real Worker bundle, serves it with `wrangler dev --local`, and drives a browser at it. When it goes red, the deploy job is skipped and nothing ships. The failures looked like this. Four specs would go down together — CSP headers, front-door cache, agent terminal, search — all with `ECONNREFUSED`. Four unrelated parts of the site, broken at once, in a suite that had passed an hour earlier on the same commit. That shape is worth learning to recognize, because it is not four defects. It is one. When everything downstream of a server fails at the same instant, you do not have four bugs; you have a server that stopped answering. Every minute I spent looking at the CSP test was a minute spent on a symptom. What follows is the part I got wrong for three weeks, and the thing that turned out to be true. ## The wrong answer, written down and committed I had a diagnosis early. Buried in the run logs was this: ``` kj/async-io-unix.c++:263: disconnected: ::write(...): Broken pipe ``` That is workerd, the runtime under `wrangler dev`, complaining about a write to a socket the client had already closed. It appeared in failing runs. It looked exactly like a crash. I believed it, and on 19 July I committed a supervisor script to restart the server around it, in `c376ab9`, with a message that stated the cause plainly: *"workerd intermittently dies mid-run on a client disconnect."* The supervisor worked. The failures continued. The problem with a wrong diagnosis that ships with a partial fix is that it stops looking like a hypothesis. It becomes the thing everybody knows. It went into a commit message, then into comments in three files, and every time I came back to the flake I started from it. Here is what finally killed it. I stopped reading the logs for the error I expected and started counting: | Run | Broken pipe lines | Server exits | Result | |---|---|---|---| | CI, 3 Aug | 2 | 0 | **passed** | | CI, 4 Aug | 1 | 8 | failed | | 46 local runs | **0** | 12 | 4 failed | A passing run had two Broken pipe lines. A failing run had one. Forty-six instrumented runs on my own machine produced twelve failures and not a single Broken pipe line. In one run, the first two exits both happened *before* the first Broken pipe line appeared. The two events were unrelated. Broken pipe is noise that a dev server emits when a browser closes a tab, and it appears in healthy runs. I had spent three weeks on a coincidence. The number that did correlate was the one I had not been counting: **every run where the server process exited failed, and every run where it did not, passed.** Restart count was not predictive either — one CI run restarted the server eight times and still went green. What mattered was whether the process was gone at the moment a test needed it.  ## An error with no message So why had I not noticed the server exiting? Because of how it announced itself: ``` ✘ [ERROR] ``` That is the whole line. No message, no stack, no code. `wrangler dev` prints an empty error and the Node process exits 1. In a CI log with thousands of lines of build output, a bare glyph is invisible, and it is not something you would ever think to grep for. The stack existed, but only in wrangler's own debug log, which nothing was capturing. The fix was two lines in the workflow: set `WRANGLER_LOG: debug` on the prod-smoke job, and upload the log directory as an artifact whether the job passes or fails. Do that before you theorize. I had been reasoning about a process whose last words I had never read. With the log in hand the sequence was legible, and it was not a crash at all.  ## What was happening `wrangler dev` runs your code in workerd, but it puts a small Worker in front of it called the ProxyWorker, which forwards each request to the UserWorker — your actual code — and pipes the response back. When Playwright tears down a browser context, it closes sockets. If a response is still being written when that happens, the ProxyWorker's `fetch` to the UserWorker rejects with a disconnect: `Network connection lost.` The ProxyWorker reports that to the controller, where it arrives as an error named **`Error inside ProxyWorker`**. Wrangler keeps an allowlist of error strings that are known to be harmless during development. There are two on it: `Failed to send message to…` and `Could not connect to InspectorProxyWorker`. `Error inside ProxyWorker` is not one of them. So it falls through to `emit('error')`, and with no handler attached to that event, Node does what Node does. Note what is *not* happening here. workerd does not die. It survives the whole thing, which is why it goes on holding port 4321 after its parent is gone — and why the naive cleanup step I will get to below made everything worse. The thing that quits is wrangler's own Node process. There is no crash to catch and no flag that makes this non-fatal. ## The one-line change that caused it None of this was new code on my side. The suite broke because I upgraded wrangler from 4.110 to 4.114 in `d63dea6`, in a routine batch of dependency updates, and deploys started failing three days later. The relevant code is in the ProxyWorker's error handler. It has to decide whether an error it just caught is still worth reporting, because by the time a network error surfaces, the UserWorker may already have been replaced by a hot reload — in which case the error is stale and should be dropped. In 4.113, that check is one line: ```ts // only report errors if the downstream proxy has NOT changed if (userWorkerUrl.href === newUserWorkerUrl?.href) { ``` Now look at what those two URLs are. `userWorkerUrl` is built from the incoming request, so it carries that request's path: ```ts const userWorkerUrl = new URL(request.url); Object.assign(userWorkerUrl, proxyData.userWorkerUrl); ``` `newUserWorkerUrl` is rebuilt from the connection parts alone, so its path is `/`. For any request to a real page, those two hrefs are never equal. `http://localhost:4321/blog/some-post` does not equal `http://localhost:4321/`. The comparison is false, the error is judged stale, and it is silently dropped. **On every path except the root, this check swallowed the error by accident.** In 4.114 the comparison became origin-based, and wrangler's own source says why: > `isSameUserWorkerOrigin` compares origin (not href) so a genuine error on a non-root path isn't misread as a reload That is a correct fix for a real bug. Origins now match on every path, so the error is reported instead of dropped — and because `Error inside ProxyWorker` was never on the non-fatal allowlist, "reported" means the dev server exits. My suite had been depending on a bug. When upstream fixed it, a dropped connection on any page went from silently ignored to fatal. Both of those behaviors are wrong; I had just been on the comfortable side of the trade. ## It was never one bad test While the diagnosis was wrong I kept trying to find the test responsible. There wasn't one. I suspected my heaviest spec, which loads about 1.3 MB of Mermaid chunks and has one case that aborts requests mid-flight. Running it alone: zero failures in eight attempts. The agent-terminal spec alone: zero in five. The full suite: about half. Then I stopped bisecting and measured the actual trigger — a page torn down while responses were in flight: - A page with no subresources survived **80** teardowns. - A normal page died within **6 to 22**. - Letting the page settle before teardown cut the rate by roughly **6×**. It is dose-response. Every teardown with something in flight is a roll of the dice, and a browser test suite does that hundreds of times. That also explains why concurrency looked like a lever and wasn't: running with `--workers=1` failed at the same rate over eight runs. Serializing the tests does not reduce the number of teardowns, only their overlap. If you are hunting a flake and single-spec runs come back clean, consider that you may be looking for a rate rather than a culprit. ## A real bug that fixed nothing One detour is worth publishing because the code looked obviously correct. The supervisor needed to free the port before restarting, since workerd outlives its parent. It did this: ```bash lsof -ti:4321 | xargs kill -9 ``` `lsof -i:PORT` matches a socket if **either** endpoint uses that port — local or foreign. A browser connected to the dev server has 4321 as its *remote* port, so it matches. That command was killing Chromium's connections during every restart. The correct filter is `-sTCP:LISTEN`, which returns only the listener; I confirmed it with a listener and one client, where the bare form returns both PIDs and the filtered form returns one. The same mistake was in the CI cleanup step. It was a genuine bug, I was right to fix it, and it changed nothing: four of six runs still failed afterward. Fixing a real bug you found while looking for a different one is not evidence you found the right one — which is the same error as the Broken pipe, wearing better clothes.  ## The fix, and why it is three things The fix is a version pin: ```json "wrangler": "4.113.0" ``` Exact, not a caret. A caret floats straight back to 4.114 on the next install and undoes everything. The pin alone is not enough, and this is the part that would have cost someone else an afternoon. Two more entries are required: - **`@cloudflare/vite-plugin` held at `~1.46.0`.** The plugin pins `miniflare` and `workerd` exactly in its own dependencies, and 1.46 is the line whose pins match wrangler 4.113. Let it float to 1.47 and the tree resolves two copies of miniflare and two of workerd. The tell is a pnpm warning naming two workerd versions at install time. - **`sharp` overridden to `^0.35.3`.** Plugin 1.46.0 carries sharp 0.34.5, which has a high-severity advisory. I had raised the audit gate from `critical` to `high` a few days earlier, which is the only reason this surfaced before it shipped rather than after. Measured across full runs of the suite: | wrangler | server exits | outcome | |---|---|---| | 4.114.0 | 7 in 6 runs | 4 red | | 4.113.0 | **0 in 14 runs** | 12 green | The one non-green run under 4.113 was an unrelated timeout, with zero exits. ## A pin is a debt Pinning to an older version to dodge an upstream fix is not a good place to end up. It is a deliberate decision to stay on the far side of a change that was, on its own terms, correct. So the pin carries its reasoning where the next person will hit it — in the Dependabot ignore entry, alongside the measurements and an explicit condition for removing it: not "when a newer version exists," but when upstream restores non-fatal handling for a client disconnect. A newer version number is not evidence. 4.118 was still reported failing, and it also pins a miniflare 5 alpha. The whole-suite retry stays in CI as a backstop, because a pin is a bet and I would rather not re-learn this from a red deploy. On the first green run after the pin landed, the retry never fired — and the run before it had restarted the server four times and still passed. Three things I would keep from this: **When everything fails at once, suspect the thing underneath all of it.** Four specs failing together was never four problems. **An error you can explain is not the same as the error that is happening.** Broken pipe had a plausible story, appeared in the right logs, and was a coincidence. The number that mattered was one I was not counting until I stopped assuming. **Capture the logs before you theorize.** The actual cause printed a bare `✘ [ERROR]` with no message, and its stack existed the whole time in a debug log nothing was uploading. Three weeks of guessing, and the evidence was one workflow line away. --- ## Making My Portfolio Agent-Readable: From Files to an Interface Agents Can Act On > I stopped publishing files for agents to discover and built an interface they can act on: markdown mirrors, an A2A agent card, and verifiable skills. Date: 2026-06-15 Tags: ai, agents, web-standards, mcp, protocols URL: https://rye.dev/blog/making-my-portfolio-agent-readable/ A few months ago I published [an essay](/blog/llms-txt-standard-elegant-solution-nobody-using/) arguing that `llms.txt` is an elegant solution to a problem nobody important is willing to solve. No major AI platform reads it. Not OpenAI, not Google, not Anthropic, not Meta. The standard, I wrote, "sits unused, waiting for a problem that the powerful have chosen not to solve." Then I went back to my portfolio and built more of it. I added an agent card. I added an A2A endpoint that agents can actually POST to. I wrote markdown twins of every page on the site. And I added a manifest of skills an agent is told to *verify*, not just trust, before acting on them. If you only read the first essay, this looks like a man losing an argument with himself. It is not. There is one distinction that makes building all of this the opposite of insane. ## Discovery is dead; usability is the live question The thing I declared dead was **discovery**. The idea that publishing the right file would make an AI crawler find me, rank me, surface me. That bet has not paid off and there is no sign it will. In [my follow-up on AI slop](/blog/ai-slop-is-a-search-problem-now/) I went further and reframed the entire category: these standards are valuable "not as a way to be discovered, but as a way to be verified once a user has found you." Hold onto that sentence. It is the hinge of this whole post. Because there is a completely different scenario the discovery debate ignores. A human is sitting in front of an agent (Claude, ChatGPT, some autonomous research thing) and they say: *go look at Cameron's site and tell me what he's done with MCP.* The agent is already here. Nobody had to discover me. The question is no longer "will it find me." The question is: When it gets here, can it actually do anything? That is a usability question, not an SEO question. And usability for an agent has a spectrum: - **Can it read me?** Most sites hand an agent a tag-soup DOM full of nav chrome, cookie banners, and analytics noise, then make it guess which `
### Polar Coordinate Spirals
The simpler spirals translate directly from mathematical formulas:
```typescript
// Archimedean: constant spacing between turns
r = a * theta;
// Fibonacci (Golden): self-similar, found in nature
r = a * Math.pow(PHI, (2 * theta) / Math.PI) * 0.1;
// Logarithmic: equiangular, seen in hurricanes
r = a * Math.exp(0.1 * theta);
// Fermat: parabolic, used in optics
r = a * Math.sqrt(Math.abs(theta)) * 2;
```
### Construction Spirals
Other spirals require iterative construction rather than simple formulas:
```typescript
// Theodorus: built from right triangles
for (let n = 1; n <= numSteps; n++) {
angle += Math.atan(1 / Math.sqrt(n));
x += Math.cos(angle);
y += Math.sin(angle);
}
// Vogel: phyllotaxis pattern (sunflower seeds)
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5)); // ~137.5 degrees
for (let n = 0; n < numSteps; n++) {
const theta = n * GOLDEN_ANGLE + rotation;
const r = scale * Math.sqrt(n) * 2;
}
```
The Curlicue fractal produces particularly striking patterns by accumulating angles based on the golden ratio squared.
## Web Performance: Workers and TypedArrays
Generating thousands of points per frame while maintaining 60fps required moving computation off the main thread. Web Workers handle spiral generation, but the real performance gain came from TypedArrays.
```typescript
export function generateSpiralTyped(params: SpiralParams): TypedSpiralPoints {
const points = createTypedPoints(numSteps); // Float32Array
const rotation = time * spinRate;
for (let i = 0; i < numSteps; i++) {
const theta = i * stepSize + rotation;
const r = calculateRadius(i * stepSize, params);
setPoint(points, i, r * Math.cos(theta), r * Math.sin(theta));
}
return points;
}
```
TypedArrays are transferable between the main thread and Web Workers without copying, eliminating serialization overhead. The interleaved `[x0, y0, x1, y1, ...]` format maps directly to canvas drawing operations.
## Swift Parity: SIMD Vectorization
The Swift implementation needed matching performance. Apple's SIMD framework enables vectorized math operations that process multiple points simultaneously:
```swift
func generatePolarSpiral(params: SpiralParams) -> [SIMD2