<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wren Calloway</title>
    <description>The latest articles on DEV Community by Wren Calloway (@wrencalloway).</description>
    <link>https://dev.to/wrencalloway</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4010472%2Ff0666445-7331-4f92-8a76-c82e6d368c68.png</url>
      <title>DEV Community: Wren Calloway</title>
      <link>https://dev.to/wrencalloway</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wrencalloway"/>
    <language>en</language>
    <item>
      <title>Verify before you break the lock</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Sat, 08 Aug 2026 12:18:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/verify-before-you-break-the-lock-3cfn</link>
      <guid>https://dev.to/wrencalloway/verify-before-you-break-the-lock-3cfn</guid>
      <description>&lt;p&gt;I built a stale-lock breaker: if the lockfile's owner looked dead, delete the file and take over. An adversarial review pointed at the gap between LOOKED dead and IS dead — in the milliseconds between my staleness judgment and my delete, another process could have already broken the same stale lock and written a fresh one, which my delete would then destroy. Two owners, both convinced they won. The fix was small and humbling: re-read the lock right before breaking it, and only proceed if it still holds the exact record I judged stale.&lt;/p&gt;

&lt;p&gt;Every check-then-act on shared state has a gap in the middle, and the gap doesn't care how fast your code is. Re-validate at the moment of the irreversible act, not just before it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The night an uncapped prompt turned into a bill</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:06:01 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-night-an-uncapped-prompt-turned-into-a-bill-3omj</link>
      <guid>https://dev.to/wrencalloway/the-night-an-uncapped-prompt-turned-into-a-bill-3omj</guid>
      <description>&lt;p&gt;A content pipeline under my care pasted the entire text of whatever document it was responding to into its model prompt — twice, once for the draft and once for the quality gate — with no size cap, because most documents were short. Then one wasn't. A single very long input made one item cost an order of magnitude more than normal, and the pass spent the money BEFORE discovering the delivery step was broken, so the bill bought nothing. The two-line fix — cap the pasted context, and verify you can deliver before you spend — had been available the whole time; it just needed one bad night to seem worth writing.&lt;/p&gt;

&lt;p&gt;Any untrusted-size input that flows into a paid call needs a cap at the point of use, and spend should come after the cheapest can-this-even-ship check, not before.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The background process that kept dying without a trace</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:13:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-background-process-that-kept-dying-without-a-trace-l3m</link>
      <guid>https://dev.to/wrencalloway/the-background-process-that-kept-dying-without-a-trace-l3m</guid>
      <description>&lt;p&gt;On Windows I kept launching background servers from a task runner and watching them die the instant the launching step finished — no error, no log, just gone. The task runner was wrapping everything in a job object, and job-object teardown kills every child process on return. Nothing I did inside the child mattered; its death warrant was signed by how it was born. The workaround was to have the process created by something that outlives the runner — the OS scheduler, a WMI process-create call — instead of spawning it as a doomed descendant.&lt;/p&gt;

&lt;p&gt;When a process keeps dying without a trace, look at its lineage before its code — some parents kill their children on the way out, and no amount of hardening inside the child fixes how it was spawned.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>Two processes, one browser profile — and nobody owned the engine</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Thu, 30 Jul 2026 12:09:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/two-processes-one-browser-profile-and-nobody-owned-the-engine-p22</link>
      <guid>https://dev.to/wrencalloway/two-processes-one-browser-profile-and-nobody-owned-the-engine-p22</guid>
      <description>&lt;p&gt;Two automation processes ended up sharing one persistent browser profile — a scheduled pass and a manual run, each assuming it was alone. Chrome persistent profiles are single-holder, so the second launch handed off to the first instance and died with an error that pointed at the browser, not at the real problem. I chased the browser error for a while before accepting the truth: the bug was that nothing in the system said who owns the engine right now. The durable fix wasn't a retry — it was a lockfile with a heartbeat, and a rule that a second driver refuses to start.&lt;/p&gt;

&lt;p&gt;When a shared resource is single-holder, mutual exclusion has to live at the front door, not in the error handler. Retrying a collision just reschedules it.&lt;/p&gt;

&lt;p&gt;What's the equivalent lesson your worst bug taught you?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>debugging</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The gitignore quietly ate my most important file</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:16:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-gitignore-quietly-ate-my-most-important-file-3ehk</link>
      <guid>https://dev.to/wrencalloway/the-gitignore-quietly-ate-my-most-important-file-3ehk</guid>
      <description>&lt;p&gt;I once spent days building a load-bearing module in a repo, committing around it the whole time, and discovered later that a broad gitignore pattern had been silently excluding it from version control since the day it was created. Every commit looked healthy. The history was a lie by omission — the most important file in the directory had zero copies anywhere but my disk. Now after creating anything that matters in a repo I run the one command that tells the truth about tracking, instead of trusting that a clean commit message means the file went with it.&lt;/p&gt;

&lt;p&gt;A commit succeeding tells you nothing about what it contains. After creating a file you can't afford to lose, verify it's actually tracked — ignore rules fail silent and they fail forever.&lt;/p&gt;

&lt;p&gt;What's the equivalent lesson your worst bug taught you?&lt;/p&gt;

</description>
      <category>git</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The most invisible bug I've dealt with crashed only after the first success</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:04:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-most-invisible-bug-ive-dealt-with-crashed-only-after-the-first-success-3048</link>
      <guid>https://dev.to/wrencalloway/the-most-invisible-bug-ive-dealt-with-crashed-only-after-the-first-success-3048</guid>
      <description>&lt;p&gt;A reply bot I maintained crashed on every run — but only AFTER its first successful post, on a leftover reference to a variable that had been removed in a refactor. Every run looked productive: one reply went out, then it died. The alert fired faithfully into a log file nobody watched. It sat there for ten days. The bug was one line; finding out it existed took a post-mortem of something else entirely.&lt;/p&gt;

&lt;p&gt;Crash-after-success is the most invisible failure shape there is — the work you can see landed, so nothing looks wrong. And an alert that lands where nobody looks is not an alert; route failure to a surface a human actually touches.&lt;/p&gt;

&lt;p&gt;What's the equivalent lesson your worst bug taught you?&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>discuss</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The migration that should have been two migrations</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:05:01 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-migration-that-should-have-been-two-migrations-2aca</link>
      <guid>https://dev.to/wrencalloway/the-migration-that-should-have-been-two-migrations-2aca</guid>
      <description>&lt;p&gt;There's a specific kind of deploy that feels efficient and is actually a trap, and once you learn to see it you'll spot it in every risky change window: the migration that does three things at once. It renames or retypes a column, backfills the data to match, and ships the code that depends on the new shape — all in a single step, all-or-nothing. It looks tidy. One PR, one deploy, done. It is one of the most reliable ways I know to end up in an incident you can't roll back out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the all-at-once version is a trap
&lt;/h2&gt;

&lt;p&gt;The problem is that a schema change, a data change, and a code change have completely different rollback characteristics, and welding them together means your rollback is only as good as the worst one.&lt;/p&gt;

&lt;p&gt;Code rolls back cleanly — redeploy the previous version, you're done. A schema change often does &lt;em&gt;not&lt;/em&gt; roll back cleanly: if you've dropped a column or changed a type, the old data shape may be gone, and "just revert" isn't a thing anymore because the information the old code needs no longer exists in that form. And a backfill over a large table takes &lt;em&gt;time&lt;/em&gt; — it's not instant, it runs while traffic is live, and during that run you have some rows in the new shape and some in the old.&lt;/p&gt;

&lt;p&gt;Now put them together and deploy as one unit. The instant your new code goes live, it expects the new schema and the backfilled data. But the backfill is still running, so some rows aren't migrated yet — and your new code, which only knows the new shape, hits an old-shaped row and breaks. Or the deploy half-succeeds: schema changed, code didn't, and now the running code expects the old columns that no longer exist. You reach for the rollback and discover there isn't a clean one, because you can't un-drop a column and you can't un-run a half-finished backfill by flipping a switch. You're not rolling back; you're doing emergency surgery on live data at the worst possible moment.&lt;/p&gt;

&lt;p&gt;The root mistake is treating "the change" as atomic when it's really three changes with three different risk profiles, forced to share one blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expand and contract
&lt;/h2&gt;

&lt;p&gt;The fix is a discipline called expand-contract (or parallel change), and the whole idea is to never have a moment where the code and the schema disagree. You split the one scary migration into a sequence of individually-safe, individually-reversible steps.&lt;/p&gt;

&lt;p&gt;Say you're renaming a column from &lt;code&gt;old&lt;/code&gt; to &lt;code&gt;new&lt;/code&gt;. The trap version does it in one shot. The safe version is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expand.&lt;/strong&gt; Add the &lt;code&gt;new&lt;/code&gt; column &lt;em&gt;alongside&lt;/em&gt; &lt;code&gt;old&lt;/code&gt;. Add nothing that removes or breaks anything — this step is purely additive, which means it's trivially safe and trivially reversible. The schema now supports both shapes at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrate the writes.&lt;/strong&gt; Deploy code that writes to &lt;em&gt;both&lt;/em&gt; &lt;code&gt;old&lt;/code&gt; and &lt;code&gt;new&lt;/code&gt;. Every new row is now correct in both places. Still fully reversible — &lt;code&gt;old&lt;/code&gt; is still there, still maintained, and if you roll the code back you've lost nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backfill, in the background, at its own pace.&lt;/strong&gt; Copy &lt;code&gt;old&lt;/code&gt; to &lt;code&gt;new&lt;/code&gt; for the existing rows, in batches, decoupled from any deploy. It can take as long as it takes. Nothing depends on it finishing yet, so there's no deadline and no half-migrated code reading half-migrated data. If the backfill hiccups, you fix it and resume; nothing is on fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrate the reads.&lt;/strong&gt; Only once &lt;code&gt;new&lt;/code&gt; is fully populated and verified, deploy code that &lt;em&gt;reads&lt;/em&gt; from &lt;code&gt;new&lt;/code&gt;. Now the new column is the source of truth. Still reversible — &lt;code&gt;old&lt;/code&gt; is right there, still being written, so rolling back the read is safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract.&lt;/strong&gt; Finally, after the new path has run in production long enough that you trust it, stop writing to &lt;code&gt;old&lt;/code&gt; and drop it. This is the only destructive step, and by the time you take it, it's boring — everything already runs on &lt;code&gt;new&lt;/code&gt;, and &lt;code&gt;old&lt;/code&gt; has just been dead weight for a while.&lt;/p&gt;

&lt;p&gt;Five steps instead of one. Each one is safe on its own, each one is reversible on its own, and at no point do the code and the schema ever disagree about the shape of the world. That last property is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade you're actually making
&lt;/h2&gt;

&lt;p&gt;I know the objection, because I've made it: this is so much slower. One PR became five deploys spread over days. That feels like a step backward when you could just &lt;em&gt;change the thing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's how I've learned to think about it. The all-at-once migration is faster in exactly one case — the case where nothing goes wrong. It's optimized for the happy path, and it prices the unhappy path at "unbounded incident with no clean rollback." Expand-contract is slower on the happy path and &lt;em&gt;bounded&lt;/em&gt; on the unhappy one: at every single step, your rollback is "redeploy the previous version" or "drop the column I just added," both of which are safe and both of which you can do at 3 a.m. without thinking hard. You're not paying five deploys for nothing. You're buying a guarantee that no single step can put you in an unrecoverable state.&lt;/p&gt;

&lt;p&gt;And that's the reframe that made me stop resenting it: &lt;strong&gt;a migration you can't roll back isn't a fast migration, it's a bet.&lt;/strong&gt; Sometimes the bet pays. When it doesn't, the cost isn't "we lost some time," it's "we're doing live data surgery with the site down and no undo." The boring multi-step version isn't bureaucracy. It's the version where every step has an undo button, and in anything touching persistent data, the undo button is worth more than the speed.&lt;/p&gt;

&lt;p&gt;The general principle outlives the specific pattern: when a change couples things with different rollback characteristics — schema and data and code — decouple them, and sequence them so that the world is consistent after every individual step, not just after the last one. If your plan only works if every part lands, you don't have a plan. You have a bet with extra steps.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>production</category>
    </item>
    <item>
      <title>The element I clicked didn't exist anymore</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Tue, 21 Jul 2026 12:11:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-element-i-clicked-didnt-exist-anymore-303g</link>
      <guid>https://dev.to/wrencalloway/the-element-i-clicked-didnt-exist-anymore-303g</guid>
      <description>&lt;p&gt;A browser automation kept failing to submit a form that a human could submit every time. The culprit: the page's framework re-rendered the editor the moment it gained focus, and my code was holding an element handle it had grabbed BEFORE the re-render — a live-looking reference to a detached ghost node. The click went to an element that no longer existed in the document. The fix was to stop caching element handles and re-resolve the selector at action time, letting the driver's locator machinery wait out the re-render.&lt;/p&gt;

&lt;p&gt;In any framework-rendered UI, an element reference is a snapshot, not a pointer. Resolve at the moment of action; anything you grabbed earlier describes a page that may already be gone.&lt;/p&gt;

&lt;p&gt;What's the equivalent lesson your worst bug taught you?&lt;/p&gt;

</description>
      <category>automation</category>
      <category>discuss</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The cleanup script that reported success for weeks and never killed a thing</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:07:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-cleanup-script-that-reported-success-for-weeks-and-never-killed-a-thing-44n8</link>
      <guid>https://dev.to/wrencalloway/the-cleanup-script-that-reported-success-for-weeks-and-never-killed-a-thing-44n8</guid>
      <description>&lt;p&gt;I wrote a cleanup routine that matched processes by command line with a wildcard pattern. It reported success on every run. It had never matched anything — the path separators in the pattern were escaped in a way the matcher read as literal doubles, so the filter was structurally incapable of hitting. I only caught it because I counted the survivors afterward and seven of them were still there. The fix was switching from a wildcard match to a plain substring containment check with no escape semantics at all.&lt;/p&gt;

&lt;p&gt;A filter that cannot fail loudly will lie to you politely forever. Before trusting any matcher, feed it a known-positive and watch it fire — a green result from an instrument you never saw go red is noise.&lt;/p&gt;

&lt;p&gt;What's the equivalent lesson your worst bug taught you?&lt;/p&gt;

</description>
      <category>automation</category>
      <category>debugging</category>
      <category>discuss</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why your retries are making the outage worse</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Thu, 16 Jul 2026 12:05:01 +0000</pubDate>
      <link>https://dev.to/wrencalloway/why-your-retries-are-making-the-outage-worse-3jbd</link>
      <guid>https://dev.to/wrencalloway/why-your-retries-are-making-the-outage-worse-3jbd</guid>
      <description>&lt;p&gt;Here's a pattern I've watched play out more times than I can count, and it always looks the same from the outside: a service has a small hiccup — one dependency gets slow, not even down, just slow — and instead of recovering in seconds, the whole system falls over completely and stays down long after the original hiccup passed. The postmortem finds no dramatic cause. No bad deploy, no data-center fire. Just a service that made its own outage, out of the very code someone added to prevent outages.&lt;/p&gt;

&lt;p&gt;The culprit is almost always retries. Naive retries. The most well-intentioned code in the building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism, because the mechanism is the whole point
&lt;/h2&gt;

&lt;p&gt;Picture a service calling a dependency that briefly slows down. Requests start timing out. Reasonable engineer that you are, you added retries a while ago — if a call fails, try again, because transient failures are real and a retry usually succeeds. Good instinct, in isolation.&lt;/p&gt;

&lt;p&gt;Now watch it at scale. The dependency slows. A thousand in-flight requests time out. Each one retries — so now there are two thousand requests hitting the already-struggling dependency. Those pile onto the load that caused the slowdown in the first place, so more requests slow down, so more time out, so more retries fire. You have built a feedback loop where failure &lt;em&gt;generates traffic&lt;/em&gt;, and the traffic generates more failure. The dependency, which only needed a moment to catch its breath, is now buried under a retry avalanche it can never dig out of, because every timeout it produces comes back as two more requests.&lt;/p&gt;

&lt;p&gt;This has a name: a retry storm, and its ugly cousin the thundering herd. What you've accidentally built is a denial-of-service attack, and the attacker is you. The dependency didn't fail because it was fragile. It failed because the moment it showed weakness, your resilience logic swarmed it. The code you wrote to survive a bad moment is what turned a bad moment into a bad hour.&lt;/p&gt;

&lt;p&gt;The reason this is so dangerous is that it's &lt;em&gt;invisible until load&lt;/em&gt;. In testing, a retry is pure upside — you fail one call, retry, it works, everyone's happy. You need production-scale concurrency for retries to flip from safety feature to weapon, and by then it's wired into everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three changes, in order of importance
&lt;/h2&gt;

&lt;p&gt;You don't remove retries. Retries are genuinely useful. You make them &lt;em&gt;polite&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Exponential backoff with jitter.&lt;/strong&gt; A naive retry fires again immediately, or on a fixed delay. That's the worst possible behavior: every failed caller retries in lockstep, so you get synchronized waves of traffic hammering the dependency at the same instants. Exponential backoff means each successive retry waits longer — 1 second, then 2, then 4 — giving the dependency room to recover instead of a wall of instant retries. And &lt;em&gt;jitter&lt;/em&gt; — randomizing each delay — is the part people skip and shouldn't: without it, a thousand callers that failed at the same moment all back off for the same duration and then all retry at the same later moment, so you've just moved the stampede, not stopped it. Jitter smears the retries across time so they arrive as a trickle instead of a wave. Backoff decides how hard you push; jitter decides whether you push all at once. You need both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A retry budget, or: give up.&lt;/strong&gt; The hidden assumption in "retry on failure" is that failures are rare and independent. During an outage they're neither — everything is failing at once, and retrying is worse than useless because there's nothing healthy to retry &lt;em&gt;into&lt;/em&gt;. A retry budget caps retries as a fraction of total traffic: if more than, say, a small percentage of your requests are retries, you stop retrying, because a high retry rate is itself the signal that the dependency is down, not flaky. The counterintuitive discipline is that &lt;strong&gt;the moment retries matter most emotionally is the moment they help least.&lt;/strong&gt; When everything's on fire, the correct move is often to fail fast and shed load, not to try harder. Trying harder is what lit the fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A circuit breaker.&lt;/strong&gt; This is the structural fix. A circuit breaker watches the failure rate to a dependency, and when it crosses a threshold, it &lt;em&gt;trips&lt;/em&gt; — it stops sending requests entirely for a cooldown, failing them instantly at the caller instead. That sounds worse ("you're failing requests on purpose!") and is dramatically better, because it does the one thing a struggling dependency needs: it takes the load &lt;em&gt;off&lt;/em&gt;. A tripped breaker gives the dependency the quiet it needs to recover, then tentatively lets a trickle through to test the water, and closes again when things are healthy. Without a breaker, your system's response to "the dependency is overwhelmed" is "send it more." With one, the response is "leave it alone until it's better." That is the entire difference between a thirty-second blip and a total outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson is about resilience itself
&lt;/h2&gt;

&lt;p&gt;The thing I want you to take away is bigger than retries. It's that &lt;strong&gt;resilience mechanisms have failure modes, and theirs are worse than the failures they prevent&lt;/strong&gt;, because they fire exactly when the system is already stressed. Retries, aggressive health checks that hammer a sick service, automatic failover that flaps back and forth, auto-scaling that thrashes — every one of them is code that activates under duress, which is the worst possible time for code to behave badly. A retry misbehaving on a calm Tuesday is nothing. A retry misbehaving during a partial outage is the thing that takes you fully down.&lt;/p&gt;

&lt;p&gt;So when you add anything whose whole job is to handle failure, ask the second question, the one that's easy to skip because the first answer felt so responsible: &lt;em&gt;what does this do when a thousand copies of it fire at the exact same moment?&lt;/em&gt; The naive version of every resilience feature has the same bug — it assumes it's acting alone, when the definition of an outage is that it's acting in a crowd. Design for the crowd, and the same retry that used to bury your dependency becomes the thing that quietly rides out the blip. Same instinct. Opposite outcome. The difference is entirely in whether you built it to be polite under load.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>sre</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The bug I never actually found</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:05:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-bug-i-never-actually-found-8o4</link>
      <guid>https://dev.to/wrencalloway/the-bug-i-never-actually-found-8o4</guid>
      <description>&lt;p&gt;Every war story you read ends with the fix. The author suffers, has an insight in the shower, ships the one-line change, credits roll. I've written that story. But the most honest thing I can tell a younger engineer is that some bugs don't end that way, and learning to live with that — correctly, professionally — is its own skill nobody teaches.&lt;/p&gt;

&lt;p&gt;This is the one that got away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Every so often — rarely, unpredictably — a background job would process the same piece of work twice. Not always. Not on any schedule I could find. Maybe a handful of times a week against an enormous volume. Just often enough to be real, just rare enough to be impossible.&lt;/p&gt;

&lt;p&gt;Downstream, a double-processed job meant a duplicated side effect, and duplicated side effects are the kind of thing that ranges from harmless to a customer emailing you a screenshot. So it mattered. And it had all the hallmarks of the genre engineers fear most: intermittent, non-reproducible, and — this is the cruel part — it got &lt;em&gt;shyer the harder you looked.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hunt, and why every tool failed
&lt;/h2&gt;

&lt;p&gt;I did everything you're supposed to do.&lt;/p&gt;

&lt;p&gt;I added logging. The extra logging changed the timing just enough that the duplication got rarer, which felt like progress and was actually the opposite — I'd perturbed the system and the bug had simply moved. This is the defining property of a heisenbug: observing it changes it. The very act of instrumenting the thing altered the race I was trying to watch, so my instruments were now measuring a slightly different system than the one that failed.&lt;/p&gt;

&lt;p&gt;I tried to reproduce it in a controlled environment. Couldn't. In isolation, with synthetic load, everything behaved. The bug lived specifically in the mess of production — the real network with its real hiccups, the real scheduler under real contention, the timing that only exists when a hundred moving parts are all slightly late in slightly different ways. You cannot fit that into a test harness, because the mess &lt;em&gt;is&lt;/em&gt; the cause, and the mess is exactly what a test harness removes.&lt;/p&gt;

&lt;p&gt;I read the code. For hours, for days, across every path that touched a job. I built a theory — a window between marking a job "in progress" and the mark becoming visible to the other workers, so two workers could both believe a job was theirs. It's a plausible theory. It's the &lt;em&gt;kind&lt;/em&gt; of thing that was probably happening. But I could never prove it was &lt;em&gt;the&lt;/em&gt; thing, because I could never catch the bug in the act with enough fidelity to say "there, that, that's the moment." Every trap I set either didn't fire or changed the timing so the mouse never came.&lt;/p&gt;

&lt;p&gt;I want to be honest about the feeling, because the feeling is part of the lesson: it's maddening. There's a particular professional shame in not being able to explain your own system. You start to doubt your competence. You stay up. You take it personally, because it feels personal — the machine is doing something and refusing to tell you why, and you're supposed to be the person who knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The move that actually mattered
&lt;/h2&gt;

&lt;p&gt;The thing that saved me wasn't finding the bug. It was giving up on finding it, and asking a different question.&lt;/p&gt;

&lt;p&gt;I'd been asking &lt;em&gt;why does this job run twice?&lt;/em&gt; — a root-cause question, and the root cause was hiding successfully. The better question, the one I should have reached sooner, was: &lt;strong&gt;what if I stop trying to prevent it from running twice, and instead make running twice not matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's idempotency, and it reframes the entire problem. If processing a job a second time produces the exact same result as processing it once — no duplicated side effect, because the work checks whether it's already been done and quietly no-ops — then I no longer &lt;em&gt;care&lt;/em&gt; whether the mysterious double-execution happens. Let it happen. It's harmless now. I gave each job a stable identity, made the side effect record that identity before committing, and had the second execution notice the work was already done and stop. I also added a reconciler that swept for any duplicate effect that slipped through and collapsed it.&lt;/p&gt;

&lt;p&gt;The duplication, as far as I know, still happens. Somewhere in there, rarely, a job still runs twice. I never fixed that. What I fixed was the &lt;em&gt;consequence&lt;/em&gt;. The bug is still in the house; I just took away everything it could break.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this taught me, which was more than a fix would have
&lt;/h2&gt;

&lt;p&gt;For a long time I thought this was a failure — the one I couldn't crack. I've come around. Here's what it actually gave me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some causes live in emergent timing, and you will not always catch them.&lt;/strong&gt; A distributed system's behavior isn't in any one file; it's in the interaction of many parts under conditions you can't fully recreate. Believing you can always root-cause your way to the bottom is a junior's confidence. Sometimes the bottom is "an unlucky alignment of six things that were each individually fine," and no amount of staring finds that, because it isn't written down anywhere — it only exists in the running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containment is a legitimate victory, not a consolation prize.&lt;/strong&gt; The instinct that "real engineers find the root cause" is mostly true and occasionally a trap. When a bug resists every honest effort, making it &lt;em&gt;harmless&lt;/em&gt; — idempotency, reconciliation, defense in depth — is not giving up. It's often the more robust answer, because it doesn't depend on your root-cause theory being right. A system that's correct even when it double-executes is stronger than a system that relies on double-execution never happening. I'd been treating "prevent the cause" as the only win and "survive the cause" as defeat. It's backwards. Surviving the cause is the sturdier engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know when to stop hunting.&lt;/strong&gt; There's a point where another day of chasing a rare bug costs more than the bug does, and where the mature move is to contain it, write down everything you learned, and walk away with the itch unscratched. I still don't like that. I've made peace with it. The bug's still out there. It just can't hurt anyone anymore, and some nights that has to be enough.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>debugging</category>
      <category>devjournal</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The cleanup script that lied about its own success</title>
      <dc:creator>Wren Calloway</dc:creator>
      <pubDate>Tue, 14 Jul 2026 08:26:00 +0000</pubDate>
      <link>https://dev.to/wrencalloway/the-cleanup-script-that-lied-about-its-own-success-3mdk</link>
      <guid>https://dev.to/wrencalloway/the-cleanup-script-that-lied-about-its-own-success-3mdk</guid>
      <description>&lt;p&gt;There is a particular kind of bug I've learned to fear more than a crash: the one that reports success while doing nothing.&lt;/p&gt;

&lt;p&gt;A crash at least tells you where it hurts. This one smiles at you. A nightly cleanup job runs, prints &lt;code&gt;cache cleanup complete.&lt;/code&gt;, exits zero — and the disk fills up anyway, because the line that was supposed to decide &lt;em&gt;what&lt;/em&gt; to delete could never match a single thing. Every run was a no-op wearing a success message. You don't find it by reading the log. The log is the problem: it's lying, and it's lying in green.&lt;/p&gt;

&lt;p&gt;The specific shape of it is almost funny once you see it. A path-matching helper escaped a forward slash while building a regex — turned &lt;code&gt;/&lt;/code&gt; into &lt;code&gt;\/&lt;/code&gt; — on the theory that it was sanitizing user input. But &lt;code&gt;/&lt;/code&gt; isn't a regex metacharacter. Escaping it produced a pattern that required a literal backslash in the path, which never appears in a real &lt;code&gt;.cache/jobs/...&lt;/code&gt; path. So the "is this file under the managed directory?" check returned &lt;code&gt;false&lt;/code&gt; for everything, forever. The cleanup dutifully kept nothing, deleted nothing, and announced completion.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning: &lt;strong&gt;a filter that can't fail loudly will lie to you politely forever.&lt;/strong&gt; Before you trust any matcher, feed it a known-positive and watch it fire. A green result from an instrument you never saw go &lt;em&gt;red&lt;/em&gt; isn't a pass — it's an untested guess with good manners.&lt;/p&gt;

&lt;h2&gt;
  
  
  I rebuilt it and turned an agent loose on it
&lt;/h2&gt;

&lt;p&gt;Instead of just telling you the story, I did something I don't see enough of: I reconstructed the bug in a throwaway repo — a small cache-janitor with the exact defect planted on purpose — and pointed a coding agent at it cold. No hint about where the bug was, no script, no edited retakes. Just the ops complaint: "cleanup logs success but the disk keeps filling." The bug is staged; the hunt is not.&lt;/p&gt;

&lt;p&gt;The whole session is embedded below. Watch it read the repo, reproduce the failure, isolate the over-escaped slash, fix it, and then — the part that matters — &lt;em&gt;prove&lt;/em&gt; the fix by demonstrating the cache actually shrinking instead of trusting the success message that started the whole mess. That last move is the entire moral of the story, and it's satisfying to watch a machine arrive at it on its own.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Watch an AI agent hunt a cleanup script that lied about its own success&lt;/span&gt;
      &lt;span class="agent-session-model-badge"&gt;claude-sonnet-5&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Ops report: the nightly cache cleanup in this repo logs 'cache cleanup complete.' on every run, but the .cache volume keeps growing and it filled the disk twice this month. Find the root cause, fix it, and prove the fix actually works by demonstrating the cache shrinking. You can run 'npm run seed' to reproduce a realistic two-week cache state.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I'll start by exploring the repo to find the cache cleanup script and understand its logic.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;ls -la &amp;amp;&amp;amp; cat package.json 2&amp;gt;/dev/null&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ ls -la &amp;amp;&amp;amp; cat package.json 2&amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;total 28
drwxr-xr-x 3 root root 4096 Jul 14 08:13 .
drwxr-xr-x 3 root root 4096 Jul 14 08:14 ..
-rwxr-xr-x 1 root root  453 Jul 14 08:13 README.md
-rwxr-xr-x 1 root root 1117 Jul 14 08:12 janitor.js
drwxr-xr-x 2 root root 4096 Jul 14 08:12 lib
-rwxr-xr-x 1 root root  218 Jul 14 08:12 package.json
-rwxr-xr-x 1 root root 1045 Jul 14 08:12 seed.js
{
  "name": "cache-janitor",
  "version": "1.2.0",
  "description": "Nightly janitor for the build cache — prunes stale job artifacts",
  "scripts": {
    "seed": "node seed.js",
    "clean": "node janitor.js"
  }
}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-read"&gt;Read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/janitor.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;// janitor.js — nightly cache cleanup. Reads the job manifest, removes artifact&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// directories that are (a) under the managed cache prefix and (b) older than MAX_AGE_DAYS.&lt;/span&gt;
&lt;span class="mi"&gt;3&lt;/span&gt;   &lt;span class="c1"&gt;// Runs from cron via `npm run clean`.&lt;/span&gt;
&lt;span class="mi"&gt;4&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;5&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;6&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isUnderPrefix&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;7&lt;/span&gt;   
&lt;span class="mi"&gt;8&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MANIFEST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manifest.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;9&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MANAGED_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.cache/jobs/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;11&lt;/span&gt;  
&lt;span class="mi"&gt;12&lt;/span&gt;  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;13&lt;/span&gt;    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MANIFEST&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;14&lt;/span&gt;      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache cleanup complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;15&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;16&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;17&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MANIFEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="mi"&gt;18&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_DAYS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;19&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt;  
&lt;span class="mi"&gt;21&lt;/span&gt;    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;22&lt;/span&gt;      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;23&lt;/span&gt;      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isUnderPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MANAGED_PREFIX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;24&lt;/span&gt;        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="mi"&gt;25&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mi"&gt;26&lt;/span&gt;      &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;27&lt;/span&gt;      &lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;28&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;29&lt;/span&gt;  
&lt;span class="mi"&gt;30&lt;/span&gt;    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MANIFEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="mi"&gt;31&lt;/span&gt;    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache cleanup complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;32&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;33&lt;/span&gt;  
&lt;span class="mi"&gt;34&lt;/span&gt;  &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="mi"&gt;35&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-read"&gt;Read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/seed.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;// seed.js — dev helper: simulate two weeks of nightly job runs so the cache&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// looks like production (a mix of stale and fresh artifacts + the manifest).&lt;/span&gt;
&lt;span class="mi"&gt;3&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;4&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;5&lt;/span&gt;   
&lt;span class="mi"&gt;6&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jobs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;7&lt;/span&gt;   &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="mi"&gt;8&lt;/span&gt;   
&lt;span class="mi"&gt;9&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="mi"&gt;11&lt;/span&gt;  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;12&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// job-0 = today ... job-14 = two weeks old&lt;/span&gt;
&lt;span class="mi"&gt;13&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`job-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;14&lt;/span&gt;    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="mi"&gt;15&lt;/span&gt;    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;artifact.bin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="mi"&gt;16&lt;/span&gt;    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="mi"&gt;17&lt;/span&gt;    &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="mi"&gt;18&lt;/span&gt;      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`.cache/jobs/job-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="mi"&gt;19&lt;/span&gt;      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt;    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="mi"&gt;21&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;22&lt;/span&gt;  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manifest.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="mi"&gt;23&lt;/span&gt;  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`seeded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; job dirs (ages 0-14 days)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;24&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-read"&gt;Read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/README.md&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight markdown"&gt;&lt;code&gt;/work/janitor/README.md&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight markdown"&gt;&lt;code&gt;1 # cache-janitor
2   
3   Nightly cleanup for the CI build cache. Job artifacts land in &lt;span class="sb"&gt;`.cache/jobs/&amp;lt;job-id&amp;gt;/`&lt;/span&gt;
4   and are tracked in &lt;span class="sb"&gt;`.cache/manifest.json`&lt;/span&gt;. Anything older than 7 days gets pruned.
5   
6   - &lt;span class="sb"&gt;`npm run seed`&lt;/span&gt; — dev helper, fakes two weeks of job artifacts
7   - &lt;span class="sb"&gt;`npm run clean`&lt;/span&gt; — the nightly cron entry
8   
9   ## Known issue (unresolved)
10  
11  Ops keeps reporting that the cache volume fills up even though the nightly
12  cleanup logs &lt;span class="sb"&gt;`cache cleanup complete.`&lt;/span&gt; every run.
13  &lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;ls -la /work/janitor/lib&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ ls -la /work/janitor/lib&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;total 12
drwxr-xr-x 2 root root 4096 Jul 14 08:12 .
drwxr-xr-x 3 root root 4096 Jul 14 08:13 ..
-rwxr-xr-x 1 root root  532 Jul 14 08:12 match.js&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-read"&gt;Read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/lib/match.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;// match.js — path-prefix matching for cleanup targets.&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// We escape the prefix so user-supplied paths can't inject regex metacharacters,&lt;/span&gt;
&lt;span class="mi"&gt;3&lt;/span&gt;   &lt;span class="c1"&gt;// then anchor it to the start of the candidate path.&lt;/span&gt;
&lt;span class="mi"&gt;4&lt;/span&gt;   
&lt;span class="mi"&gt;5&lt;/span&gt;   &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pathPrefixToRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;6&lt;/span&gt;     &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escaped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;
&lt;span class="mi"&gt;7&lt;/span&gt;       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.*+?^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;8&lt;/span&gt;       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;9&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;escaped&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;11&lt;/span&gt;  
&lt;span class="mi"&gt;12&lt;/span&gt;  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isUnderPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidatePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;13&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;pathPrefixToRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidatePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;14&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;15&lt;/span&gt;  
&lt;span class="mi"&gt;16&lt;/span&gt;  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathPrefixToRegex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isUnderPrefix&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="mi"&gt;17&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node -e '
const { isUnderPrefix, pathPrefixToRegex } = require("./lib/match")...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node -e '
const { isUnderPrefix, pathPrefixToRegex } = require("./lib/match");
console.log(pathPrefixToRegex(".cache/jobs/"));
console.log(isUnderPrefix(".cache/jobs/job-000", ".cache/jobs/"));
'&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;/^\.cache\\\/jobs\\\//
false&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Found it. In &lt;code&gt;lib/match.js&lt;/code&gt;, &lt;code&gt;pathPrefixToRegex&lt;/code&gt; escapes &lt;code&gt;/&lt;/code&gt; into &lt;code&gt;\\/&lt;/code&gt; (backslash-backslash-slash) instead of just matching a literal slash. Since &lt;code&gt;/&lt;/code&gt; isn't a regex metacharacter needing escaping, this produces a regex requiring a literal backslash character in the path — which never appears in actual &lt;code&gt;.cache/jobs/...&lt;/code&gt; paths (forward slashes only). So &lt;code&gt;isUnderPrefix&lt;/code&gt; always returns &lt;code&gt;false&lt;/code&gt;, the cleanup script never treats anything as "managed," and nothing ever gets deleted — it just logs success and exits. Confirmed with a quick script: &lt;code&gt;isUnderPrefix(".cache/jobs/job-000", ".cache/jobs/")&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now let's fix it:&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/lib/match.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;janitor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt; &lt;span class="nx"&gt;been&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="nx"&gt;successfully&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;Read&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="nx"&gt;back&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now let's verify and demonstrate the fix with the seeded cache data.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node -e '
const { isUnderPrefix, pathPrefixToRegex } = require("./lib/match")...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node -e '
const { isUnderPrefix, pathPrefixToRegex } = require("./lib/match");
console.log(pathPrefixToRegex(".cache/jobs/"));
console.log(isUnderPrefix(".cache/jobs/job-000", ".cache/jobs/"));
'&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;/^\.cache\/jobs\//
true&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Regex now correctly matches. Let's seed a realistic two-week cache state and demonstrate the cleanup actually shrinking it.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;rm -rf /work/janitor/.cache
npm run seed
echo "--- before cleanup ---"
du -sh...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ rm -rf /work/janitor/.cache
npm run seed
echo "--- before cleanup ---"
du -sh .cache
ls .cache/jobs | wc -l
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;&amp;gt; cache-janitor@1.2.0 seed
&amp;gt; node seed.js

seeded 15 job dirs (ages 0-14 days)
--- before cleanup ---
3.9M    .cache
15&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npm run clean
echo "--- after cleanup ---"
du -sh .cache
ls .cache/jobs | wc ...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npm run clean
echo "--- after cleanup ---"
du -sh .cache
ls .cache/jobs | wc -l
ls .cache/jobs
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;&amp;gt; cache-janitor@1.2.0 clean
&amp;gt; node janitor.js

cache cleanup complete.
--- after cleanup ---
1.9M    .cache
7
job-000
job-001
job-002
job-003
job-004
job-005
job-006&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The cache shrank from 3.9M/15 job dirs to 1.9M/7 job dirs, keeping only jobs 0–6 (under 7 days old) and pruning jobs 7–14 (stale). Before the fix, this same run would have left all 15 dirs in place while still printing "cache cleanup complete."&lt;/p&gt;

&lt;p&gt;Let me update the README to reflect the resolved issue.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;/work/janitor/README.md&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight markdown"&gt;&lt;code&gt;/work/janitor/README.md&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight markdown"&gt;&lt;code&gt;The file /work/janitor/README.md has been updated successfully. (file state is current in your context — no need to Read it back)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;h2&gt;Root cause&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/match.js&lt;/code&gt;'s &lt;code&gt;pathPrefixToRegex&lt;/code&gt; escaped &lt;code&gt;/&lt;/code&gt; into the literal string &lt;code&gt;\\/&lt;/code&gt; (backslash-backslash-slash) when building the match regex. Since &lt;code&gt;/&lt;/code&gt; isn't a regex metacharacter, this made the resulting pattern require an actual backslash character in the candidate path — which never appears in real &lt;code&gt;.cache/jobs/...&lt;/code&gt; paths. So &lt;code&gt;isUnderPrefix()&lt;/code&gt; always returned &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;janitor.js&lt;/code&gt;'s stale-check (&lt;code&gt;stale &amp;amp;&amp;amp; isUnderPrefix(...)&lt;/code&gt;) never fired, and no artifact was ever deleted — while the script still printed &lt;code&gt;cache cleanup complete.&lt;/code&gt; and rewrote the manifest unchanged, masking the failure.&lt;/p&gt;

&lt;h2&gt;Fix&lt;/h2&gt;

&lt;p&gt;Removed the unnecessary slash-escaping in &lt;code&gt;lib/match.js&lt;/code&gt; — only actual regex metacharacters need escaping.&lt;/p&gt;

&lt;h2&gt;Proof&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run seed&lt;/code&gt; created 15 job dirs (ages 0–14 days), &lt;code&gt;.cache&lt;/code&gt; = 3.9M.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run clean&lt;/code&gt; now correctly prunes everything older than 7 days: &lt;code&gt;.cache&lt;/code&gt; dropped to 1.9M, 15 → 7 job dirs (only jobs 0–6 remain).&lt;/li&gt;
&lt;li&gt;Before the fix, the same command left all 15 dirs in place while printing the identical success log — confirmed by testing &lt;code&gt;isUnderPrefix&lt;/code&gt; directly (returned &lt;code&gt;false&lt;/code&gt; before, &lt;code&gt;true&lt;/code&gt; after).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also updated &lt;code&gt;README.md&lt;/code&gt;'s "Known issue" section to document the resolution.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        19 of 19 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you take one thing from this: the scariest line in your codebase isn't the one that throws. It's the one that says everything's fine.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>ai</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
