<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Clay Tercek</title><description>Writing about software development. Mostly tooling, caching, and things that bother me.</description><link>https://tercek.me/</link><language>en-us</language><atom:link href="https://tercek.me/blog/rss.xml" rel="self" type="application/rss+xml"/><copyright>© 2026 Clay Tercek</copyright><managingEditor>hello@tercek.me (Clay Tercek)</managingEditor><webMaster>hello@tercek.me (Clay Tercek)</webMaster><ttl>1440</ttl><item><title>My Clearest Briefs Go to a Robot</title><link>https://tercek.me/blog/my-clearest-briefs-go-to-a-robot/</link><guid isPermaLink="true">https://tercek.me/blog/my-clearest-briefs-go-to-a-robot/</guid><description>&quot;Prompt engineering&quot; is briefing with a new name. LLMs are the first audience that punishes vague writing instantly, so everyone quietly got better at it. Then they send the inflated output and keep the good part private.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The clearest briefs I&apos;ve ever written have all been addressed to a robot.&lt;/p&gt;
&lt;p&gt;When I hand a task to Claude, I explain the background, spell out the constraints, define what done looks like, and call out the edge cases I&apos;m worried about. I&apos;ll even list the things I &lt;em&gt;don&apos;t&lt;/em&gt; want it to do. It&apos;s some of the best technical writing I produce all week.&lt;/p&gt;
&lt;p&gt;Nobody I actually work with gets that treatment. Humans get &quot;hey, can you take a look at this when you get a chance?&quot;&lt;/p&gt;
&lt;h2&gt;The robot is a brutal audience&lt;/h2&gt;
&lt;p&gt;Why does the robot get the good version? Because it punishes vague writing instantly. Hand a colleague an underspecified task and they&apos;ll nod, fill in the gaps from shared context, maybe ask a clarifying question a day later. Hand a model an underspecified task and it confidently does the wrong thing, right in front of you, in about thirty seconds.&lt;/p&gt;
&lt;p&gt;That&apos;s the tightest feedback loop on unclear communication anyone has ever had. &quot;Prompt engineering&quot; gets dressed up as a new technical discipline, but strip away the mystique and it&apos;s just briefing: context, goal, constraints, examples of what good looks like. The stuff we always owed each other and mostly skipped, because a human recipient absorbs the ambiguity for free and the cost stays invisible.&lt;/p&gt;
&lt;p&gt;I ran into this same pattern with &lt;a href=&quot;/blog/agents-md-is-not-readme&quot;&gt;agent config files&lt;/a&gt;: people who never got around to writing a README will happily fill an AGENTS.md with build steps, conventions, and architecture notes. The robot asked, so the docs finally got written. Turns out that wasn&apos;t just about docs.&lt;/p&gt;
&lt;h2&gt;And yet the inbox got worse&lt;/h2&gt;
&lt;p&gt;Here&apos;s the part that gets me. Everyone leveled up at written communication at the same moment, and the messages showing up in my inbox got &lt;em&gt;worse&lt;/em&gt;. Longer, rounder, padded with headers and bullet points nobody chose.&lt;/p&gt;
&lt;p&gt;Because the good writing stays private. You write a tight three-sentence brief, hand it to the model, and the model inflates it into four polite paragraphs. Then you send the four paragraphs and keep the three sentences.&lt;/p&gt;
&lt;p&gt;Andrew Gelman was &lt;a href=&quot;https://statmodeling.stat.columbia.edu/2026/08/09/people-keep-sending-me-ai-slop-that-they-want-me-to-post-on-the-blog/&quot;&gt;complaining about exactly this&lt;/a&gt; on his blog just yesterday: &quot;Instead of sending me the chatbot output, just send me your goddamn prompt.&quot; He&apos;s right, and the reason he&apos;s right is simple: everything you wanted to say was already in the prompt. The model didn&apos;t add information, it spread the same information across more words. So now I have to read your four paragraphs trying to reconstruct the three sentences you started from. Plenty of people don&apos;t even do that anymore, they paste the message into an LLM and ask for a summary. Inflate on your end, deflate on mine, lossy in both directions. We&apos;ve invented the world&apos;s slowest way to send three sentences.&lt;/p&gt;
&lt;p&gt;Niklas Gruhn has a name for the person doing the forwarding: a &lt;a href=&quot;https://gruhn.me/blog/2026-08-03/&quot;&gt;meat proxy&lt;/a&gt;. If all you did was carry the model&apos;s output to my inbox, you added nothing but latency. And as he points out, if I wanted a chatbot&apos;s take, &quot;I can talk to Claude myself. It&apos;s going to be faster and I get to control the context.&quot;&lt;/p&gt;
&lt;h2&gt;The rule&lt;/h2&gt;
&lt;p&gt;Before you hit send, ask which artifact carries more information per word: the output, or the prompt that produced it? If it&apos;s the prompt, send the prompt. Not as a transcript with an apology attached. Just send the message you typed, because that was the actual message all along.&lt;/p&gt;
&lt;p&gt;The corollary is less comfortable. If you&apos;d be embarrassed to send the prompt (&quot;make this sound professional,&quot; &quot;write an update saying things are on track&quot;), the polished version has nothing to say either. The polish is laundering.&lt;/p&gt;
&lt;h2&gt;When the output is the message&lt;/h2&gt;
&lt;p&gt;To be fair, sometimes the model genuinely adds information: it researched something you didn&apos;t know and you verified it, it wrote code that runs, it boiled ten documents down to a real decision. In those cases the output carries more than the prompt did, so send the output. The sin was never using AI. The sin is sending text that contains less information than the prompt that produced it.&lt;/p&gt;
&lt;h2&gt;Aim it at people&lt;/h2&gt;
&lt;p&gt;You already know how to write. The proof is sitting in your prompt history: context up front, constraints named, done defined. You&apos;ve been doing it every day, for an audience that gives you nothing back but compliance.&lt;/p&gt;
&lt;p&gt;Next time, skip the middleman. Send the brief.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://statmodeling.stat.columbia.edu/2026/08/09/people-keep-sending-me-ai-slop-that-they-want-me-to-post-on-the-blog/&quot;&gt;Andrew Gelman, &quot;People keep sending me AI slop that they want me to post on the blog&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gruhn.me/blog/2026-08-03/&quot;&gt;Niklas Gruhn, &quot;Don&apos;t be a meat proxy&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/agents-md-is-not-readme&quot;&gt;AGENTS.md Is Not a README&lt;/a&gt; — the docs version of this same argument&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item><item><title>A Field Guide to Zig Allocators</title><link>https://tercek.me/blog/zig-allocators/</link><guid isPermaLink="true">https://tercek.me/blog/zig-allocators/</guid><description>Zig hands you the memory strategy as a plain value you pass around. Here&apos;s a tour of the ones you&apos;d actually pass in, with an interactive demo for each.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been learning Zig, and the thing that stuck with me isn&apos;t comptime or the syntax everyone posts about. It&apos;s that the language flat out refuses to allocate memory behind your back.&lt;/p&gt;
&lt;p&gt;In most languages, memory just appears. You write &lt;code&gt;new Thing()&lt;/code&gt; or &lt;code&gt;malloc(64)&lt;/code&gt;, or you let the garbage collector sort it out, and where those bytes actually came from is somebody else&apos;s problem. Zig makes a different call: any function that needs to allocate takes an &lt;code&gt;Allocator&lt;/code&gt; as a parameter. If a function doesn&apos;t take one, it can&apos;t allocate. You can tell what a function does to memory from its signature alone.&lt;/p&gt;
&lt;p&gt;Sounds like a chore. In practice it&apos;s the most interesting decision in the language, because it means the &lt;em&gt;strategy&lt;/em&gt; for handing out memory is just a value you pass around. Swap the value, swap the behavior, and the code in between never changes.&lt;/p&gt;
&lt;p&gt;That&apos;s the part I want to dig into here. Not &quot;how do I allocate in Zig,&quot; but the handful of strategies you&apos;d actually pass in: what each one is good at, and where each one bites you. There&apos;s a little interactive strip in each section. Tap a size to allocate, tap a block to free it, or drag a block&apos;s right edge to resize it, and watch how the same requests play out differently depending on who&apos;s handing out the memory.&lt;/p&gt;
&lt;h2&gt;One interface, many strategies&lt;/h2&gt;
&lt;p&gt;The whole thing hangs on one small type. Simplified a little, &lt;code&gt;std.mem.Allocator&lt;/code&gt; is a pointer plus a table of function pointers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub const Allocator = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        alloc: *const fn (ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8,
        resize: *const fn (ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool,
        remap: *const fn (ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8,
        free: *const fn (ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void,
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four functions. &lt;code&gt;alloc&lt;/code&gt; hands you bytes, &lt;code&gt;free&lt;/code&gt; gives them back, &lt;code&gt;resize&lt;/code&gt; and &lt;code&gt;remap&lt;/code&gt; try to grow or shrink in place. Every allocator in the standard library, and every one you write yourself, is just a different implementation of those four.&lt;/p&gt;
&lt;p&gt;So a function that needs memory doesn&apos;t care which one it got:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn readLines(allocator: std.mem.Allocator, path: []const u8) ![][]u8 {
    // ... uses allocator.alloc, allocator.free, and friends
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Call it with an arena and every line lives until you drop the arena. Call it with a general purpose allocator and you free each line yourself. Same function. The caller picks the memory strategy, not the library. That inversion is the whole point, and once it clicks you start seeing the plain &lt;code&gt;Allocator&lt;/code&gt; as a slot you drop a strategy into.&lt;/p&gt;
&lt;p&gt;Here&apos;s the lineup.&lt;/p&gt;
&lt;h2&gt;The bump allocator: fast, and it never gives anything back&lt;/h2&gt;
&lt;p&gt;Start with the simplest thing that could possibly work. You have a block of memory and a pointer to the next free spot. Every allocation hands back the pointer and shoves it forward by the requested size. That&apos;s it. That&apos;s the whole allocator.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Allocate a few blocks and you can watch the region fill from the left. Now tap one to free it. Nothing happens, and the demo tells you why: a bump allocator has no idea how to reclaim one allocation out of the middle. There&apos;s a single pointer and it only goes forward. You can drag the &lt;em&gt;top&lt;/em&gt; block&apos;s edge to grow it, since that&apos;s just nudging the pointer a little further, but the blocks behind it don&apos;t even have a handle to grab, and the only way to get memory back is to reset the whole thing, which frees &lt;em&gt;everything&lt;/em&gt; at once.&lt;/p&gt;
&lt;p&gt;That sounds broken until you notice how often it&apos;s exactly what you want. Zig calls this an arena:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const a = try allocator.alloc(u8, 64);
const b = try allocator.alloc(u8, 128);
// no free() anywhere. deinit() drops all of it in one shot.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Think about handling a single web request. You allocate a pile of temporary junk, parse some JSON, build a response, and the second the request is done, all of it is garbage. You don&apos;t want to free thirty things in the right order. You want to wipe the slate. An arena turns thirty frees into one, and every allocation in between costs about as much as bumping a pointer.&lt;/p&gt;
&lt;p&gt;The tradeoff is right there in the demo: you can&apos;t free just one thing. If some allocations in an arena are short lived and others need to stick around, the long lived ones pin the whole region, and your &quot;temporary&quot; memory quietly becomes permanent until the reset. Arenas are for phases with a clear end, not for objects with their own individual lifetimes.&lt;/p&gt;
&lt;p&gt;(A stack allocator is the close cousin here: same bump pointer, but it lets you free the most recent allocation and roll the pointer back, as long as you free in reverse order. You&apos;ll see exactly that behavior in the next section.)&lt;/p&gt;
&lt;h2&gt;The fixed buffer allocator: same idea, no heap at all&lt;/h2&gt;
&lt;p&gt;Take that bump allocator and hand it a buffer that lives on the stack. No &lt;code&gt;malloc&lt;/code&gt;, no OS, no heap. Just an array and an offset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&amp;amp;buf);
const allocator = fba.allocator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything about the bump behavior carries over, with one hard new rule: the buffer is a fixed size and it never grows. Fill it up and the next allocation returns &lt;code&gt;error.OutOfMemory&lt;/code&gt;. The demo below is deliberately tiny so you can hit the wall fast.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Keep allocating and you&apos;ll run out of room, and the allocator says so instead of quietly grabbing more from somewhere. That failure is a feature. On an embedded target with no heap, or in a hot path where you refuse to touch the OS allocator, a fixed buffer gives you a hard, visible ceiling. You know at compile time exactly how much memory this code can use, because you&apos;re the one who declared it.&lt;/p&gt;
&lt;p&gt;One nuance the demo shows: a &lt;code&gt;FixedBufferAllocator&lt;/code&gt; &lt;em&gt;can&lt;/em&gt; free, but only the most recent allocation, because that&apos;s the only one it can prove sits right at the top of the buffer. Free the last thing and the offset rolls back. Try to free something in the middle and it can&apos;t, for the same reason the arena couldn&apos;t. That &quot;free the top&quot; move is the stack allocator I mentioned, hiding in plain sight. Growing works the same way: drag the top block&apos;s edge and the offset slides up, right until it would run off the end of the buffer, where you hit the wall again.&lt;/p&gt;
&lt;h2&gt;The pool allocator: one size, no fragmentation, ever&lt;/h2&gt;
&lt;p&gt;Everything so far allocates arbitrary sizes and can only free from the end. The pool goes the other direction. It commits to exactly one size up front, and in exchange it lets you free anything, in any order, for basically free.&lt;/p&gt;
&lt;p&gt;The trick is that if every slot is identical, freeing one just puts it back on a list of empty slots. Allocating grabs the first one off that list. No searching, no measuring, no leftover gaps that don&apos;t fit anything. Both operations are O(1), and the memory can never fragment, because there&apos;s nothing to fragment: every hole is exactly the right size for the next request, by construction.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Allocate and free in whatever order you like. Slots light up and go dark, and the layout never gets messy, because it &lt;em&gt;can&apos;t&lt;/em&gt;. In Zig this is &lt;code&gt;MemoryPool&lt;/code&gt;, parameterized by the type it hands out:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Node = struct { value: i32, next: ?*Node };

var pool = std.heap.MemoryPool(Node).init(std.heap.page_allocator);
defer pool.deinit();

const n1 = try pool.create();
const n2 = try pool.create();
pool.destroy(n1); // n1&apos;s slot goes straight back on the free list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the allocator you reach for when you&apos;re churning through a lot of one thing: nodes in a linked list or a graph, entities in a game, particles, connection structs. The catch is right in the name. A pool of &lt;code&gt;Node&lt;/code&gt; holds nodes and nothing else. The moment you need two sizes, you need two pools, and the &quot;one size fits all&quot; superpower is also the whole limitation. That&apos;s also why the pool slots in the demo have no resize handle at all: there&apos;s only the one size, so there&apos;s nothing to drag.&lt;/p&gt;
&lt;h2&gt;The general purpose allocator: does everything, and now you have to think&lt;/h2&gt;
&lt;p&gt;Sooner or later you need the real thing: arbitrary sizes, freed individually, in any order, memory actually reused. This is what &lt;code&gt;malloc&lt;/code&gt; has always done, and it&apos;s the default mental model most of us drag in from other languages. In Zig it&apos;s the &lt;code&gt;DebugAllocator&lt;/code&gt;, though most people still call it the GPA out of habit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

const a = try allocator.alloc(u8, 64);
defer allocator.free(a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To pull off &quot;any size, freed in any order,&quot; it keeps a list of free holes and, on each request, goes hunting for one big enough. Which is where it gets interesting, and where it can bite you. Play with the demo: allocate a few blocks of different sizes, then free a couple out of the &lt;em&gt;middle&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Watch the &quot;largest hole&quot; number. You can have plenty of total free space and still fail to allocate, because that free space is chopped into pieces too small to use. The demo will happily tell you that you&apos;ve got, say, 9 bytes free but the biggest run is only 3, so a request for 4 fails anyway. That&apos;s fragmentation, and it&apos;s the price of admission for a general purpose allocator. It&apos;s the exact failure mode the arena and the pool are built to dodge.&lt;/p&gt;
&lt;p&gt;Freeing next to another free block helps: the demo coalesces adjacent holes back into one bigger hole, which is what a real allocator does to fight fragmentation. But it can&apos;t fix everything, and no general purpose allocator can. That&apos;s the deal. In return for handling every case, it makes you pay in bookkeeping, in the occasional slow request while it hunts for a hole, and in the fragmentation you just watched happen.&lt;/p&gt;
&lt;p&gt;Resize is where the free list really shows off. Drag a block&apos;s right edge to grow it: if the hole right after it is big enough, the allocator just extends in place and nothing moves. If it isn&apos;t, let go, and watch the block jump somewhere else on the strip entirely. That&apos;s a remap, the allocator finding a bigger hole, moving your bytes there, and handing back a new pointer. It&apos;s the same thing that happens under the hood when you grow a dynamic array and it suddenly lives at a new address.&lt;/p&gt;
&lt;p&gt;The Zig flavored bonus: the &lt;code&gt;DebugAllocator&lt;/code&gt; is packed with safety checks. Double frees, leaks, use after free, it catches them and points at the exact line. You run it in development, and when it&apos;s time to ship you swap in a faster backing allocator by changing one line, because everything downstream only ever knew about the &lt;code&gt;Allocator&lt;/code&gt; interface.&lt;/p&gt;
&lt;h2&gt;The ring buffer: run in a circle and overwrite the past&lt;/h2&gt;
&lt;p&gt;Every allocator so far treats running out of room as an error. The ring buffer treats it as the normal case. You lay your memory out in a circle: allocations march forward like a bump allocator, but when the write pointer hits the end, it wraps back to the start and writes straight over the oldest data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Keep allocating past the end and watch the writes wrap back around to the start. The oldest blocks just get stomped, no error, no freeing, no bookkeeping. That&apos;s the whole point, and it&apos;s exactly what you want when you only ever care about the most recent slice of a stream: the last few seconds of audio, incoming network packets you handle and forget, the tail of a log, a history buffer for undo. The data structure &lt;em&gt;is&lt;/em&gt; the eviction policy.&lt;/p&gt;
&lt;p&gt;The tradeoff is that you don&apos;t get to keep anything. A ring buffer will happily overwrite data you were still using if you let the pointer lap you, so it only fits when &quot;old enough&quot; reliably means &quot;safe to throw away.&quot; It doesn&apos;t really resize either, because the entire idea is a fixed window that recycles itself. Zig&apos;s &lt;code&gt;std.RingBuffer&lt;/code&gt; is a byte-oriented take on this, and the pattern shows up all over streaming and I/O code.&lt;/p&gt;
&lt;h2&gt;The buddy allocator: split in halves, merge in pairs&lt;/h2&gt;
&lt;p&gt;The free list fought fragmentation by searching and coalescing. The buddy allocator fights it with structure instead. Every block is a power of two, and the whole region starts life as one big block. To allocate, you round the request up to the nearest power of two, then split a block in half, and half again, until you land on one the right size. Each split makes two &quot;buddies,&quot; and that pairing is the trick: when you free a block, if its buddy is also free, the two snap back into the bigger block they came from. Merge all the way up and you&apos;re back to one clean region.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Allocate a few odd sizes and you can watch the region divide into neat power-of-two pieces (the demo draws each free block separately so the structure is visible). Free two buddies and they merge. This is what a lot of kernels and GPU or DMA drivers reach for, because merging is O(1) and cheap: you don&apos;t scan a free list, you check one buddy and you&apos;re done.&lt;/p&gt;
&lt;p&gt;Two tradeoffs fall out of the power-of-two rule, and the demo shows both. Ask for 3 bytes and you get 4, so a byte is wasted &lt;em&gt;inside&lt;/em&gt; the block. That&apos;s internal fragmentation, the cost of rounding. And two free blocks sitting right next to each other won&apos;t always merge, because they have to be actual buddies, the specific pair some split created, so you can still strand free space you can&apos;t recombine.&lt;/p&gt;
&lt;h2&gt;All six, one strip&lt;/h2&gt;
&lt;p&gt;Here&apos;s all six in one place. Same buffer, same buttons, and a set of tabs to swap the strategy underneath. Try running the &lt;em&gt;same&lt;/em&gt; sequence of allocs, resizes, and frees through each one and watch where they split: the arena that won&apos;t free, the fixed buffer that hits a wall, the pool that never makes a mess, the free list that fragments and remaps, the ring that laps itself, the buddy that halves and merges.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎛️ &lt;strong&gt;Interactive demo&lt;/strong&gt; — &lt;a href=&quot;https://tercek.me/blog/zig-allocators/&quot;&gt;view this post on tercek.me&lt;/a&gt; for the playground.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That divergence is the entire argument for doing it Zig&apos;s way. In a language where allocation is baked in, &quot;use an arena for this request&quot; is a rewrite. Here it&apos;s a different value passed to the same function. The code that reads the file, parses the input, builds the tree, none of it knows or cares. It asked for memory through an interface, and you got to decide what was behind the interface.&lt;/p&gt;
&lt;h2&gt;So which one&lt;/h2&gt;
&lt;p&gt;There&apos;s no universal answer, which is sort of the point, but here&apos;s the rough map I&apos;ve landed on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Arena&lt;/strong&gt; when a batch of allocations share a lifetime and die together: a request, a frame, a parse pass. Free everything at once, never free anything on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed buffer&lt;/strong&gt; when you want a hard memory ceiling and no heap at all: embedded, hot paths, anywhere a clean &lt;code&gt;error.OutOfMemory&lt;/code&gt; beats a surprise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pool&lt;/strong&gt; when you&apos;re churning through many of the exact same thing and want O(1) alloc and free with zero fragmentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;General purpose&lt;/strong&gt; when you genuinely need arbitrary sizes and independent lifetimes and you&apos;re willing to pay for it. In Zig, also your safety net while developing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ring buffer&lt;/strong&gt; when you only care about the most recent slice of a stream and old data is safe to overwrite: audio, packets, logs, undo history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buddy&lt;/strong&gt; when you want arbitrary-ish sizes with fast, cheap coalescing and can eat a little rounding waste: kernels, GPU and DMA memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most real programs use several at once: a GPA at the root, an arena per request hanging off it, a pool for the hot inner loop. Zig doesn&apos;t make that composition clever, it makes it boring. Everything speaks the same four function interface, so you stack them like Lego.&lt;/p&gt;
&lt;p&gt;I came to Zig for the comptime stuff. The allocator design is what made me want to stay. Passing memory strategy around as a plain value sounds like a small thing, right up until you realize most of the memory bugs you&apos;ve ever chased were really just the language deciding this for you, out of sight.&lt;/p&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item><item><title>Caching Is Portable. Invalidation Isn’t.</title><link>https://tercek.me/blog/caching-is-portable/</link><guid isPermaLink="true">https://tercek.me/blog/caching-is-portable/</guid><description>Astro 7 gives route caching the piece Cache-Control never could: a portable invalidation API.</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A couple months ago I wrote about why we should &lt;a href=&quot;/blog/cache-html-too&quot;&gt;cache the HTML too&lt;/a&gt;. The basic argument was pretty simple: the fastest request is the one your server never sees. We cache images, CSS, and JavaScript without thinking twice, then render the exact same HTML again for every visitor.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://astro.build/blog/astro-7/&quot;&gt;Astro 7&lt;/a&gt; just shipped with a bunch of performance work, but one feature stood out to me: &lt;a href=&quot;https://docs.astro.build/en/guides/caching/&quot;&gt;route caching&lt;/a&gt;. It takes that argument and fixes the most annoying part of actually putting it into practice.&lt;/p&gt;
&lt;h2&gt;Fast enough, until it isn&apos;t&lt;/h2&gt;
&lt;p&gt;I feel like caching has been largely ignored in the headless world because most sites are fast and cheap &lt;em&gt;enough&lt;/em&gt;. A serverless function renders the page in a few hundred milliseconds, the hosting bill stays tiny, and nobody has a reason to care that the same work happens on every request.&lt;/p&gt;
&lt;p&gt;Then the site gets a giant audience and both problems become urgent at the same time.&lt;/p&gt;
&lt;p&gt;Serverless hosting can make this stranger. A platform might run your function near the visitor, which cuts down the round trip, but moving the server closer doesn&apos;t remove any work. The function still runs. The CMS still gets queried. The HTML still gets rendered. Edge execution and edge caching sound similar, but they&apos;re solving different problems.&lt;/p&gt;
&lt;p&gt;In a lot of setups the CDN sits in front of the serverless function but doesn&apos;t cache its HTML by default. Every request passes straight through to compute. You can add an application cache behind the CDN, but by then the request has already invoked your function, which is exactly the work I want to avoid.&lt;/p&gt;
&lt;p&gt;Cloudflare just shipped &lt;a href=&quot;https://blog.cloudflare.com/workers-cache/&quot;&gt;Workers Cache&lt;/a&gt; to fix exactly this problem. Workers originally ran in front of an origin and its cache. Once frameworks started deploying the entire application as a Worker, the Worker &lt;em&gt;became&lt;/em&gt; the origin, so there was nothing left behind it to cache. Workers Cache flips that architecture around: on a hit, the cache responds and the Worker never runs.&lt;/p&gt;
&lt;h2&gt;Headers only got us halfway there&lt;/h2&gt;
&lt;p&gt;To be fair, we already have a portable interface for telling a CDN how to cache a response: the &lt;code&gt;Cache-Control&lt;/code&gt; header. Set &lt;code&gt;s-maxage&lt;/code&gt;, maybe add &lt;code&gt;stale-while-revalidate&lt;/code&gt;, and any CDN that understands standard HTTP caching has enough information to save the response.&lt;/p&gt;
&lt;p&gt;That works great until the content changes.&lt;/p&gt;
&lt;p&gt;Say a product is cached for an hour, then someone updates its price in the CMS. You could wait out the hour, but serving a knowingly wrong price for 59 minutes isn&apos;t a particularly good caching strategy. You want the CMS to fire a webhook and purge that product immediately.&lt;/p&gt;
&lt;p&gt;There isn&apos;t a standard HTTP header for that. Cloudflare, Netlify, Vercel, and every other CDN have their own purge APIs, authentication, tag formats, and limitations. At that point your otherwise portable application grows a little bundle of infrastructure-specific code.&lt;/p&gt;
&lt;p&gt;This is where Astro&apos;s route caching gets interesting. A page can describe how it should be cached:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache.set({
  maxAge: 300,
  swr: 60,
  tags: [`products:${Astro.params.id}`],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then a webhook can invalidate the same tag when the product changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await cache.invalidate({ tags: [`products:${slug}`] });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application owns the caching intent. The adapter translates it into whatever the host actually needs. Astro 7 currently has experimental CDN cache providers for Cloudflare, Netlify, and Vercel, and the same &lt;code&gt;Astro.cache&lt;/code&gt; and &lt;code&gt;cache.invalidate()&lt;/code&gt; calls work across all three.&lt;/p&gt;
&lt;p&gt;That&apos;s the abstraction I want. Cache lifetimes were already mostly portable. Invalidation was the part gluing an application to its CDN.&lt;/p&gt;
&lt;p&gt;You can also define cache behavior for groups of routes in &lt;code&gt;astro.config.mjs&lt;/code&gt;, which is probably where I&apos;d put the boring defaults:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;routeRules: {
  &apos;/blog/[...path]&apos;: { maxAge: 300, swr: 60 },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then route code only needs to get involved when it has useful tags or needs to opt out.&lt;/p&gt;
&lt;h2&gt;Next.js got close first&lt;/h2&gt;
&lt;p&gt;Next.js has had full-route caching and on-demand revalidation for a while. On Vercel, the whole thing is integrated: Next.js knows how the response was rendered, Vercel knows how it was cached, and &lt;code&gt;revalidatePath()&lt;/code&gt; or &lt;code&gt;revalidateTag()&lt;/code&gt; connects the two.&lt;/p&gt;
&lt;p&gt;Self-hosted caching works too, but &lt;a href=&quot;https://nextjs.org/docs/app/guides/cdn-caching&quot;&gt;on-demand revalidation stops at the Next.js server cache&lt;/a&gt;. An arbitrary CDN will keep serving its copy until the TTL expires unless you also call its purge API. That&apos;s the gap. Astro&apos;s adapters make the host-specific purge part of the cache provider, while the application keeps the same interface.&lt;/p&gt;
&lt;h2&gt;Portable doesn&apos;t mean automatic&lt;/h2&gt;
&lt;p&gt;There are tradeoffs here. The CDN providers are still experimental, and right now &quot;CDN agnostic&quot; really means the three supported platforms. If you host somewhere else, someone still has to write the provider.&lt;/p&gt;
&lt;p&gt;You also still have to decide what is safe to cache. A personalized dashboard is not made cacheable just because the API is pleasant. Cache the public product page, not the logged-in account sitting next to it. Invalidation webhooks need authentication. Tags need enough thought that updating one product doesn&apos;t purge the entire catalog (unless that&apos;s actually what you want).&lt;/p&gt;
&lt;p&gt;The API removes platform plumbing. It can&apos;t remove the need to understand your own data.&lt;/p&gt;
&lt;p&gt;Still, this feels like exactly the right direction. My hope is that the provider interface eventually escapes Astro and becomes something more framework agnostic, maybe as a Vite plugin or a small standard that Vite frameworks can share. Every server-rendered framework needs to express the same handful of ideas: cache this response, serve it stale while refreshing, and purge it by path or tag.&lt;/p&gt;
&lt;p&gt;We already have standard language for the first two. Astro is taking a real swing at the third.&lt;/p&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item><item><title>Agents Saved React (Unfortunately)</title><link>https://tercek.me/blog/agents-saved-react/</link><guid isPermaLink="true">https://tercek.me/blog/agents-saved-react/</guid><description>We were close to React losing its grip as the default frontend framework. Then AI coding tools arrived and made it permanent.</description><pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There&apos;s &lt;a href=&quot;https://mastrojs.github.io/blog/2026-05-23-is-AI-causing-a-repeat-of-frontends-lost-decade/&quot;&gt;an article making the rounds&lt;/a&gt; about AI causing a repeat of frontend&apos;s &quot;lost decade.&quot; The central claim is that agentic coding tools are &quot;deskilling&quot; developers the way JavaScript frameworks did before them.&lt;/p&gt;
&lt;p&gt;Half of that is right.&lt;/p&gt;
&lt;p&gt;The deskilling argument actually lands for agentic coding. When an AI writes your code and you accept it without reading it, you don&apos;t need to understand what it&apos;s doing to ship something. That&apos;s a real concern.&lt;/p&gt;
&lt;p&gt;But applying &quot;deskilling&quot; to React and the framework era? That&apos;s where I disagree. The article&apos;s own list of lost skills is worth reading back: semantic HTML, CSS, browser differences, accessibility, progressive enhancement, network performance. That&apos;s exactly the list of things a good React developer still has to know. React didn&apos;t remove those requirements. It repackaged them into a component model that&apos;s more portable and easier to reason about at scale. The skills didn&apos;t disappear. The syntax changed. That reads more like &quot;old school dev mad at change&quot; than a real critique.&lt;/p&gt;
&lt;p&gt;Anyway. The bigger thing I&apos;ve been chewing on is React itself.&lt;/p&gt;
&lt;h2&gt;DX rules, UX drools&lt;/h2&gt;
&lt;p&gt;I&apos;ve spent years quietly hoping the frontend world would move on. Not because React is broken. It&apos;s not. The developer experience is genuinely excellent. Component model, unidirectional data flow, a massive library ecosystem. You can build anything with it. Slowly, on a bad connection, but anything.&lt;/p&gt;
&lt;p&gt;But &quot;you can build anything&quot; and &quot;you should build this&quot; aren&apos;t the same claim. The answer to the second one is almost never React.&lt;/p&gt;
&lt;p&gt;The performance story has been bad since the beginning. Shipping a runtime to the browser to reconcile a virtual DOM with the actual one is a strange thing to make the default, especially once browsers got good at the exact job React was hired to do. The virtual DOM was always &lt;a href=&quot;https://svelte.dev/blog/virtual-dom-is-pure-overhead&quot;&gt;a tradeoff, not a free win&lt;/a&gt;. And the framework made that tradeoff on the user&apos;s behalf, not the developer&apos;s. Bundle sizes ballooned. Time to interactive got worse. Developer experience kept improving while user experience slid in the other direction.&lt;/p&gt;
&lt;h2&gt;We were so close&lt;/h2&gt;
&lt;p&gt;Here&apos;s what&apos;s annoying: fine-grained reactivity has existed for years. &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt; figured this out. You write components, the compiler produces lean output, there&apos;s no virtual DOM to reconcile. It&apos;s faster. The bundles are smaller. The expensive work happens at build time instead of in the user&apos;s browser. Svelte is excellent, and I genuinely enjoy working with it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.solidjs.com/&quot;&gt;Solid&lt;/a&gt; went further and asked: what if you got Svelte-level performance but kept React&apos;s mental model? Signals, composable primitives, JSX. Turns out you can have both. Solid routinely lands near the top of the &lt;a href=&quot;https://krausest.github.io/js-framework-benchmark/current.html&quot;&gt;framework benchmarks&lt;/a&gt;, right next to vanilla JavaScript. Solid is a great framework. Nobody uses it.&lt;/p&gt;
&lt;p&gt;Preact &lt;a href=&quot;https://preactjs.com/blog/introducing-signals/&quot;&gt;shipped signals&lt;/a&gt; for the same reason back in 2022. And the &lt;a href=&quot;https://react.dev/blog/2024/10/21/react-compiler-beta-release&quot;&gt;React Compiler&lt;/a&gt; that Meta shipped in late 2024 is trying to automate, at build time, the memoization that compiler-first frameworks made unnecessary half a decade earlier. It doesn&apos;t touch the core issue: the runtime and the reconciler still ship to the browser, and the compiler just re-renders less of the tree. It&apos;s a stopgap with good marketing.&lt;/p&gt;
&lt;p&gt;Around 2023 and into 2024, there was real momentum behind the idea that React would stop being the default. Not die. Just become one tool among several, chosen when it&apos;s actually the right fit. SvelteKit was getting attention. Solid was maturing. The conversation felt like it was actually happening.&lt;/p&gt;
&lt;h2&gt;Then agents happened&lt;/h2&gt;
&lt;p&gt;When an AI is writing your code, it writes what it knows best. Models have more React training data than anything else in the frontend space, the patterns are exhaustively documented, and the community is enormous. Ask an agent to scaffold a component and you&apos;re getting React, a &lt;a href=&quot;https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-double-rendering-in-development&quot;&gt;&lt;code&gt;useEffect&lt;/code&gt; that fires twice in development&lt;/a&gt;, and a &lt;code&gt;package.json&lt;/code&gt; with thirty dependencies. That&apos;s not a criticism, it&apos;s just how it works.&lt;/p&gt;
&lt;p&gt;It&apos;s not even subtle. Someone &lt;a href=&quot;https://saschb2b.com/blog/llm-default-react-stack&quot;&gt;asked eight different models and tools&lt;/a&gt; to build a frontend: Claude, GPT, Gemini, DeepSeek, Qwen, v0, Lovable, Bolt. They all landed on nearly the same answer. React, Next.js, TypeScript, Tailwind, shadcn/ui. The stack is a property of the training corpus, not a choice. And because the next model trains on the code this one generates, the corpus skews further toward React every cycle. The default reinforces itself.&lt;/p&gt;
&lt;p&gt;So instead of the frontend world diversifying, it consolidated. React got a massive tailwind. Not the CSS kind.&lt;/p&gt;
&lt;p&gt;Svelte and Solid are still out there and still measurably faster. They&apos;re just not what the agent defaults to.&lt;/p&gt;
&lt;p&gt;What bothers me most is the timing. &lt;a href=&quot;https://svelte.dev/blog/svelte-5-is-alive&quot;&gt;Svelte 5&lt;/a&gt; shipped with &lt;a href=&quot;https://svelte.dev/blog/runes&quot;&gt;runes&lt;/a&gt; in October 2024. &lt;a href=&quot;https://start.solidjs.com/&quot;&gt;SolidStart hit 1.0&lt;/a&gt; that May. Real alternatives existed with real communities. The moment when developers might have started making deliberate, performance-conscious framework choices arrived right as the era of developers writing code by hand started winding down.&lt;/p&gt;
&lt;p&gt;AI coding tools get marketed as accelerators of innovation. But if you&apos;re generating the same patterns from three years ago at ten times the speed, that&apos;s not innovation. It&apos;s just more of the same, faster.&lt;/p&gt;
&lt;p&gt;The lost decade is here. It just comes with better autocomplete.&lt;/p&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item><item><title>AGENTS.md Is Not a README</title><link>https://tercek.me/blog/agents-md-is-not-readme/</link><guid isPermaLink="true">https://tercek.me/blog/agents-md-is-not-readme/</guid><description>Committing agent config files to shared repos quietly imposes one person&apos;s workflow on the whole team. Git figured out the right layering decades ago. We haven&apos;t caught up.</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Open source projects are getting plagued by agent files. &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.cursor/rules&lt;/code&gt;, &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, committed skills, the works. The convention, pushed hard by the people who make these tools, is to commit them. The &lt;a href=&quot;https://agents.md&quot;&gt;agents.md&lt;/a&gt; spec frames the file as &quot;a README for agents,&quot; project infrastructure that belongs in source control next to your CI config. GitHub has written up &lt;a href=&quot;https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/&quot;&gt;lessons from 2,500+ repos&lt;/a&gt; treating it as shared documentation.&lt;/p&gt;
&lt;p&gt;I think this is mostly a mistake.&lt;/p&gt;
&lt;p&gt;These files are inherently personal. The way I drive an agent is not the way you drive an agent, and committing my workflow into a shared repo quietly assumes the whole team should converge on one. I don&apos;t think that&apos;s true, and I don&apos;t think it&apos;s desirable. Your agent workflow can and should be bespoke. It&apos;s closer to your editor config or your shell aliases than to your test suite. Nobody commits their &lt;code&gt;.vimrc&lt;/code&gt; to the project repo and expects everyone to adopt it.&lt;/p&gt;
&lt;p&gt;The usual argument for committing the file is that it documents the architecture, the repo layout, the build and release process, the conventions a new collaborator needs. All of that should be shared. But none of it is &lt;em&gt;agent&lt;/em&gt; information. It&apos;s documentation, and documentation belongs in the README, where humans will actually read it. Putting it in &lt;code&gt;AGENTS.md&lt;/code&gt; just hides your project docs somewhere only the robots look. As &lt;a href=&quot;https://news.ycombinator.com/item?id=44957647&quot;&gt;spawarotti&lt;/a&gt; put it on Hacker News:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;At this point AGENTS.md is a README.md with enough hype behind it to actually motivate people to populate it with contents. People were too lazy to write docs for other people, but funnily enough are ok with doing it for robots.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The shareable content was never agent content. It&apos;s the docs we should have been writing for each other all along.&lt;/p&gt;
&lt;p&gt;This instinct isn&apos;t fringe, but it&apos;s a minority position. You can watch the debate play out in real time: a &lt;a href=&quot;https://news.ycombinator.com/item?id=47975333&quot;&gt;Hacker News commenter&lt;/a&gt; says flatly, &quot;I would .gitignore all of those files. Is this not what is done in industry?&quot; and gets mostly disagreement. The split is always the same: shared rules enforce consistency, but they also flatten everyone into one person&apos;s setup.&lt;/p&gt;
&lt;h2&gt;So why does everyone commit them anyway?&lt;/h2&gt;
&lt;p&gt;Because the alternative is genuinely worse right now.&lt;/p&gt;
&lt;p&gt;The reason to colocate agent instructions with the code is that git gives you branch-awareness for free. Say you&apos;re on a feature branch and you tweak &lt;code&gt;AGENTS.md&lt;/code&gt; to describe a module that only exists on that branch. Then you switch back to &lt;code&gt;main&lt;/code&gt;. Because the file is tracked, your changes travel with the branch, so &lt;code&gt;main&lt;/code&gt;&apos;s agent never sees references to code that doesn&apos;t exist yet. Anyone who has tried git-ignoring their agent files knows the opposite feeling: a single global instruction file that&apos;s perpetually slightly wrong for whatever branch you&apos;re actually on.&lt;/p&gt;
&lt;p&gt;So you&apos;re stuck between two bad options. Commit the file and impose your workflow on the team, or ignore it and lose the branch-awareness that made it useful.&lt;/p&gt;
&lt;h2&gt;What we have today&lt;/h2&gt;
&lt;p&gt;The tools do offer escape hatches, they&apos;re just incomplete.&lt;/p&gt;
&lt;p&gt;Claude Code, for instance, has a real hierarchy (&lt;a href=&quot;https://code.claude.com/docs/en/memory&quot;&gt;documented here&lt;/a&gt;): a user-level &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; that applies across all projects and never gets committed, a team-shared project &lt;code&gt;./CLAUDE.md&lt;/code&gt;, and a personal &lt;code&gt;./CLAUDE.local.md&lt;/code&gt; that you&apos;re meant to add to &lt;code&gt;.gitignore&lt;/code&gt;. AGENTS.md has an &lt;a href=&quot;https://github.com/agentsmd/agents.md/issues/91&quot;&gt;open proposal&lt;/a&gt; to standardize a global &lt;code&gt;~/.config/agents/AGENTS.md&lt;/code&gt;, explicitly modeled on how git handles &lt;code&gt;~/.gitconfig&lt;/code&gt; versus a repo&apos;s &lt;code&gt;.git/config&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That git analogy is the right one. Git solved exactly this problem decades ago: global defaults in &lt;code&gt;~/.gitconfig&lt;/code&gt;, repo overrides in &lt;code&gt;.git/config&lt;/code&gt;, and personal per-repo ignores in &lt;code&gt;.git/info/exclude&lt;/code&gt; that never touch the shared &lt;code&gt;.gitignore&lt;/code&gt;. The local layer is never committed. We just haven&apos;t fully ported that model to agent memory.&lt;/p&gt;
&lt;p&gt;The gap is branch-awareness. &lt;code&gt;CLAUDE.local.md&lt;/code&gt; is per-worktree, not per-branch. None of the personal-config options track your branches the way a committed file does.&lt;/p&gt;
&lt;p&gt;That gap is what pushed me to build &lt;a href=&quot;https://github.com/claytercek/offstage&quot;&gt;offstage&lt;/a&gt;, a small Go CLI that keeps personal files colocated with a project without committing them to the project&apos;s repo. It&apos;s backed by a separate private git repo, and it&apos;s branch-aware: files are stored under &lt;code&gt;&amp;lt;project-id&amp;gt;/&amp;lt;git-branch&amp;gt;&lt;/code&gt;, so each branch gets its own notes, synced across machines via git hooks. It&apos;s been working for me, but I&apos;m floating it as one option, not a recommendation, and your mileage may vary. It&apos;s a partial fix at best: if a branch gets merged via a PR, offstage has no way to know, so you have to manually run &lt;code&gt;offstage merge&lt;/code&gt; to reconcile. The branch lifecycle and the file lifecycle drift apart the moment the merge happens somewhere offstage can&apos;t see.&lt;/p&gt;
&lt;h2&gt;A better solution&lt;/h2&gt;
&lt;p&gt;The real fix is more foundational than a sync tool. A few directions I think are worth exploring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native per-branch personal memory in the agent itself.&lt;/strong&gt; The agent already knows what branch you&apos;re on. It could keep personal instructions keyed by branch, stored outside the repo, and reconcile on branch events (merge, delete, rebase) by hooking into git the way it already hooks into everything else. This is offstage&apos;s job, done by something that can actually observe the merge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A standardized local layer, universally honored.&lt;/strong&gt; If every agent agreed on a &lt;code&gt;~/.config/agents/&lt;/code&gt; global file plus a gitignored local override, and actually merged them at runtime, most of the &quot;personal vs. shared&quot; tension disappears. The committed &lt;code&gt;AGENTS.md&lt;/code&gt; becomes a genuine team README; everything bespoke lives in your local layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat the shared file as the floor, not the ceiling.&lt;/strong&gt; Commit only the truly project-wide facts (build commands, test runner, deploy steps) and socially discourage stuffing personal workflow into it. Keep the file boring and uncontroversial on purpose.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these fully exist yet. For now I commit a deliberately minimal &lt;code&gt;AGENTS.md&lt;/code&gt; and keep everything bespoke in offstage. But I think the long-term answer is for agents to treat memory the way git treats config: layered, with the personal layer kept firmly off the shared record.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://agents.md&quot;&gt;agents.md&lt;/a&gt; — the open spec, now under the Linux Foundation&apos;s Agentic AI Foundation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/memory&quot;&gt;Claude Code memory docs&lt;/a&gt; — the user / project / local hierarchy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/agentsmd/agents.md/issues/91&quot;&gt;agents.md issue #91&lt;/a&gt; — proposal for a global user-level AGENTS.md&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=47975333&quot;&gt;HN: &quot;I would .gitignore all of those files&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=44957647&quot;&gt;HN: spawarotti, &quot;AGENTS.md is a README.md with enough hype&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/docs/gitignore&quot;&gt;git-scm.com/docs/gitignore&lt;/a&gt; — &lt;code&gt;.git/info/exclude&lt;/code&gt; and &lt;code&gt;core.excludesFile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/claytercek/offstage&quot;&gt;offstage&lt;/a&gt; — my attempt at the branch-aware personal layer&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item><item><title>Cache the HTML Too</title><link>https://tercek.me/blog/cache-html-too/</link><guid isPermaLink="true">https://tercek.me/blog/cache-html-too/</guid><description>The fastest request is the one your server never sees. Most teams cache assets and skip HTML. That&apos;s the part worth fixing.</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A lot of web frameworks and platforms bury the single easiest way to make a site faster and cheaper: CDN caching. We spend enormous effort shaving milliseconds off server-side rendering, picking the fastest meta-framework, optimizing database queries, and then skip past the one move that beats all of it. Just save the result. There&apos;s no reason to repeat the same work on every single request.&lt;/p&gt;
&lt;p&gt;The fastest request is the one your server never sees.&lt;/p&gt;
&lt;h2&gt;HTML is cacheable too&lt;/h2&gt;
&lt;p&gt;Most CDNs make this trivial, but the default trips people up. Cloudflare, Fastly, CloudFront, Vercel&apos;s edge: they all cache your static assets (CSS, JS, images, fonts) out of the box. What they do &lt;em&gt;not&lt;/em&gt; cache by default is HTML. As Cloudflare&apos;s docs put it plainly: &lt;a href=&quot;https://developers.cloudflare.com/cache/concepts/default-cache-behavior/&quot;&gt;&quot;The Cloudflare CDN does not cache HTML by default.&quot;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&apos;s the part people miss. Caching media feels obvious, but caching pages feels dangerous. Yet a huge fraction of the HTML you serve is identical for every visitor, and there&apos;s no reason to render it fresh each time.&lt;/p&gt;
&lt;p&gt;Turning it on is mostly a one-liner. The mechanism across every CDN is the &lt;code&gt;Cache-Control&lt;/code&gt; header: &lt;code&gt;s-maxage&lt;/code&gt; controls how long the CDN holds the response, and &lt;code&gt;stale-while-revalidate&lt;/code&gt; lets it serve the stale copy instantly while refreshing in the background. On Cloudflare you can also do it without touching your app, via a &lt;a href=&quot;https://developers.cloudflare.com/cache/how-to/cache-rules/examples/cache-everything/&quot;&gt;Cache Rule&lt;/a&gt; that marks matching URLs as eligible for cache.&lt;/p&gt;
&lt;p&gt;One real caveat: &quot;cache everything&quot; means &lt;em&gt;everything&lt;/em&gt;. If you blanket-cache HTML, you can hand one user&apos;s logged-in page to the next visitor. So scope it: exclude anything with an auth cookie, exclude your dashboard, cache the public pages. Anything that sets &lt;code&gt;Set-Cookie&lt;/code&gt;, &lt;code&gt;private&lt;/code&gt;, or &lt;code&gt;no-store&lt;/code&gt; is skipped automatically, but you should still be deliberate about it.&lt;/p&gt;
&lt;h2&gt;How much of your site actually needs to be fresh?&lt;/h2&gt;
&lt;p&gt;This is the question worth asking before you reach for anything fancier. Your blog, your marketing pages, your docs, your product listings... how often do they really change? Sure, modern frameworks render pages fast. But &quot;fast to render on every request&quot; will always lose to &quot;already rendered, sitting at an edge node 20ms from the user.&quot; Static beats dynamic, and a cache turns dynamic into static for the 99% of requests where nothing changed.&lt;/p&gt;
&lt;h2&gt;A note on framework caching&lt;/h2&gt;
&lt;p&gt;Frameworks have their own caching layers, and it&apos;s easy to conflate them with CDN caching. Next.js is the instructive case because its caching model is famously confusing.&lt;/p&gt;
&lt;p&gt;Next.js has &lt;a href=&quot;https://nextjs.org/docs/app/guides/caching&quot;&gt;several distinct caches&lt;/a&gt;: Request Memoization (dedupes calls within a single render), the Data Cache (persists &lt;code&gt;fetch&lt;/code&gt; results across requests), the Full Route Cache (server-side), and the Router Cache (client-side). The Full Route Cache serves fully rendered static pages straight from cache: at build time Next.js renders static routes to HTML plus the RSC payload, and serves that cached output instead of re-rendering per request. That&apos;s exactly the static-serving behavior you want.&lt;/p&gt;
&lt;p&gt;Server Actions are not cached. They use POST and are mutations by design; Next.js only ever memoized GET-style data fetches. Actions can &lt;em&gt;trigger&lt;/em&gt; invalidation via &lt;code&gt;revalidatePath&lt;/code&gt; / &lt;code&gt;revalidateTag&lt;/code&gt;, but they&apos;re never served from cache.&lt;/p&gt;
&lt;p&gt;Worth knowing too: &lt;a href=&quot;https://nextjs.org/blog/next-15&quot;&gt;Next.js 15 flipped the defaults&lt;/a&gt; toward less implicit caching: &lt;code&gt;fetch&lt;/code&gt; is &lt;code&gt;no-store&lt;/code&gt; by default now, and GET route handlers aren&apos;t cached by default. The framework is leaning away from caching-by-magic, which makes the case for owning your caching at the CDN layer even stronger.&lt;/p&gt;
&lt;p&gt;None of this means Next.js is bad at caching. The point is that framework caches and CDN caches solve overlapping but different problems, and the CDN layer is the portable one. It works the same no matter what you build with, and it&apos;s hard to beat for the effort it takes. Reach for it first.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/cache/concepts/default-cache-behavior/&quot;&gt;Cloudflare: default cache behavior&lt;/a&gt; — what gets cached out of the box, and what doesn&apos;t&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/cache/how-to/cache-rules/examples/cache-everything/&quot;&gt;Cloudflare: &quot;Cache Everything&quot; via Cache Rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/guides/caching&quot;&gt;Next.js caching guide&lt;/a&gt; — the four cache layers, explained&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/blog/next-15&quot;&gt;Next.js 15 release notes&lt;/a&gt; — the caching default changes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control&quot;&gt;MDN: &lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;s-maxage&lt;/code&gt;, &lt;code&gt;stale-while-revalidate&lt;/code&gt;, and friends&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>hello@tercek.me (Clay Tercek)</author></item></channel></rss>