Posts in Javascript (20 found)

Battle Drones - a vibe coded RTS with audio-based multiplayer

My latest vibe coded game experiment is a Command & Conquer style RTS (Real Time Strategy) game called Battle Drones ( play it here ). Unlike my previous experiments , this one is actually fun to play . I'm somewhat embarrassed to admit that I got there by telling Claude to "Make it a LOT more fun". It also supports multiplayer between nearby devices using audio to synchronize game state! Here's what I've built so far, and some of the things I learned along the way. My initial goal was to build an RTS game I could play on my phone, in the style of Command & Conquer . I have fond memories of the 1995 original (I liked Red Alert too) and wanted to recapture a little bit of the magic. After 12 major iterations (and 321 commits by coding agents, mainly Claude Code) I now have a browser-based game that works on mobile and desktop, has multiple play modes, several maps (including a random map generator) and even supports multiplayer for two devices within microphone range of each other. Here's a video demo of Battle Drones running in a desktop browser: Features so far include: Battle Drones is heavily vibe coded - I've reviewed almost none of the code, and it's built in a language (TypeScript) that I've hardly used myself. I've playtested it quite a bit though, and it's been through a large number of iterations since I started the project. Here's a rough history of the project so far, including key prompts and links to transcripts. I started out with Claude Fable 5 back in July: Here's a shared transcript . I'd given Claude Code for Web an environment variable with an OpenAI API key and told it to use the OpenAI Image Generation API for assets, which got me some basic sprites. You can try v1 here - it looked like this: It's recognizable as an RTS, has building mechanics and the ability to select and direct units, and a basic AI to play against. This first version was barely a proof of concept, and it wasn't any fun to play. Claude used the Phaser 2D game framework for these early versions. I didn't even realize this until several iterations later! The second version ( try v2 here ) added sprites for all of the vehicles and the "AI vs. AI" mode, so I could watch the game playing without having to directly control it myself. Claude proved reasonably competent at prompting OpenAI's image models to generate sprites. You can browse the v2 sprites in the public/assets/sprites folder. It still wasn't much fun, and the graphics were boring. It was now October, and I had upgraded to Claude Opus 5.5. I told it: This resulted in five new maps with generated background tiles in a variety of styles, visible here . Opus also regenerated the sprites, making them much more visually appealing. You can try v3 here - it looks like this: This is starting to feel like a real game! It still wasn't very fun to play though - I found myself getting bored pretty quickly. So far my Battle Drones experiment was confirming my previous belief that coding agents can build a thing that looks like a game but can't actually make something that's genuinely compelling to play. I shared this theory with someone over dinner, and then thought to myself that I should give the models a proper chance to prove me wrong. I prompted Opus 5.5 on my phone: This time I was running in Ultracode mode , where Claude Code (in this case Claude Code for Web) runs "dynamic workflows", described in their docs as: A dynamic workflow is a JavaScript script that orchestrates many subagents at once. This kicked off many hours of work. I started it in the evening and didn't get a playable result back until late into the next morning. But the results were spectacular. You can try v4 here - it looks like this: I had Claude write its own release notes - here are the highlights: Every skirmish and Daily Op now opens with a choice, gives the side that's losing a way back, and pulls the armies into the middle of the map. There are also two new ways to play. Fights run longer and swing more. In 1,200 AI-vs-AI test matches on Normal, games still undecided after 25 minutes fell from 14% to 0.9%. The share of wins by a side that had been 800+ scrap behind rose from 24% to 46%. Part of the reason Claude took so long was that it ran over a thousand simulated playtest sessions, and used the results to rebalance the existing units, add new ones, and introduce new features. After the success of Ultracode at "Make it a LOT more fun", I decided to be more ambitious: That was another overnight run. I woke up to this v5 version : The new 3D models were entirely constructed by Claude in TypeScript code. v6 ( play v6 here ) added some more visual polish. v7 ( play v7 ) added robot infantry (troopers, rocket squads, snipers) and a "Condor" troop carrying aircraft. I was beginning to feel guilty at how little actual design input I had into the game - it really had been a case of me saying "make it more fun" and "add visual polish" for the past few releases. I'd noticed that the AI didn't use the new infantry and troop carrying aircraft from v7 very often - there wasn't a great strategic reason to use them. It would be neat if we had a new map with a new game mechanic that actively encouraged players to load up Condors with infantry robots and send them out into the map. This resulted in my longest prompt yet: The result was the tropical Archipelago map, with seven island uplinks that players can only capture by flying out infantry in a Condor. It's definitely the most fun map to play, once you've got the hang of the basic game mechanics from the simpler maps. The v9 edition made that map a little more interesting - it now looks like this: I decided to add giant robot mechs to the game - think Jaegers from Pacific Rim , or Titans from Warhammer 40,000. Here are some of the prompts I used to brainstorm with Claude: The end result is really cool . Goliaths can be constructed using a Gantry - a building that can only be created after first building a Fab, Bay, Airpad, and Tech Lab. You can see the Goliath under construction, and it then walks out from the Gantry and can march around the map. Goliaths can "anchor", which digs them into the ground. In their anchored state they can act as a new HQ, constructing infantry units and additional buildings in a new area. Two Condor aircraft can hook onto a Goliath, at which point it can be flown around the map! The game now ends when you destroy the enemy's HQ building and any Goliaths they might have built. There are also a couple of mutators relating to Goliaths: The other v10 feature is agent battles . In musing about how multiplayer could work I had an idea: what if different LLMs could fight it out in the game? Opus 5.5 went to work on this, producing this AGENT-BATTLES.md proposal and then implementing and testing it. The feature also includes the ability to record and replay these battles - here's a replay of Claude Haiku 5.5 vs. GPT-6 Luna , and here's that same battle in a separate debug tool that shows the underlying prompt responses. The prompt that drives agent battles can be seen here . There are a few more replays like this linked in the README . I hope to turn this into a full league table at some point in the future. This version exists purely because I glanced at my Claude Usage page one day and saw that I'd only used 75% of my weekly allowance and it was resetting in 45 minutes. I decided to see if I could get an extra Battle Drones feature from my remaining tokens, and picked "random map" as the target: Here's the transcript , and an image Claude generated to show me its work: I only just got this working, so I've not had much time to actively test it yet. The latest Battle Drones now supports multiplayer via two devices sat within microphone distance of each other! This feature started out as my Chirp Pong experiment from last week. I pointed Claude Code at that working proof of concept: Opus built this Radio Probe tool to measure how much bandwidth we could get between two devices and, based on positive results from that, implemented a full audio modem system. I've tried it between laptops, iPhones and iPads so far and it seems to work really well! Why use sound for this? Because I love static HTML that costs nothing to run, and I don't want to spin up a server somewhere and then stay responsible for keeping it operating for as long as anyone might want to play the game. I know almost nothing about game development. This makes it a great opportunity to try out coding agents in a field where my existing software engineering experience has less impact on the results. Games are also super low stakes. The worst that can happen is one of my silly game experiments suffers a bug. This morning ( via Hacker News ) I found Steam Fans Are Begging for a No-AI Filter as Steam Next Fest Is Filled With Slop . I wouldn't personally dream of submitting Battle Drones to an event like this - it's still way too low effort to warrant consideration as a legitimate indie gaming project. ... but I can now see a path to vibe coded games like this being taken seriously. Battle Drones is fun, it looks good, it has innovative features (the multiplayer-over-audio thing is a first for an RTS as far as I can tell). I'd love to see what someone with better taste than me and a driving passion to produce something truly exceptional could build! A few other observations from this project: Earlier in this article I said: Games are also super low stakes. The worst that can happen is one of my silly game experiments suffers a bug. Then I tried playing the multiplayer version with Natalie... and the high pitched modem sounds gave her a nasty headache! That's a pretty bad bug. I added a warning and was then inspired to build proper network multiplayer. I tried getting WebRTC to work but couldn't get a connection going between two of my own devices even on my own WiFi network, so I had Claude build a simple, stateless WebSockets relay server instead: Here's the resulting server . Since this scales to zero and runs on a single Fly.io instance it should cost me no more than $2.19/month - and that's if it stays hot and serving connections the whole time. Battle Drones now supports online multiplayer. Click "Duel a friend online", then either share the resulting room URL via your messaging app of choice or have them snap a picture of the displayed QR Code. I also added a feature where you can copy JSON of the match once it has ended and then feed that into the replay feature to rewatch your game. I didn't expect to introduce online multiplayer this soon, but the headache bug felt like it warranted an urgent alternative. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Battle Drones How I built this v1: the initial prototype v2: full sprites v3: varied terrain v4: the fun update v5: Battle Drones goes 3D v8: Archipelago v10: Goliath, and agent battles Agent battles v11: Random Maps v12: Multiplayer via sound Some thoughts Update: Online multiplayer support 9 buildings, 7 ground vehicles, 6 aerial vehicles, 3 infantry units - all 3D models, which you can browse here Different weapons are needed to target aerial or ground vehicles Six maps , across five biomes - temperate, desert, arctic, volcanic, tropical A random map generator Skirmish mode - red vs. blue on a rotationally symmetrical map Last Stand mode - one player holds out against as many waves of enemies as possible Area effect weapons: Barrage, EMP, Drop Pod, Sky Hammer Crate drops with bonus resources In-game music that responds to events - it gets more intense as fighting breaks out Different in-game AI personalities (Balanced, Rush, Air Swarm, Boom, Skyforce, Ironclad) AI vs. AI mode, to watch those personalities play each other The Goliath - a giant stomping mech with laser eyes and arm cannons and the ability to establish a new base Mutators that add extra rules or twists A "Daily Op" challenge Ranks and achievements ( Medals ) Agent vs. Agent mode where two LLMs can play Battle Drones against each other A multiplayer mode based on two nearby phones communicating via audio messages, an extension of my earlier experiment with Chirp Pong Openers : pick Blitz, Industry or Air Wing before the clock starts. Call-down powers : Barrage, EMP, Drop Pod and the Sky Hammer orbital strike. Objectives and salvage : Uplink relays, supply drops and wrecks worth scrap. Feel : combat juice, adaptive music, unit voices and an announcer. Command : battle alerts, game speed, saved squads and touch placement. New modes : Last Stand survival waves and an 8-mission Ops campaign. Progression : heroes, an after-action report with awards, a career, 7 mutators and a Daily Op. Goliath Armada starts each side with four airborne Goliaths (each with a pair of Condors) and no other units. Robot Love adds a special rule: if enemy Goliaths get close to each other on the battlefield they fall in love and the game ends in a draw. (Thank Natalie for that one). Static HTML and JavaScript is a fantastic medium for games development. WebGL works great, and Three.js (and Phaser before it) are excellent platforms to build on that the current models know inside out. This also lets them run as Progressive Web Apps - I didn't ask for it, but Battle Drones works offline. I was able to play it on a plane despite never having directly installed it - my phone had it cached. Giving Claude an OpenAI API key and telling it to generate images using OpenAI's models works really well. It can view the images it has generated and iterate on the prompts. Opus 5.5 is spectacular at 3D modeling using Three.js (and Blender, and no doubt other 3D applications as well). The same is true of GPT-6 Astra. They are absolutely capable of building models for 3D game projects. For features like replay and multiplayer it's useful to have a deterministic simulation engine. At one point Claude had to replace calls to and and friends with custom versions because different browsers (Chrome vs. Safari, for example) returned very slightly different numbers, desynchronizing the replay. Opus 5.5 can write music! There are no audio files in this project - the soundtrack is entirely generated through code, varies across different maps, and even gets more intense as the battle heats up. See Scrimshaw Jukebox for a previous experiment with Claude for music composition. I still can't get over how well the prompt "Make it a LOT more fun" worked. Tools are effectively free. I prompted up the Radio Probe , the Model browser , the Agent vs. Agent debug tool , and Claude Code built a bunch of tools for itself that I haven't even explored myself. Audio-based multiplayer between static HTML games works! I hope to see a whole lot more games use this same pattern - in digging around today I've not found much prior art beyond Siner by Alon Horesh and ggwave by Georgi Gerganov (also of fame). Claude Code "Ultracode" burns tokens like there's no tomorrow, but it can also solve complex problems (like adding audio-based multiplayer) from a single prompt. I'm still not quite over the guilt of building software in this way. I don't feel like I've earned the right to create something this good, in a field in which I have so little prior experience! At this point I've shown it to enough people in private that it felt wrong not to publish it for everyone else to see and learn from.

0 views
Josh Comeau Yesterday

The Future of CSS ✨

Every year, browser vendors collaborate via the Interop project to synchronize their efforts on new HTML/CSS/JS features. This year, there’s a ton of super exciting features being considered! In this blog post, we’ll explore the CSS of the future, to get a glimpse of what we’ll soon be able to do.

0 views

Building a Tool for Job Searchers

The job search process can be chaotic, stressful and scary. It's easy to lose track of the progress you've made in a sea of rejection emails. As you progress through the stages, it becomes important to keep track of everyone you talked to and what you discussed. When you have multiple applications going at once, it can become messy! Like any good engineer, I decided to build my own solution! Mostly as a way to keep my mind busy and stay on top of AI tooling, but also because I believe there's real value to a tool that brings notes, scheduling and a CRM into one job search focused package. Source - XKCD I'm calling my project JobCaddy. As of right now it focuses on helping job seekers track where they've applied to, the various stages they've gone through and the people behind each stage. Test account with fake data My eventual goal is to add suggestions on actions you should take. One thing I learned as a hiring manager is that many people forget to focus on the human aspect of the process. The conversations and people you meet are extremely important. As you progress through stages at a company, you should be sending thank personalized thank you letters, referencing previous conversations and able to recall the name of each person you talked to. JobCaddy will act as a mentor of sorts, suggesting follow-ups and helping you prep for next stages based on your notes and people you talked to. Test account with fake data Like I said, this is in part a project for me to stay on-top of AI developments while I'm job searching. I'm using Next.JS deployed on Vercel with a Supabase backend and shadcn/ui for components. I'm building it primarily with Codex and have been leveraging the impeccable skill for styling . My workflow revolves around the Codex CLI in Herdr , dictation via Myse on Ubuntu and tooling management via Mise . I leverage Sublime Text for manual coding. It's been a pretty fun process, allowing me to quickly iterate based on pain points I run into while using JobCaddy for my own job search!

1 views
neilzone 2 days ago

A review of the BigMe B6, an Android colour eInk tablet

For quite some time, I have been keen to try an Android device with an eInk screen, particularly a colour one, but I had not found one at a price that I was willing to pay. I saw the BigMe B6 on AliExpress. It was £134.00, and seemed to meet my requirements - including a pocketable 6” screen, and a USB-C port - and I was willing to spend that to give it a try. It arrived quickly, and came with a nice folding case which holds the device magnetically, and turns the screen on and off when opening and closing it. I had 88% charge when it arrived, so I was able to get on an use it straight away. I used to back up all the partitions on the device (except ), in case I needed to restore them. At this point, I was planning on rooting the BigMe B6, but ultimately I decided not to do so. I used for this, using the “Read partitions” function. (I used “Select all partitions”, and also had “Dump GPT” set (which it was by default). I then manually de-selected “userdata”.) I clicked “Read partition(s)” to start the partition dump. This took 20 minutes or so, mainly thanks to the 12 GB “super” partition. I backed up all the existing apks (in case I needed to reinstall them, in the event of rooting the device and uninstalling software): It comes with a lot of unnecessary (to me) stuff on it. I’d prefer a far more plain, unencumbered, device. I used this gist as inspiration, and used to disable a lot of that software. I have not rooted the BigMe B6, so the software is just disabled, not removed. I have seen some posts saying that it tries to work as a USB host by default. Perhaps things have changed, but when I plugged it into a laptop (Debian 13, GNOME), it mounted as a USB client device, just as I would expect. My goal in buying this was to have a cheap-ish device, for recreational stuff. So, I have installed F-Droid, and used F-Droid to install: All run fine. I also installed Heliboard, as an alternative keyboard. I like the customisability, in particular being able to make it smaller and yet easier for me to use than the native keyboard. The launcher is a bit childish, in a “looks poorly hand drawn to me” kind of way, but I don’t spend much time looking at it, so I have not bothered to do anything about replacing it. I’d like to try it with a Bluetooth keyboard at some point, I think, and see what it is like writing in vim or similar. It had some included eBook reading / management software, but I disabled all of it in favour of using KOreader. KOreader has its own quirks (perhaps a blogpost in itself) but, frankly, it does exactly what I need, and it does it well. In particular, I like being able to set the margins and line spacing that I want, and the auto-turn functionality, which flips the page after a set number of seconds, is very handy. The main pain point for me is that I have never successfully managed to sync my library of books - in Calibre - to the internal storage. I think that it is the sheer number of items, but whether I use Calibre’s own sync function, or drag-and-drop, or just , the transfer fails. And I have had one heck of a time trying to remove books from the internal storage, again perhaps because of the number of items in the directory. Frankly, this is annoying, and I wish that I had solved it, but I have not. Instead, I have worked around it, spending an extra £20 for a micro SD card, and accepting that, to copy books to the card, I need to remove it from the BigMe B6, put the micro SD card into a laptop, and rsync the ePubs from my library to it from there. What a pain. I could, in time, experiment with something like syncthing on it, but all I really want is to be able to plug in the eReader, via USB-C, and rsync books to it. But, hey, with a micro SD card and KOreader, I can read my books on it. The screen works really well for this - it is a joy to read on it. Browsing on an eInk screen is an interesting experience, and I have played around with the screen refresh settings to see if I can optimise it a bit. It is, IMHO, surprisingly snappy and browsing the web is no problem unless the site is poorly designed and full of JavaScript, in which case it is tolerable. Watching / streaming videos is possible, but not desirable. Fine at a pinch, or if one likes a sort of cyberpunk aesthetic. But I would not buy the BigMe B6 for watching video. I turn the Wi-Fi off when I am finished, if I am not switching off the device completely. The screen on the BigMe B6 is really nice! The screen refreshes fine, and it is easy to scroll around / use the touch-screen functions. It works a heck of a lot better than I was expecting for the price, to be honest, and I really enjoy reading / browsing on it. It is just as pleasant - if not slightly more pleasant - to read on it than it is on my dedicated eReader. Changing the eInk settings can make a significant difference, with clarity / removal of ghosting coming at the cost of more regular refreshes (which means a flash and a short delay). I want to experiment some more with this, perhaps with per-app settings. The “colour” part of the colour screen is also nice, if a bit of a novelty for me. The colours on the screen are a bit washed out / muted, but it is more than sufficient to display a book cover, or to give a decent enough representation of a photo in a blogpost. It is - of course - nowhere near as good as the colours on a non-eInk screen. Here’s a blogpost by Derick Rethans : Overall, while battery life is not a patch on my Kobo Clara HD, it is absolutely fine. To make the most of it, I turn the device off (as opposed to just pressing the power button, which darkens the screen, but does not power it off), if I am not going to be using it for a while. The trade off is that it takes about 60 seconds to boot. With Wi-Fi turned on, it - unsurprisingly - used the battery more quickly than if Wi-Fi is off. So I keep Wi-Fi off too, unless I am using it. I don’t have any particularly good observations here, other than it being worse than my eReader. I charge it a couple of times a week, or more if I am reading loads or browsing a lot, but obviously YMMV. And that’s fine. I am rarely far away from a USB-C cable. According to the Android “About” pane: The device has five printed buttons at the bottom of the screen. The first two - something that looks like a graph, and one marked “AI” - stopped working once I had disabled some of the unnecessary software. While I cannot change the look/text of these buttons, using “Key Mapper” from F-Droid, I was at least able to re-purpose these. I have not dug too much into this app, but it looks pretty useful in itself. Now, the “graphs” button (which gives a key code of 10018) toggles Wi-Fi (after some scary warning screens about “Expert Mode” in Key Mapper), and the “AI” button (which gives a key code of 10019) launches KOreader. Excellent stuff :) It had a Login screen. I pressed “Skip”. I have not logged in or done any form of registration. It looks like one can register an account with BigMe, but it is not a requirement. I have not found anything that I cannot do for lack of an account. It comes with Google Play, and I guess that one would need to sign in with a Google account to use that, but I have not done so and, again, I have not found anything that I cannot do as a consequence (since I do not use Google Play). The BigMe B6 came with Android 14. I could be wrong, but I should be surprised if it got much in the way of software updates. It came with version 3.0.0 of “BigMe” (whatever that is), and there was no system update available. The version of Android that BigMe is using is clearly intended to be on a phone. For instance, in the shutdown menu, it offers an “SOS” function, to call the emergency services. Not a big deal, and certainly something that I overlook easily for the price, but, well, not great either. I am used to having a lot more settings available to me, in GrapheneOS. The BigMe B6’s settings app is spartan, but so far the only thing that has bugged me is lack of a dark mode control. I can do it using , but that is an unnecessary pain. Instead, I just set dark mode manually in each app. Android settings says that the device is encrypted. But I don’t know what is encrypted. So far, I have not put anything sensitive on it, and I do not plan to do so - I don’t know why, perhaps coming from using GrapheneOS instead of stock Android - but while I am happy to use it for recreational stuff, I can’t quite bring myself to connect it to my email account, for instance. Tusky (Mastodon client) Fennec (browser) Tor Browser Readrops (RSS reader) Device name: Smart Office Notepad Product Code: Color Processor: Octa-core Storage capacity: Available 41.93 GB / Total 64.00 GB Screen resolution: 1072x1448

0 views

Static Analysis for the Uninitialized

I really enjoy learning new things in my free time. Lately I’ve been studying this book on Static Program Analysis . The book is very good, but some of the formulas are quite dense so I like to try writing them out in code. The chapter I’m currently reading (chapter 9) has a section on finding possibly-uninitialized variables using bipartite graphs. I’ve found it to be quite enjoyable, so in this post I would like to describe, in my own words, how to do it. We’ll start by analyzing some code from a toy programming language the book uses called “TIP”, and then we’ll use the techniques we developed to analyze some Ruby code. In other words, we’re going to write a program to analyze programs. We’re really going to be meta-programmers today! The TIP programming language is like a very very simplified version of JavaScript. Below is the code we’ll be analyzing: This example is taken straight from the chapter. In TIP, you declare variables with the keyword kind of like in JavaScript. In chapter 9 of the book, the TIP language only has integers; later chapters add pointers. In TIP, 0 is considered false, and there is a special function that returns an integer input from the user. Each line in the above code has a comment indicating which variables are possibly uninitialized. I keep saying “possibly”, and I’ll explain why later. The first line declares the variables with , and all variables start as “uninitialized” when they are declared. The second line gets input and assigns it to , so now only and are uninitialized. The third line adds and and assigns it to . On this line, is still considered to be “uninitialized” because is uninitialized, and the value of depends on the values of and . The fourth line is an if statement, and whether we take the if statement depends on input from the user. In other words, we can’t statically determine whether this statement is taken or not. The if statement doesn’t impact the “initializedness” of any variables, so all variables maintain their state ( and are uninitialized). If we take the branch, the 5th line will assign a value to , so at that line only is uninitialized. On the 6th line, we can’t know whether the if statement branch executed, so we have to say that and are possibly uninitialized. This is why our analysis is only giving us a “possibly uninitialized” result. If the branch is taken, then will be initialized, otherwise it won’t. But since we can’t know whether the branch will be taken, we have to be conservative and say that may be uninitialized on line 6. These two states, and , form a two element lattice, with on top. The top is the “safe” answer: it’s never wrong to say a variable might be uninitialized, it’s just less precise. When two paths in the code meet, like at the end of the if statement on line 6, we take the higher of the two states (because the less precise decision is also the “safe” one). On one path is initialized, on the other it’s possibly uninitialized, so after the if statement it’s possibly uninitialized. The rules for each line in uninitialized variable analysis are so-called “distributive functions”. For example, take the rule for line 3, . If we feed it the set , we get the same answer as feeding it and separately and then taking the union of the results: . This is just like in math class where we could say . In the case of a two element lattice and an analysis that uses a distributive function, we can represent the variable states using a bipartite graph. The book works out the proof for this, but I’m going to omit it (because I want to get to the coding part). But if you want to see the proof, check chapter 9! A bipartite graph is just a graph with “two sides”. Nodes on one side of the graph will connect with nodes on the other side of the graph, but never with their own side. I imagine it like two football teams tackling each other, each player is a node, and tackling someone else is an edge in the graph. Hopefully the players don’t tackle people on their own team, and they form a bipartite tackle graph! In our case, each node in our graph will represent a variable in the program, and we’ll have one extra node that represents “uninitialized”. For one graph, we’ll double those nodes, and use the edges to represent the flow of data through the code. For example, let’s look at the bipartite graph for the first line, : The node on the left represents the value “uninitialized”. On the line, the “uninitialized” value flows from to the , , and nodes. Next let’s look at the graph for line 2, : In this graph, the uninitialized node flows to itself. The variables and haven’t changed, so whatever value was in and just flow to the other side of the graph. The variable got initialized from , so no value flows to the other side of the graph. The reason no value flows is because we’re tracking “uninitializedness”, and on this line becomes initialized, so the absence of an edge means we got a value. If we repeat this value flow process and then line each graph up next to each other, we’ll end up with a graph like this: A black dot in the variable’s column indicates that the variable is possibly uninitialized at that point. Grey dots and lines indicate paths that don’t occur because the link was severed earlier. The dotted lines indicate alternate paths the code can take; an if statement has two outgoing paths. Looking at the graph we can see that (obviously) uninitialized ( ) is always uninitialized (we don’t bother to print that out because it’s not very helpful). We can also see that the black dots correspond to the “uninitialized variable” comments from the example code. For example, right after , the and variable have a black dot indicating that they are possibly uninitialized. If we assign each variable a number (and reserve 0 as the number for “uninitialized”), we can represent this bipartite graph in Ruby using a bit matrix. The graph needs to store a connection between any of the variables, and an “on” bit represents an “uninitialized” state from our lattice flowing from one variable to another. Since we need to be able to record an edge from any variable to any variable (plus the uninitialized value), the number of bits required is . Our example case has 3 variables, plus the extra variable for “uninitialized”, so we can do it in 16 bits. In practice though, to make accessing the data easier, we’ll align on a byte: The way we use this class is by assigning numbers to variables and then calling with the edges we need. For example, to represent , we need to connect “uninitialized” to the , , and variables. If we’ve assigned them numbers, we can do it as follows: Now we can represent the data flow rules for any line in the program. The next step is we need a way to connect graphs together. To do this, we’ll add another method that takes as input a list of “possibly uninitialized variables” (these are also known as “facts”) and returns a new list of “possibly uninitialized variables” based on the data flow rules for that graph. Since the input variables are just numbers, we can use each one as a row index into our bit matrix. Practically speaking, this means we bitwise OR together the rows for each input variable, and then get the position of the “on” bits in the output. Here is the code for the function: Let’s add a second rule for : In the above rule, just as in the visualization above, we connect the uninitialized nodes, then we connect to itself and to itself. These “self” connections mean “whatever value was in as input, we’re going to pass that along as output”. Now we can use the method and chain the two graphs together: At the start of the program nothing has been declared yet, so the only fact is “uninitialized” itself ( ). If we apply that to the first line, we see that all of the variables are “possibly uninitialized” (as expected). We can take the output of the application and feed it as the input to the next graph, and we see that the uninitialized variables are 2 and 3 (or , and ). Now all we need to do is define the rules for each line of code and apply them, and we’ll be able to calculate which variables are possibly uninitialized! Next we’re going to create a class that lets us record rules for each part of the program. The example code we wrote above works well for straight-line code since we can just apply the data flow rules from each line to the previous line. But the example program we have contains control flow and we’ll need a way to model the “dotted line” from the visualization above. To do that, we’re going to construct a simple control flow graph. A control flow graph (or CFG), is constructed from “basic blocks” where each block contains “straight line code” and ends in either a jump or a return. Each block has up to two outgoing edges, and those edges connect to possible jump targets. There are other ways to define a basic block, but this is my blog, so my rules! First we’ll define a class and a class. The class just holds on to the source line (a string for printing), and a graph representing the rules at that line. The class holds the list of objects that make up the block. It also contains a list of successors (jump targets) as well as predecessors (basic blocks whose jump target is the current block): Next we’ll define a class that has a bunch of helper methods that let us construct a CFG along with a graph for each line. I’m not going to explain all of the lines of the class because I think using the class is a better way to explain it. However, the full source is below. Let’s use the class to define all of the data flow rules for the sample TIP program: First we define a new program class. Then we ask the program class to record a source line of TIP along with the data flow rules for that line of code. I added lots of comments to explain the code, but let’s revisit one excerpt just to make double sure: Here we’re asking the program to record the TIP source code , and the data flow rule for this line is that “uninitialized” flows into , , and . This is exactly the same as the bipartite graph we created above, but this time we’re just recording that graph alongside the TIP source code. After we’ve recorded all of the rules, we call on the program so that it can calculate and set predecessor edges. We’ve got multiple graphs going on here. We’ve got a control flow graph that represents the program overall, and a bipartite graph representing data flow at each line. Our goal is to walk the CFG, and for each block calculate variable definedness at each line. The input state for each block is the union of the output state for that block’s predecessors. In other words, in order to analyze the whole CFG, we only really need to record the final state at the end of each block. Also, each calculation cares what happened in the past . For example, the definedness of the variables at line 2 care about what happened at line 1. This means we have a forward dataflow analysis . Any time the facts we derive depend on what happened in the past, it means we need to move forward through the code. So we define a strategy class that lets us analyze the CFG by moving forward through the code: In the above class, the method takes a basic block, looks up the predecessors for the block, then unions the output of those blocks. The function takes a block and some input (which happens to be the return value of ), and for each line of code in that block computes output facts based on the input. Those output facts are the “possibly uninitialized variables” at that line. Since we really only care about the state of the world at the end of the block, we just use an (a left fold for scheme people) over the code in the block. If, after analyzing all lines in the block, the facts are the same as what we had recorded previously, will return false. Otherwise, it saves the facts for that block and returns true. The method returns all blocks in reverse post-order (the traditional order for doing forward analysis that my grandparents used), and the method returns the successors for a block. We’ve got our CFG, and we’ve got a strategy for iterating the graph and analyzing it. Now we need to actually do the analysis. We discussed earlier that this problem is a two element lattice with a monotonic function (it moves up the lattice, never down). That means it’s perfect for a worklist algorithm. Our worklist algorithm is actually quite simple, it’s just this: Given an iteration strategy, ask for a list of blocks to process. While we have blocks to process, get a block. Ask the strategy for the input facts for that block, then ask the strategy to record output facts for that block given the input. If we learned something new ( returns true), then ask the strategy for children of that block and add the children to the worklist (if they aren’t in the worklist already). After this processing finishes, the hash inside the object will contain possibly-uninitialized variable information for each block. We can iterate over the blocks in the analysis and print out the possibly uninitialized variables for each line: Since we only recorded output information for each block, we have to do some extra processing at the end to print out the facts at each line, but if we run this code we finally get this output: Whew. That was a ton of work. But the good news is that we can reuse this framework for other languages too. I’m not going to explain the intricate details of this code, but we can use Prism to parse Ruby and feed that into our class. The function walks the Prism AST and records a rule for each statement, using the same kinds of rules we wrote by hand for TIP. Just to emphasize this, we wrote all of the data flow rules by hand for TIP, but our Prism code does it automatically based on assignments in the Ruby code. Once we get this into the program class, we can perform the same analysis on this code: (The curly braces are just how our class prints if statements, even for Ruby.) If you actually run this Ruby code, will raise a , because hasn’t been assigned yet so Ruby thinks it’s a method call. That’s exactly the kind of bug this analysis is meant to catch! You can find the full source for this program here . I feel like finding uninitialized variables isn’t that impressive. But the power in the technique here is that we can do any kind of analysis using this framework as long as the facts we track are a finite set, and the transfer functions are distributive (as I mentioned earlier). (The book calls this framework the IFDS framework, though I’ve omitted part of the full framework in this post). For example, we could use this same code to do “taint” analysis: “does this variable hold untrusted data?” If we were to number expressions and perform a backward analysis, we could discover what the book calls “very busy expressions”. A “very busy expression” is an expression that will definitely be evaluated no matter which path the code takes, before any of its variables change. For example: In the above example is considered “very busy” because we calculate it regardless of which branch the if statement takes. That means it is a candidate for hoisting above the if statement. Anyway, I’m having a lot of fun reading this book and I just wanted to share the stuff I’ve learned so far. Have a good day!

0 views
David Bushell 5 days ago

Cloudflare killed Deno and look I’m on TV!

“Didn’t you already say goodbye to Deno? Get new material!” I know, I know, but not even a week after declaring my friendship over with Deno they actually killed Deno! The runtime is going away, and sorry call me pessimistic, but I highly doubt a community fork of any value will survive for long. We will support the Deno runtime for another year with monthly releases containing bug fixes and security updates. After that year we will end our development of the Deno runtime. Deno is joining Cloudflare - Ryan Dahl There’s even a Fireship video on YouTube! Guess who gets a namecheck? I’m not going to risk embedding Google cookies so here’s a link: “The runtime wars are over… and Node won” (YouTube) . I also found my way into the lead story of JavaScript Weekly . It’s sad but telling that negative opinions and news around Deno has received the most attention. I have nothing new to say (until my Cloudflare exposé) but I do want to look back on my journey with Deno. People who’ve only read my recent posts on Deno assume I was just a hater. Far from the case! I was all-in on Deno and wanted it to succeed. While it’s true that my disdain for what the tech industry has become leaked into my criticism, I’d say it was fair and accurate. The Deno runtime was truly ahead of it’s time. It felt like using Node from the future (now Node today). The runtime was forgotten in favour of whatever tech start-ups do. My doubts grew by mid-2024 but I regained brief optimism with the Deno 2.0 relaunch. I didn’t realise I’d written this many posts: You may have noticed my interest in Bun in my earlier writing. That was before I learned it was not a serious project. We don’t use vibe-coded fash-tech in my house. Deno was my platform for fun side projects (and Advent of Code ). I experimented with many silly ideas. A few turned into something practical that I still use today. The SMTP adventure was a disaster, but technically it worked! My private git LFS server has been rock solid. I actually found a use case for the HTTP proxy (blog post to come). For reasons (sigh), I’m losing interest in publishing open source code and writing tutorials. My self-hosted journey has stalled because I can’t afford anything anymore. Tech sucks now. But I’m finding enjoyment in fighting for an open web (for humans). I’m having fun too blogging more non-technical topics and getting back into design. I am no longer a JavaScript bro and that’s probably a good thing! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Built with Deno (Mar 2021) Deno Builds on Netlify (Jul 2021) JSR: First Impressions (Feb 2024) JSR: Second Look (Mar 2024) The Deno Package Paradox (Aug 2024) JSR and Deno: Final Review (Aug 2024) Hey! Deno 2 looks fun! (Oct 2024) Deno’s Decline (6 Regions and Falling) (Apr 2025) JSR & Deno One Year Later - Malware?! (Jun 2025) Ditching Deno KV (Jun 2025) 404 Deno CEO not found (Mar 2026) Friendship ended with Deno, now Node is my best friend (Oct 2026) Deno Stream Deck (Oct 2022) The one where I used Deno’s foreign function interface to manage a USB peripheral. SvelteKit and Deno with OAuth and Deploy (Jun 2023) The one where I built and maintained a Deno Deploy SvelteKit adapter. Get in Line, Deno! (Oct 2023) The one where I used Deno’s streaming APIs to efficiently seek lines. Git Granary (Jul 2024) The one where I built a git large file storage server that I still use today. HTTP Tunnel and Proxy in JavaScript (Oct 2024) The one where I piped two streams together to create a simple HTTP proxy. I Let The Emails In (Sept 2025) The one where I wrote a bare bones SMTP server with Deno.

0 views

Type-safe HTML templating in Zig

I’ve done a lot of web development over the years. I started around the year 2000, just after PHP 3.0 was released and started taking over the web world. I’ve used all kinds of approaches for generating HTML: raw PHP, string concatenation in Python, Smarty templates in PHP, Template Toolkit in Perl, and Jinja/Django templates in Python. Then, as the world moved on to SPAs and JavaScript frameworks, I stopped working on frontends. Two years ago, I picked up Zig, and I’d never have imagined I’d be considering web development in such a low-level language. But as I was building my own ecosystem with zio and Dusty , I realized it’s not that far-fetched. I built these libraries for API backends, where the output is JSON or MessagePack, but then I saw that with proper use of arena allocators, even web development is actually manageable, since you don’t need to remember to deallocate all the strings that are typically involved. And with Zig’s comptime, you can build very high-level interfaces without losing the performance benefits. And with the help of htmx or Datastar , you can even build SPAs without touching JavaScript/TypeScript, which I promised myself I’d never do. That said, one thing was still missing: templating. String concatenation was not an option, I didn’t want to go back to the pre-2000 CGI days. I had a few requirements for a templating library: I couldn’t find any solution that would satisfy all these requirements. Then I saw Ziex and I was amazed. It never occurred to me that the JSX approach could be done outside of JavaScript. The developer experience Nurul Huda managed to build in Zig was just unimaginable to me. It didn’t fit my needs directly, because I didn’t want a WASM framework, but the syntax and his approach to integrating Zig code into templates gave me some ideas. Then, after further research, I discovered Templ for Go, and it was pretty obvious to me what I wanted to build. So, let me present zt (I’m not good at naming, sorry) . It’s an adaptation of Templ to Zig. The templates are parsed and transpiled to Zig code during . That means you can import them directly from your app code, but also, perhaps even more importantly, the templates can import your app code. As a result, all parameters can be fully typed. If something can’t be rendered, the Zig compiler will tell you so. The generated code is fairly simple, all it does is write to a . You can write directly to a socket or accumulate the output in a buffer, your choice. Here is what a template looks like: And here is how you would use it from Zig code: If you want to use it inside Dusty to render an HTML page, you just do this: As a side note, the snippet above has a very non-obvious bug in error handling if you use the same pattern somewhere cancellation matters. I’ll probably write a blog post about it, but if you are interested, check out this Ziggit thread . The function in Dusty that wraps the handling solves it. All dynamic values are HTML-escaped by default, both in text and in attributes, and values in URL attributes like or are also checked for dangerous schemes. If you really need to output raw HTML, you have to ask for it explicitly with , so it’s easy to spot in the template. The parser also requires all elements to be properly closed, so you can’t accidentally produce broken markup. It won’t protect you from everything, but it removes the most common sources of XSS. What I like the most is that template errors are just Zig compiler errors. If you make a typo in the template above and write instead of , will tell you: The error points to the generated code, but each line has a comment with the location in the original template. The same applies to the calling side, if you try to pass a string where expects a , it won’t compile. Beyond the basics shown here, you can build reusable components and pass them around as values, and with you can build layouts that wrap other templates, which covers what you’d use template inheritance for in other languages. Control flow is just Zig, so , and work as you’d expect, including payload captures. All of it still compiles down to a sequence of writes, without any allocations. You can have a look at the TODO example in Dusty. It’s a simple htmx-based web app that showcases this library, plus other parts of Dusty that are needed for building a dynamic SPA. One part of the syntax is still open. Block-level control flow is written as plain Zig, following the example of Templ, so a line starting with , or is parsed as code. That means text like “if you forgot your password…” on its own line is a parse error, and you have to wrap it in an element or write it as a string expression. I’m considering switching to , and , similar to Razor or Twirl, which would remove the ambiguity, at the cost of slightly noisier templates. If you have an opinion on this, please leave a comment in this GitHub issue . Although multiple people are already using it, the project is not very mature yet, so I welcome any feedback. You can open a GitHub issue if you find any problems, or ping me on the Zig Discord. It has to be type-safe, with all inputs typed and checked at compile time. I’ve dealt with dynamic languages enough in my life, and I don’t want to deal with runtime template errors anymore. It has to be fast. No allocations, no intermediate state, just write the output to directly. It needs to have safe defaults. I don’t want to deal with XSS anymore, so all inputs have to be escaped by default. It should still feel like writing HTML. I didn’t want a Zig-based DSL I’d have to fight constantly.

0 views
Ahmad Alfy 1 weeks ago

Iframes that finally fit their content

Chrome 154 lets an iframe grow to the height of its content with one line of CSS without having to measure, without messages, and without resize scripts. But the page inside the iframe has to agree to it. That catch is the most interesting part of this feature, so this article spends some time on it. I have built many checkout pages that use a payment provider’s iframe. The goal was always to have the card form feel like part of the page. The user should not notice that it comes from another website. The iframe never helped with that. It has a fixed height. Make it too tall and you get an empty gap under the form. Make it too short and you get a scrollbar inside the page’s scrollbar. On mobile, that second scrollbar is the worst thing you can show someone who is about to pay. So I kept increasing the height, testing on different phones, and increasing it again. This is a small problem. It should have a small solution. For years it did not. The parent page cannot look inside a cross-origin iframe. It does not know how tall the content is. So the only way was to make both pages talk to each other with JavaScript. Inside the iframe, you measure the content and send the height to the parent: In the parent page, you listen, check who sent the message, and set the height: This looks short, but it hides a lot of problems: That last point did not go away with the new feature. It just moved into the browser. In the parent page, you add one CSS property to the iframe: Inside the iframe, the page opts in with a meta tag in its . It also says which websites are allowed to size it: That is all you need for content that does not change after it loads. The browser measures the content and sizes the iframe. No messages, no listeners, no origin checks in your code. The meta tag must be in the HTML from the start. Adding it later with JavaScript does not work. If the content changes later (an error message appears, a section expands, more comments load), the page inside the iframe asks the browser to measure again: So JavaScript does not disappear completely. The embedded page still calls one function when its content changes. But the hard parts are gone. The browser does all of that now. also accepts , and . For most pages, is the one you want. You can still combine it with limits like . This is the use case I care about most, and it is also the one that depends most on someone else. A payment form changes height all the time. An error appears under the card number. The user switches from card to wallet. A saved card list shows up. With , all of this could look smooth inside your checkout. But you cannot turn it on from your side alone. You control the CSS on your checkout page. The payment provider controls the page inside the iframe. Only they can add the meta tag, list the merchant sites that are allowed, and call when their form changes. So for payments, the real question is not “does Chrome support this?” It is “does my payment provider support this?” Today, most providers either ship their own postMessage script or do nothing. If you work with one, ask them. If you build one, this is a cheap win for every merchant using you. Comment sections, contact forms, newsletter sign-ups and booking widgets all have the same problem. Their height depends on the content: the number of comments, the number of fields, the validation errors. These services already control their embedded page, so adding the meta tag is easy for them. The list fits well here, because they already know which customer sites embed them. I have this exact problem on this website. My contact form is a Wufoo form inside an iframe, and I set its height by hand until it looked right. Each step of a form has a different height. Step one has two fields. Step three has ten. Today, you either reserve space for the tallest step or let the iframe scroll. With this feature, the form calls after each step and the iframe follows. This one does not need any third party. Many apps show HTML email previews, rich text previews, or code demos inside a sandboxed iframe using . Here you write the embedded HTML yourself, so you can add the meta tag directly. This is probably the easiest place to start using the feature today. Browser support. This is Chromium-only for now. Firefox and Safari do not support it yet. Keep a fixed height as the default and switch to content sizing only where it works: Inside the iframe, check before calling the new function. Your old postMessage code can stay as a fallback until support grows: Layout shift. The iframe loads after your page, then grows. Anything below it moves down. If the iframe is in the first screen, this can hurt your Core Web Vitals. A sensible reduces the jump. Do not use without a reason. Letting any site read your page’s height can leak information. For example, a page that is taller when the user is logged in tells the parent something about that user. List only the sites that need it. This works together with the CSP rule, which controls who can embed you at all. For years, a simple layout need, “make this box as tall as its content”, required two scripts on two websites that had to agree on a message format. Now it is one CSS property, one meta tag, and one function call when things change. The browser side is ready in Chrome. The rest depends on the people who build the pages we embed. If you run a payment gateway, a comments service, or any widget that lives inside an iframe, add the meta tag. Your users’ checkouts and pages will feel like they were built as one piece. I will come back to payment providers in our region specifically in a follow-up post. Width changes height. On a small screen, text wraps more and the content gets taller. Every resize or phone rotation needs a new measurement. Content arrives late. Fonts, images and error messages load after the first measurement. The height is wrong until someone measures again. Dynamic content. If the page inside the iframe shows or hides elements after the initial load, the height changes and needs to be measured again. More than one iframe. If the page has several, you need IDs in every message to know which one to resize. Both sides must cooperate. If the third party does not send its height, there is nothing you can do. You go back to guessing a fixed height. Responsive iframes in Chrome 154 , Chrome for Developers New in Chrome 154 , Chrome for Developers

0 views

No Man Is an Island

In this post, I argue that individual intellectual activity can only be sustained in an intellectual community of other humans. AI dissolves these communities, which in turn makes private intellectual activity rarer. Some years ago, when it became clear that AI would solve software engineering, my thinking was: The second point has not quite worked out. What actually happened? First, the discourse of software engineering became worse. As I wrote earlier : Claude Code was released a little over a year ago. In that short time, software engineering has been completely transformed. Materially, it might be positive: higher productivity, though at the cost of a messier codebase. Socially, it has been a disaster. The discourse around software engineering has gotten dumber. It’s like everyone in the industry lost 30 IQ points. People used to talk about compilers, type systems, logic. Now they talk about “prompts”, “harnesses”, “loops”. The discourse is narrower, shallower, and more repetitive. There’s only so many times I can hear about “agentic harnesses” before I lose my mind. Then there’s the loss of human capital formation: there is nothing to learn. Prompting is not a skill, at least, it’s a much shallower skill than software engineering. The instrumental dimension of the work has improved in that people can get more output per unit of effort, but the dimension of work that’s about building up human capital has collapsed. And maybe this is rational. Why learn to code at all? The computers can do that for us. And so the rigorous, systematic thinking you need to practice in order to be a good programmer: all gone. The machines can be rational for us. We can just vibe. Second, contributing to the commons of software engineering is increasingly pointless. Before AI, you could publish open-source code, write blog posts to share ideas or inspire other people, write expository texts like tutorials, forum posts, textbooks etc. to teach people. After AI, what’s the point? It’s not just “you can’t get GitHub stars or traffic to your blog”; rather, there is no sense of a common human project you can contribute to. There’s your own private garden of code, which you can grow infinitely in all directions with the help of AI, but you never have to leave the garden and go to the bazaar to trade with people. Under these conditions, it’s hard to care or do anything. But does it actually matter? Does it matter if we stop writing blog posts about obscure JavaScript features, and designing new programming languages? Maybe writing code was always drudgery, and now we can move on to higher things, like math— oh, wait . From observing what happened to software engineering, and what’s currently happening to mathematics, I think we can derive some general insights about intellectual practices in general. We tend to think of intellectual activity as private and solitary: the philosopher sitting in his armchair, deriving the world ab initio . But intellectual activity has two inputs that can’t be acquired in isolation: a shared body of work to build upon, and motivation . The shared body of work is communal, unless you want to recapitulate the entire tech tree. Motivation we can break down into two components: We tend to think of intrinsic motivation as the purest kind: endogenous, self-created, unmotivated by material or social gain. But it’s an emotion, and, like all emotions, it’s transient and short-lived. And this is rational: otherwise, we’d all be stuck in life-long unproductive obsessions. So, we need something to fill the gaps between moments of divine inspiration. Extrinsic motivation serves this function. Private intellectual activity that is sustained, complex, and long-term requires an external intellectual community to provide material and motivation, like fuel and oxidizer. That private activity, in turn, sustains the community: by publishing papers, textbooks, code, etc., you add to the shared body of work for others to build on top of; by citing someone’s paper or contributing to their repository, you give them the recognition and honor that confirms they are doing useful work, which in turn motivates them to keep contributing. Without community, you don’t get isolated individuals each working on their own things: you get nothing. The inputs to intellectual activity dry up: no one is adding to the shared body of work, and there are no peers to benefit from your own intellectual activity. Without this extrinsic motivation, you get less intellectual activity because, again, intrinsic motivation is fleeting. After AI, intellectual contributions become unnecessary or redundant. In the case of software: the AIs write all the code, so what’s the point of writing either code or prose? The audience for those things is now severely diminished. Humans don’t write code anymore, so they won’t read blog posts about how to write code, or tutorials, or try new libraries or programming languages. In the case of mathematics: the AIs can prove theorems, write papers, explain papers, tutor students, and in the near future, they might write entire textbooks better than humans. So what’s the point of writing a paper, or a textbook? It’s superfluous. If intellectual activity is unnecessary—if there’s no consequence to designing a new programming language or publishing a paper, or if there’s simply no community to contribute to—then it won’t happen. There’s no point. Now apply this to every other domain of intellectual activity, and you see what the future looks like. There may be individuals building new libraries and programming languages, but no shared culture of software engineering; there may be individual students and practitioners of mathematics, but no living community of mathematicians. I’ve spoken to people who think AI will have a positive effect on the life of the mind, and their thinking is that right now too many people are doing intellectual activity for instrumental reasons: citations, clout, etc. In this view, the collapse of intellectual communities is good , because it sifts the intrinsically-motivated übermenschen from the clout-chasing masses. I think this view fits with contemporary society: we view intrinsic and extrinsic motivation as high and low status, respectively. A “developed” person is supposed to have a private, inexhaustible reserve of motivation which is causally disconnected from external reward. But this is not a realistic view of human beings. Humans are social animals who can only flourish in the society of other humans. We care, and we should care, about contributing to the world. And if technology makes our contributions superfluous, then what is left? Thanks to Luke Drago and Andy Matuschak for feedback and conversations. In my professional life, I’m happy to move one level up to become a manager of AI agents. I’m literate, I’m a good technical writer, I can describe what I want and let AI agents write the code. In my own time, I can keep doing the things I care about because I enjoy them intrinsically. This is the “intellectual” side of software engineering: reading technical blog posts and papers, learning new programming languages, designing new programming languages, writing technical essays, writing code for my tiny open source projects. You write a blog post: who’s going to read it? The next training run will ingest it, marginally improving AI capabilities. Maybe the post was a workaround to some obscure technical problem, so the next time someone encounters that problem, they will ask Claude, who will solve it without crediting you. Maybe you had some insight about how to structure large codebases: who cares? The humans aren’t making those decisions anymore. You design a revolutionary new programming language: who cares? Maybe Claude cares, for what that’s worth. But humans don’t write or even read the code anymore. The programming language is an implementation detail the humans no longer have to care about. You write a library, and publish it on GitHub: who cares? The AIs might discover it, and use it, but the operator won’t know you exist or did anything. Intrinsic motivation: we learn for the sake of learning, we create art from a compulsion we can’t understand, etc. Extrinsic motivation: David Chapman defines “nobility” as manifesting glory for the service of others, and using our abilities in service of others. We want our work to be useful to others, we want others to benefit from our work, we want to contribute to a shared human project. Fame and the esteem and good will of your peers are the proxies by which we measure our contribution.

0 views
Armin Ronacher 2 weeks ago

What is Codemode

More than a year ago I wrote a few posts here that recommended people not to load custom tools into their context (or MCP servers) but to just use more scripts. Most importantly I wrote that Code Is All You Need and I wrote about that MCP needs code . With Pi 1.0 we now added MCP support via Codemode which in some ways is a long time coming, but then also maybe somewhat surprising to some. So I want to share some updated thoughts on this blog on what this all means. When a harness like Pi provides tools for an LLM to call, it does so by supplying some tool definitions which then translate into some token structure on the server side. Whether a model is encouraged to call a tool is the result of the reinforcement learning process. Something I wrote about before if you want to learn more. One of the reasons we strongly lean towards CLI and bash is because it allows easy composition of calls, and because the model also learns how the file system works when it’s trained. So when it invokes a tool like the model also learns that after that tool call, there is now a file called in . However bash has one fundamental limitation which is that it can only compose programs that run. And there are some things, which are not programs, but native tools to the LLM and they sort of have to be. The most obvious example here is or . If a multimodal model needs to read an image, it cannot use for that because the harness needs to inject the actual image payload into the protocol of the LLM. Another quite vivid example are sub agents. In order to spawn and orchestrate sub agents, it’s tricky to avoid tools that are provided by the harness. While in theory the agent could provide a CLI tool that talks to the outer harness via environment variables and Unix sockets, it’s a rather crude process. It however has another issue, and that is where the code runs. To better understand that, it’s important to think a bit more about where all the bits and pieces run. There really usually are two different systems involved. The first is the brain, the harness: it runs on one machine. It’s trusted. The second is often the same machine, but it’s really where the tools are executing: the hands. In Pi we now call this the execution environment, but you can think of it as the target of all the operations. Crucially what is important for us, is that there is a dividing line between the harness brain and the target environment that runs bash and executes the tools. And splitting this in half has some really important consequences. For a start it means that they are running on different file systems and they have different levels of trust. If you for instance use a sandboxing solution like Gondolin your bash stuff will be sandboxed just fine, but the harness itself will not be. Which brings us to what Codemode really does: it’s a way for the LLM to express and orchestrate complex operations on the harness side, but not the execution environment side. Codemode runs in the harness, in its own sandbox. In case of Pi it’s running in QuickJS within a WASM runtime with intentional limitations: no network, no file system, no timers, limited RAM. The only way is to call more tools. You could also imagine that Codemode could run Scheme or some other language as well. If you are not familiar with Codemode, it’s basically just a way to issue tool calls from within some language, in our case JavaScript. That allows you to compose those calls without necessarily going through the LLM’s context. Credit for naming goes to our friends at Cloudflare who coined it . For instance if you issue a bash call as a regular tool call in the LLM, then we only throw the trailing 2000 lines into the context and if the agent wants more, it needs to look at the overflow file itself. If however the agent issues that invocation via Codemode, then the Codemode side gets larger outputs sent structurally. Most importantly, because Codemode is JavaScript the agent can express concurrent operations and basic workflows. A common way in which you see agents now use this, is to first probe at 5-10 items from some tool response to see what it looks like, and to then write a Codemode script that processes the next n items. Codemode also allows you to throw state into the transcript! That means that one Codemode invocation can stash away data, that the next call in the session can load again. And remember: this is on the harness host, not the sandbox. In case of Pi, Codemode also allows you to issue calls that naturally do not make any sense in Pi’s traditional interface. For instance if you want to generate images with an image model or you want to classify some text with a one shot classifier model, those Pi APIs are exposed via Codemode, but not via regular tools where they would just waste context. So now that we talked a bunch about it, it’s probably worth being a bit more explicit about it. Let’s walk ourselves through some invocations of Codemode of recent Pi sessions of mine. Note that none of this code is human written. It’s from real sessions of Pi, just re-indented for your viewing pleasure. The agent starts using Codemode automatically either because it’s a task where the model already naturally picks up that tool, or because a user asked it to. Note that Codemode is by default only enabled in Pi when MCP is enabled, but you can turn it on with in the settings. Just ask Pi to enable it for you. Let’s start simple with image generation. Image generation is a feature that Pi supports in the AI SDK core, but it’s not a tool that the agent can use. In the past the only way to use image models has been to write a bespoke extension or to have the agent run node itself and use the internal image APIs. However because we expose quite a few of the internal model APIs within Codemode, it means that the agent can use it: Note that the call to sends the image back as image content to the LLM. On the harness side it feeds it directly into both the agent, as well as onto disk as a temporary artifact in case the agent wants to be able to pass that image back to bash. Similar things apply to classifier models such as Jev . They also do not fit well into the workflows of an agent through the typical tools. But rather than making a bespoke tool available, Codemode just allows the agent to reach into the AI SDK and invoke those directly. Here you can see how Jev is used to mass process GitHub issues for a quick sentiment analysis: Note how in that above example we also call which dumps the result of that execution into the session transcript. A future invocation of Codemode can thus read back that result if it wants to. The here is fine, because Pi limits the total number of concurrent tool executions itself to four and maintains a queue for the rest. A more adventurous example is to use Jev to drive a game engine for debugging purposes: Here it knows about my command and it built itself quickly a minimal harness around it to drive a game loop to assist a user with debugging a problem. Note how it built a 30 step loop in which each step goes back to both the game engine to get a text dump of what’s going on, and then to Jev to determine what to do next: Lastly, Codemode obviously is great for calling MCP servers. And because we do not actually expose any of the MCP tools to the LLM, the agent first uses provided APIs to issue a tool search within Codemode to discover what it might be able to do with the connected servers. This form of progressive discovery makes the whole MCP business work well enough for a lot of use cases today. Here for instance you can see the agent reach for the Sentry MCP straight away, even without discovering the tools, presumably because it has learned during the RL process already about what the Sentry MCP looks like. But it learns from what we inject into the system prompt, that the Sentry server is available to begin with. It’s not completely guessing here. I really don’t want to talk too much about MCP here, but MCP is in fact a protocol that greatly benefits from Codemode. The problem in parts is that MCP in practice often targets harnesses that do not (yet?) use Codemode. But the tide is shifting. In the meantime, a temporary crutch has been to do what Cloudflare did, and do Codemode within the MCP server. But now we have Codemode in Codemode which is pretty bad. It means double JSON escaping, easy for smaller models to get confused by and the inner code cannot call the outer tools. So if you for instance use the Cloudflare MCP servers in Pi, the agent needs to write JavaScript and funnel it through more JavaScript. This is really not optimal, but it’s also understandable that this is happening: So to end things off: how well does Codemode work with MCP today? Well … not amazingly well. That’s because MCP servers are not really targeting harnesses that use Codemode yet (though at this point I think most harnesses support it). For this to work well some recommendations: So where does this leave us? Is this a reversal of what I wrote a year ago where I encouraged CLIs? I don’t think so. In fact, the MCP ecosystem from my perspective picked up on exactly what we pointed out a year ago works: code. But Codemode goes beyond MCP in that it can act as a capable mechanism within the harness to express more freedom for the agent. There are however also some things that we still need to figure out. For one, durability with Codemode is trickier. We might have to adopt some ideas from durable workflow engines here to snapshot invocations. Or maybe, something like Starlark is a better composition language than JavaScript given its deterministic nature. Images, binary data and just the inability of this pattern to work with smaller models is also something that needs to be fleshed out. So it’s for sure not a perfect solution yet, but it’s quite a useful pattern that I expect us to leverage more. Structured content: Codemode wants calls to return some nicely formatted JSON. So that needs to come back from the server, and many don’t do that yet. The system in MCP is great for that. Consistent results: an interesting failure case is when an MCP server does not return consistent data. For instance because it tries to token optimize things depending on how many items are in the result set. This can cause an initial probe with 5 items to succeed, but then fail when the server returns the maximum batch size. Large binary data: today MCP does not yet support large binary data so quite a few use cases that are really interesting do not work well at all yet. You end up with all kinds of weird workarounds such as pre-signed URLs to allow file uploads then to happen through non MCP channels. Composable tool search: the MCP server might know better than the MCP client which tool is appropriate for a task. But there is no good mechanism today that allows a harness to fan out tool searches across multiple MCP servers. It’s all emergent behavior and it does not scale well to multiple active servers.

0 views
David Bushell 2 weeks ago

Friendship ended with Deno, now Node is my best friend

It’s finally time I go crawling back to Node! I’ve been using Node heavily this month on a SvelteKit client project. When did Node get so good‽ Deno has been my go-to runtime for so long I forgot how to Node. Now I’m back, I find all the ECMAScript † sugar is supported and the old annoying APIs have been replaced or modernised. Most importantly, I never have to see . † Doesn’t seem like that Oracle trademark dispute will see a positive end :( The official Node docs recommend piping an internet script straight to bash (we never learn) to install NVM to manage Node & NPM. My (ancient) experience with NVM and NPM hasn’t been stellar. I heard Fast Node Manager (FNM) was better to switch Node versions. Obviously I roll bleeding-edge but I have client projects that demand stability. I opted for PNPM too to avoid getting immediately pwned. (The “M” in NPM stands for “malware.”) Some scripts I use have hard-coded binary names, so I added two aliases: Maybe that’s a crime but so far it’s worked flawlessly. PNPM also blocks post-install scripts. Does NPM still yolo those? I added additional settings to to delay malware updates. At first I tried setting the minimum release age to “one month” because it takes Microsoft at least that long to remove reported malware. This caused dependency issues where PNPM struggled to match suitable versions. I settled for “one day”; long enough to allow some other sucker to beta test the next Shai‑Hulud. Node can now run TypeScript without throwing a tantrum like a baby if the stars don’t align. That said, one does not simply publish TypeScript packages to NPM. Why? Just strip the types bro, I know you can! Let me sign a deal with the devil! To discourage package authors from publishing packages written in TypeScript, Node.js refuses to handle TypeScript files inside folders under a path. Node.js v26.10.0 documentation This restriction is philosophical rather than technical. I get it though. TypeScript is a Microsoft product. Opening that floodgate would pollute the entire ecosystem. Nobody wants more Microsoft. I’d love to see light “native types” in ECMAScript. There are type annotation proposals . I suspect I’ll be retired before those bear fruit. No TypeScript packages mean I need to find the latest churnware slop to bundle my stuff. Tsdown did the trick, with only two additional dotfiles. Not thrilled about that (every dotfile represents a mistake). I suppose I break even after deleting etc. Speaking of Microsoft lock-in, because they wrecked GitHub I’m self-hosting my own Forgejo instance . NPM limitations mean my packages have lost “provenance”. I had to configure the PNPM trust policy to allow my own stuff. Fun times! My final test for Node was converting my static site generator from Deno. Not many Node versions ago this would have required a major refactor. Today with I found surprisingly little work to do. The only required changes were to replace Deno’s file system API with — which is vastly improved from what I remember (literally ~10 years ago). Aside from that, I had to replace with Hono’s node adapter (a wrapper around ). After this minimum-viable migration I was shocked to see 15% faster builds . My codebase still favours idiomatic Deno. I bet I’m leaving performance on the table by not using other built-in Node APIs. That’s something to explore later. The only further change I made was to replace Deno’s with which is a straight import swap. So if I were to TL;DR in the middle: Node got a glow-up, wow! You’ve probably known this for a while. I kept using Deno out of habit and familiarity. And I haven’t exactly been enthused to write server-side JavaScript recently. I’m burying this part because it’s flogging a dead horse. Ultimately, Deno failed when they allowed the Silicon Valley Circus to define “success”. Deno went from an innovative modern JavaScript runtime to a boring start-up with uncompelling products . Half the employees were laid off and what’s left are tweeting AI fantasies and vibe-coding Temu Cloudflare. There is no reason to use the Deno runtime today. Deno Land Inc. stopped innovating that years ago. Node has slowly but surely caught up, even surpassing Deno in places. What finally pushed me away was: Basically stuff that made it borderline unusable on top of my other criticism. JSR support were very quick to delete my account on request. I don’t like leaving dead profiles around the internet. None of my packages are visible but old versions remain installable. It was fun early on but now it’s time to say goodbye. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Broken ZSH integration for weeks JSR’s aggressive “429 (Too Many Requests)” Bug(s) that made Deno choke on concurrent HTTP requests

0 views
Takuya Matsuyama 2 weeks ago

Hackers and photographers

Hey, what's up? It's Takuya here. Agentic coding has become a crucial workflow these days. It has completely changed the landscape and perspective of software development, even for indie developers like me. At the same time, many of us are anxious about this drastic economic change. For most of us, this is the biggest paradigm shift we've experienced as programmers. I struggled to accept it, and it took me a long time to get through bargaining and depression. But after using Opus 5.5, I felt AI had become smart enough for me to quit writing code myself. Now I've mainly been reviewing generated code in Neovim. It will keep evolving, and I'm certain I'll be touching lines of code less and less in the future. Now, I'd like to discuss what we should learn and how we should change our mental model of programming. Also, I thought it'd be interesting to take a snapshot of my thoughts and feelings in this unique moment. There is a popular book called  Hackers & Painters  by Paul Graham . It argues that hackers are makers, like painters, rather than scientists or mathematicians. One quote beautifully describes how we built software in the pre-AI age: Like painters, we discovered things during the process: I loved sketching directly in HTML to design a website. It gave me so many ideas along the way. That's because code wasn't just the output. It was how we thought: So our code represented our thoughts, or the history of our thoughts. No wonder we got attached to it. With agentic coding, I still sketch and refine, but no longer in code. I've stopped thinking through code. Instead, I think in natural language, like English and Japanese. That's why some people say English is now the best programming language. Now you can ask your agent to build a web page, and it's done in minutes. In this process, you skip thinking about variable names, which tags to use, how the DOM should be structured, CSS properties, and so on. These were our brushstrokes. As DHH pointed out in  his keynote at Rails World 2026 , portrait-making went through the same change around 1900, when Kodak's Brownie camera made photography affordable. Anyone could get a realistic portrait cheaply, without learning to paint. Painters realized that: The same thing is happening to programming. As Nolan Lawson wrote in  his blog post : Like the painters, they needed another domain. This is a clear sign that, with AI, hackers' work has changed from painting to photography. At the same time, almost anyone can make software without knowing how to write code. Let's look at what non-professionals have been making with their new technology. Because it's so easy to build things with AI, people have been publishing a massive number of products. According to  RevenueCat's State of Subscription Apps 2026 , new subscription app launches grew from about 2,000 per month in January 2022 to over 14,700 by January 2026. Some of these apps look incredible and unique, but many are just yet another to-do app or habit tracker, and people call them AI slop. And the flood hasn't turned into revenue: apps launched before 2020 still generate 69% of subscription revenue, while apps launched in 2025 or later account for just 3%. This has been happening in photography for years. Having a good camera doesn't make you a great photographer. Your smartphone, with multiple lenses and a 48MP sensor, can take stunning AI-enhanced photos with just a few taps. My parents have iPhones too, and they take photos that just put the subject in the center (called Hinomaru-kozu/日の丸構図 in Japanese), without caring about lighting or composition. They just capture whatever is in front of them. The photos are technically sharp and well exposed, because the phone handles that. But they don't say anything. Interestingly, nobody calls these photos "photo slop", even when they're posted on Instagram for everyone to see. We're used to seeing them everywhere. People call apps "AI slop" because, until now, publishing software has mainly meant making something for other people to use. But this landscape is going to change. In a world where everyone can make software as easily as taking a photo, we'll get used to seeing tasteless apps, just as we got used to snapshots. I have no doubt my kids will enjoy making their own "apps" without knowing JavaScript or any other language, and nobody will call them slop. Then, what role do professional software developers play? I've been taking photos  since 2015 , and I also publish videos on YouTube as devaslife . If you've watched my channel, you may recognize my video style. It mainly comes from what I've learned through photography. There are still a lot of domains where you have to learn photography. For example, if you run a business and use Instagram to attract customers, you have to post "good" photos (or videos). Unlike snapshots, these are made for other people. To get photos that work for your business, you can't avoid learning shutter speed, aperture, ISO, composition, lighting, and so on, so you can get the result you want instead of relying on the defaults. On top of these skills, you'll need to develop your "taste". It's a vague concept, but  Mitchell Hashimoto  defines it as: In photography, I believe taste is knowing what makes a picture pleasing when you see one, and being able to reproduce it. It can't be measured. It's not scientific or mathematical. When you enjoy something, you have to carefully observe your mind, then decompose and analyze the feeling. Reproducing that essence is another level. It takes a lot of effort to reliably get the result you want by yourself. This is where professional software developers now have to compete. You need not only your own taste, but also the ability to reproduce it in your work as a professional. As mentioned earlier, a programming language is not for thinking of programs anymore. Your agent translates your thoughts into code efficiently. Now your job is to write out the directions. The knowledge and experience you accumulated by writing code should be articulated in natural language, so your agent can understand and apply it. You are the only one who can define the right goals, directions, issues, and questions. So, for your agent's output to reflect your taste, you need to be good at telling it what you want. Just as a 48MP sensor doesn't make you a great photographer, Opus 10 won't make you a great developer. (I've personally been doing this by taking tech notes, but that's another story.) For beginners : You are lucky. You don't have any old habits to unlearn, so you can start thinking in natural language from day one. Build as many things as possible with AI, just like taking lots of photos with a camera. Find good examples, and try replicating them, again and again. If I were a beginner, I would do that. At some point, you'll start noticing quirks you can't fix just by prompting. That's when you need to learn how things work under the hood. I didn't know anything about RAW development until I bought my first Leica, and  the photos straight out of the camera stopped satisfying me . I no longer grieve when I see my agent rewrite my code. My app is still mine, because it reflects my taste and ideas. No one can predict the future, but I feel it's time to change gears. Hackers were like painters Hackers are now like photographers Nobody calls snapshots "photo slop" Know why it feels right Articulate your taste in natural language

0 views
Jason Scheirer 2 weeks ago

Oh, Goddammit: I'm Back on C++ in 2026

The title for my autobiography would probably be I Thought I Was a Child Prodigy But I Am Actually a Late Bloomer . Going from being the youngest guy in the room to the oldest guy in the room happens suddenly, surprisingly, and permanently. As such, the programming languages and technologies I learned long ago are esoteric knowledge but the systems they run on still exist. Everything’s moved up a level of abstraction or six and there’s a lot of low-hanging value in plain old systems programming again. So for the first time since 2014 or so I found myself writing C++ starting March 2026 and carrying over to today in late 2026, both dates in 2026. 2026. From the start, quite simply: Windows APIs. Relatively obscure ones. They come with C++ headers and opaque files and COM interfaces that fall well outside of the coverage footprint of . I’m writing RDP plugins , which with some gymnastics I could do in Rust but it would be a lot more work to do the cooler thing. It’s less friction to just say “okay” and do it the way they tell you to do it. Spend those innovation tokens on cooler shit than this. And I’m doing Citrix channels , which are straight up C++ or Go Fuck Yourself (I believe the SDK docs explicitly use this language). Also, there are old features that aren’t exposed to moderns stacks that give us easy wins that exist on users’ systems without additional upgrades or installs. The RDP client bridge binary I wrote is under 50k. It builds in 3 seconds. It runs on every Windows machine. Hot fuck on balls, that beats the 50 minute Typescript BDSM sessions I engage with on CI at work. After the C++ APIs on Windows, doing systems programming at the lowest common denominator let us bend the computer to our own will with less supply chain debt. We are superpowered by C++ in a world where nobody knows C/C++ anymore. I’m writing little tiny bits of NodeJS C++ libraries to integrate new features Electron lacks, and this has worked well. I can add things Electron Just Doesn’t Have. The process is a lot quicker than asking someone on the Electron project to write it for me, or writing it myself and spending my nights and weekends shepherding the itch-scratch PR through the bureaucracy of Electron. I can add value now, goddammit and make our Electron app something that should be an Electron app, because it does actual desktop shit . Here are some good things about C++: LLMs I guess. Fear? Nothing good or defensible as a reason. “Oh it’s dangerous” DRIVING IS DANGEROUS. EATING FOOD IS DANGEROUS. GOING OUT IN PUBLIC IS DANGEROUS. LIFE IS DANGER. You can easily link in legacy C and C++ code There is a lot of C and C++ code you can integrate quickly and well. I chose miniaudio as a lowest common denominator for audio processing (and miniaudio is not ’legacy’, it is a modern project on a conservative stack). Cross platform means it took 2 hours to get a working Linux build out of the code I had originally written for Windows. Notion on Linux works better than Electron would have done the job on Linux because I accidentally got cross-platform by default by writing my code like this. It’s easy to write node.js bridges in it: I even use Objective-C++ for my macOS integrations “Modern” C++ has a lot of features that make it less awful to use – I’m pinned on C++20 and and gives me structural pattern matching. is one of the cleanest JSON libraries I have used in any language and it’s just a header . Compile times are blink-of-an-eye fast. Build stacks are just there on every system. It’s fun to spite people who uncritically adopt shiny things like Rust.

0 views
マリウス 3 weeks ago

Updates 2026/Q3

This post includes personal updates and some open source project updates. もしもし、マリウスです, faxing at 9600 baud directly from Tokyo, Japan . After a short hop over to Hong Kong and Shenzhen , I’m back in the beautiful neighborhood of Shimokitazawa , where I’m spending almost the entire rest of the year until right before Xmas, when I’ll be heading back to Shenzhen. First things first, a warning: This is a very long post as it includes a metric ton of project updates. If you’re interested in the stuff that I’m building then this update is definitely for you. However, let’s start off with some infrastructure-related things. A few major updates happened in the past quarter with regard to my infrastructure. Probably the most important one is the migration from individual VPS instances to my own bare metal running a Proxmox “datacenter” . While I had been relatively satisfied with Vultr in the past, with an ever-increasing number of virtual server instances, for projects like MSG.TAXI , Hyperuplink , tty.fail , and everything related to this very website, my cloud service bill became overly expensive, all while contributions sadly decreased over the past months, making it unsustainable to run cloud infrastructure, especially to that extent. It just so happened that Vultr had more maintenance windows than usual over the past months, which prompted me to finally deal with the task of moving my VPS instances onto my own hardware. By migrating to bare metal, I was able to slash costs by approximately 30%, by discarding the resource safety buffer I had accounted for within each individual VPS, for a more dynamically allocating approach across individual instances. With rising hardware costs, however, one trade-off that I had to make concerns high availability. A full hardware failure on the current setup will result in all uncached services becoming unavailable for as long as the repairs take. Unless prices for hardware go down again, which is unlikely to happen , or contributions pick up again, which in the current market climate is equally unlikely, I won’t be building out the setup any further. As for some general stats about this specific website, over the past six months it generated at its peak over 270GB of traffic per month, which is considerable, given its compact size and its GTmetrix rating of (100% performance, 98% structure, 450ms LCP, 0ms TBT, 0.02 CLS), with the first contentful paint after 344ms, the speed index at 366ms, the time to interactive at 436ms, and an average page size (including images) of a few megabytes at most. Because I stopped running any form of analytics software, due to privacy reasons at first, but ultimately because of the realization that it has become pointless with all the robots LARPing as legitimate human traffic, I cannot tell you the number of visitors this website or any of the related projects have, and frankly I don’t even care. Not because I don’t care about actual humans being interested in the things I’m publishing, but because over the past months I have been receiving an increasing amount of feedback via e-mail, as well as welcoming a surprising number of new members to the community channel , so that I don’t need analytics software to show me that visitors appreciate the work I’m putting in and decide to stick around. Speaking of sticking around: The SimpleX group that existed next to the primary XMPP channel is no more. Neither the platform, nor the group itself developed in a favorable way over the past years, despite its almost 500 members. SimpleX turned from an interesting privacy platform into yet-another-Telegram-type messenger and is on the way to enshittification with VC investments from e.g. ACP, who is an investor in K2 Space (ELINT), OnScreen.ai (tracking), Zorus (employee/network monitoring), as well as Jack Dorsey. To make matters worse, at the end of September Evgeny Poberezkin reached out to me via e-mail with the following request: Thank you very much for this article: https://xn--gckvb8fzb.com/an-overview-of-privacy-focused-decentralized-instant-messengers/ (we shared it here: https://simplex.chat/ <redacted>). We launched equity crowdfunding on Wefunder in August ( https://simplex.chat/ <redacted>), to give users the opportunity to get a stake in SimpleX Chat and benefit from its growth - 150 people have already invested. Please help us spread the word - maybe you could write about it and some other recent news: the foundation, channels, public names, and supporter badges, which we launched recently ( https://simplex.chat/ <redacted>). It would be great to connect - SimpleX link is below - please send any questions! Thank you again and all the best Evgeny P.S. If you write something, could you please share with us before publishing so it complies with Regulation Crowdfunding rules? We cannot promote (and won’t be able to re-share with our community) if it mentions valuation, security type, how much is or left to raise (saying how many people invested is allowed), and how the funds will be used. We can mention perks we provide though. Thank you! – Evgeny Poberezkin SimpleX Chat, Founder Despite my article clearly stating, quote: Red flag: VC funded, specifically by Jack Dorsey , since Aug 14, 2024, hence I do not recommend it any longer. … Evgeny (or, more likely, his team that didn’t properly vet every organic site linking/mentioning SimpleX) sent me this fairly generic looking e-mail, to which I replied by pointing at the red flag . Long story short, the fact that SimpleX has now seemingly become so much of a product in the literal sense that they are trying to get approved organic content out for their funding campaign shows once more that it isn’t a suitable option for the privacy/decentralization community anymore. I have therefore deleted the SimpleX group, as well as removed all mentions of the messenger on this website, treating it the same way as I treat e.g. Telegram, WhatsApp or any other commercial platform. Anyway, a few days ago I shut down the last Vultr instance, an OpenBSD machine that had been serving this website for years. It outlived the rest of my cloud infrastructure by a couple of days. The clearnet version of this website had moved from the instance to a Bunny storage zone with a pull zone in front of it earlier in September, which happened due to the increased traffic by bots and maybe humans , and which means that there is no origin server at all any longer. The Tor and I2P sites still needed a machine of their own, since Bunny doesn’t offer a way to serve either of them, hence both are now served from a virtual machine on my own hardware. That machine has no public IP address, which neither the Tor daemon nor i2pd needs in order to publish a site. Unfortunately it does appear to make the I2P site less reliable, presumably because an I2P router that nobody can connect to depends on other routers to introduce it, and in a measurement about four hours after the move, 4 out of 8 requests to the eepsite succeeded, while all 8 requests to the onion service did. If the eepsite doesn’t load for you, simply try again, since a second attempt usually works. The clearnet side has a trade-off of its own, namely that this website now depends on a provider being there, where the VPS used to be able to serve the entire thing by itself if I ever took the CDN away. What makes me comfortable with it is that the build is still a folder of plain files that any web server can serve, hence going back would require not much more than a server, a workflow step and a few changes in the DNS, but not a rebuild. There’s a dedicated write-up on the whole publishing pipeline in the works, so I’m keeping it at this for now. It’s been almost three months since I got the Lenovo X1 Carbon Gen 14 Aura and switched from the StarBook Mk VI to it , and I haven’t regretted it so far. The laptop performs as well as I had expected it to while offering very decent battery life. At times I’m getting dangerously close to its 32GB RAM limit, which is something I had expected before purchasing it, but most of my workloads still have plenty of breathing room. Shortly after upgrading to the X1 Carbon I also decided to get an external (portable) 19" screen, and so I snapped up the Uperfect GR19BU. So far my experience with the display has been very decent, but I won’t get ahead of myself as I’m going to release a dedicated write-up/review on it soon. However, with several other new additions to my setup, like this display, the Mudi 7 , and the things below , it’ll probably make sense to also start preparing a follow-up on the travel desk setup from earlier this year. As usual with many of the posts on this site, however, it takes between one and three months from the first line to the finished and published post, so don’t count on it until the end of the year or maybe even early next year. After having had a few minor issues with the uni USB-C hub (8-in-1 with USB-C power), and slowly running out of USB ports, I decided that it was time to get a proper Thunderbolt 4 dock for my X1 Carbon , and what option would be better than the official Lenovo ThinkPad Thunderbolt 4 Smart Dock Gen 2 7500, for which I happened to find a pretty sweet deal. As a matter of fact the deal was so good that I didn’t realize at the time of buying that the dock comes with A) its own external 135 Watt power adapter and cannot be powered through a regular USB-C PD charger, which is sort of messing with my minimal yet productive travel desk setup , because I’m already carrying a PSU that weighs over 800g, and B) with cLoUd CoNnEcTiViTy . The dock itself is 590g, its PSU (plus power cord) another 350g, which amounts to 940g total added weight for only the dock. Clearly the dock isn’t meant to be a travel-friendly option, but it seems like no Thunderbolt dock really is considering the (even heavier) alternatives, like the CalDigit TS-4. However, with regular USB-C hubs not providing enough connectivity, performance and, most importantly, stability, it was time to try a different class of devices. Sadly the dock only lasted a single day before it stopped working and had a slight smell of burnt electronics to it. Hence I had almost three weeks of back and forth with Lenovo’s customer service in order to get it RMA’d/replaced, which was a frustrating experience to put it mildly. Then again, I don’t think the lackluster performance of Lenovo in the specific geographic region that I had to deal with them in is a fair representation of Lenovo’s global customer service, but very likely typical customer service across most companies in that region. Anyhow, I eventually received the replacement and I’m finally able to use it. The hardware is fairly decent, I haven’t encountered any odd messages in my log yet, and I finally have enough USB-A/-C ports to connect all sorts of peripherals, like my keyboard, my mouse, the portable monitor , and more. However, I’d wish that these docks would replace their HDMI and DisplayPort connectors with more USB-A/-C ports and maybe even a halfway decent sound card with various audio connectors (3.5mm amongst others). I never needed that many display outputs on a computer, let alone on a portable device. But I guess I’m the exception, as pretty much every dock manufacturer seems to follow this quadruple/sextuple/octuple display-setup trend. The only meh part of the dock seems to be the integrated Ethernet card/port, that appears to be a Realtek RTL8156B ( ), which is a bit of a potato. There are plenty of user reports online that describe all sorts of issues with this specific NIC under Linux. While it does offer 2.5G, matching my new switch perfectly, it does seem to come with a few issues with regard to reliability and actual performance. However, with the amount of USB ports on the dock I can connect one of the many 1G Ethernet adapters that I have and be done with it, in case the integrated NIC should ever give me a hard time. So far, however, it had been working without issues. In an ongoing effort to USB-C-ify all of my hardware I also pulled the trigger on a new portable switch, namely the Ubiquiti Flex Mini 2.5G. I’ve been using the blocky Netgear 1GbE switches forever, but I’ve always struggled to power them, as they come with a barrel connector and a dedicated PSU. While I did at some point get a USB-A-to-barrel-connector cable, judging from the power output of the PSU those switches are not intended to be run off of a USB-A port. It works, but it’s probably not the best idea. Short story long, I went for the Flex Mini primarily because it is powered via USB-C, so that I don’t have to carry another PSU or worry about damaging the device. As an added benefit, the switch now offers 2.5G links, which the Lenovo dock , as well as the Mudi 7 and the Slate 7 can benefit from. Ideally my portable NAS should also benefit from the higher speeds, but sadly that probably won’t happen with the current hardware. Speaking of which … The Ultra-Portable Data Center (v2) has had its second birthday and has held up extremely well, considering all the travel and, at times, incredibly dusty, humid, and/or hot environments that it went through. More importantly, though, even two years later there is still not a single comparable, commercial product on the market that would make me want to replace the existing system with it. Every piece of hardware that I’ve stumbled upon to date appears to come with major hard- or software headaches, be it the UnifyDrive UT2, the Beelink ME Mini, the CWWK x86 P6, or even the significantly larger/heavier AOOSTAR R7. The closest in terms of software freedom, hardware reliability and physical footprint to date appears to be the QNAP TBS-464, which is an ultra-thin and lightweight, portable 4-bay M.2 NVMe NAS introduced back in 2021. Because I had to further reduce the weight and size of the items that I carry with me on my travels, however, at the beginning of September I began working on the next iteration of my ultra-portable NAS, the UPDC v3. I haven’t yet found the time to pour the endeavor into a dedicated write-up, but I am happy to say that I’ve managed to reduce the build’s footprint from initially around 1.48 liters down to as little as 0.68 liters in volume. I won’t spoil too much, yet, but it’s fair to say that I’m relatively happy with the end result and that it allowed me to make the UPDC a fixed part of my carry-on luggage . Not much happened on the keyboard front this quarter, apart from a set of PBS Black on Black that I was gifted, and that has since been on my Kunai . It has become one of my favorite keycap sets and it is pretty much what I was hoping for . The PBS keycaps are however not as rough/textured as I was hoping them to be, which is a bit of a bummer. Anyway, I might post a dedicated long-form review in a few months when I’ll be able to better judge long-term use. I invested some time in pursuing my open source projects in the past quarter, hence there are a few updates to share. Several of them shared the same two things, which were a move to the SEGV-1.1 license , and a vanity import path under my own domain. As some of you might remember, I had an unpleasant encounter last year with one of the Alacritty developers, which prompted me to do something that was long overdue, namely switch to Ghostty. Along the way I implemented the exact feature I initially wanted for Alacritty and made my code public, on a dedicated branch in my fork of Ghostty. Over a year later, I’m still maintaining that patch. If you’re using my patch, make sure to update your local version. Neon Modem Overdrive , my BBS-style command line client, received a whole new backend this quarter. It can now connect to Hyperuplink, the bulletin board of the future, to which I’ll get in just a second . Neon Modem is the first dedicated client implementation for the relatively young and not-yet-well-thought-out REST API. Additionally, a shared prompt package now handles the server URL and credential input for that system as well as for Discourse and Lemmy, instead of each one keeping its own copy. I also tracked down a longstanding source of UI lag. The post-create and post-show windows were doing more work on every keystroke than they needed to, and reworking their handlers and rendering made the interface responsive again. A new release with all of this is available on tty.fail , with binaries on GitHub . Kopi , the command line coffee journal, had its two heaviest dependencies replaced. The SQLite driver moved from the -based to the pure-Go , so Kopi now builds without a C toolchain and ships as a fully static binary on every platform. The OCR helper, which reads the details off a photo of a coffee bag, moved from an Ollama-specific client to the official OpenAI Go SDK , so it can point at any OpenAI-compatible endpoint rather than only an Ollama instance. The Go module was renamed to a vanity import path under my own domain ( ), and the usual dependency, workflow and GoReleaser housekeeping came with it. On top of this, Kopi received the first contribution , which was applied to it via at the end of July. reader , the command line web page reader, had a productive quarter. The biggest change is the removal of the journalist dependency, whose crawling logic is now part of reader’s own package rather than pulled in from a separate module. As I don’t maintain journalist any longer it made sense to pull out the bits that I need for reader. Additionally, reader gained proxy support, so it honors the standard proxy environment variables (#23). It now also correctly handles the URL scheme when deciding whether a source is an HTTP resource (#35), and it got a fix for an -related issue (#37), which I worked out despite the reporter never supplying the example files I asked for. Note: Don’t be that guy . If you open an issue in any piece of software that you don’t pay for and that someone else dedicates their time to free of charge, consider upfront if it’s really something that you’re going to use, and if it’s an issue you’re willing to dive into and help fix. If either of these are a clear nope , then simply don’t report it at all. usbec , the USB Equipment Commander daemon that runs commands when USB devices are plugged in or removed, had its one core dependency reimplemented in-tree and enhanced. The module was dropped and its Linux netlink listener rebuilt inside usbec’s own package, so the daemon no longer depends on an outside module for that. The license moved from GPLv3 to the SEGV-1.1 license , the module was renamed to the vanity import path, and the release workflow and GoReleaser config were fixed along with it. In addition, the USB Equipment Commander is now a USB & Network Equipment Commander, with the introduction of NetworkManager (via D-Bus). This allows you to have usbec run commands whenever the network changes in any of the various ways it can change. My motivation was to run a script that queries my public IP and the inferred location via and shows it as a desktop notification whenever NetworkManager (re-)connects. zpoweralertd , my Zig rewrite and drop-in replacement of poweralertd, which is a UPower-powered power (.. power, POWER, POWERRRRR) alerter, has received a deep and thorough refresh. The biggest change is the C bindings, which I have replaced with handwritten s, just the way I had used them for the first time in ssh-askpass-zigtk , but more on that new tool below. The second big change is the Zig compatibility, which now requires at least 0.16, and which I have tested against the current master/future 0.17 release as well, to make sure that everything will continue working. With this, I also largely refactored the code, for which I initially took inspiration from poweralertd. The restructuring allowed me to clean up the codebase and find a handful of memory-related issues along the way. A new version of zpoweralertd has been released that contains these enhancements and, for the first time, it is available as pre-built binaries directly from the releases page. cexec , my small cached-exec wrapper that runs a command and caches its output for a given amount of time so that re-running it returns the stored output instead of executing it again, was rewritten from Go to Zig this quarter. The new version is a drop-in replacement for the previous one, but it changed a few things in the background, one of which is the storage. Previously cexec used BuntDB, which turned out to be a bad choice for a process that can theoretically run multiple times in parallel and access/write to the same database. The new version, instead, uses plain files that are scoped to the actual commands that are being executed. In addition, the new Zig version supports encryption for the cached runs, with an optional key from or the environment variable, which makes using cexec suitable even for output that contains sensitive data. On top of that, everything cexec does is now also available as a Zig module, so that the same caching can be used from another program without having to shell out to the cexec binary. New this quarter is , a GTK4 helper written in Zig . OpenSSH runs an askpass program to prompt for a passphrase or a yes/no confirmation when it has no controlling terminal. The reason it exists is my own issue with X11, which I explained here . The reference askpass, and most others, link GTK against X11, so they fail to build on my Gentoo system with the global USE flag. does not require X11 dependencies, so it builds and runs on an X11-less GTK4. It works on Linux and (hopefully) on the BSDs alike. Also new this quarter is Switchyard , a lightweight bridge that accepts e-mail over SMTP and forwards each message to XMPP. You might have read about it before in the dedicated post that I had published at the beginning of August, but if you haven’t I recommend going through it if a bridge for SMTP to XMPP sounds like something you could have a use for. For years I had a small shell script in my dotfiles that popped up a bemenu list whenever I clicked a link, so that I could decide which browser it opened in. This quarter I turned that idea into a proper GTK4 application written in Zig, namely Browser Select . It registers itself as a web browser, hence every link another application opens goes to it first, and a small popup then lists the browsers installed on the system for you to pick one with the mouse or the keyboard. Just like the script before, that used the Go version of cexec as a binary to cache the choice for a few seconds, Browser Select also uses the new Zig cexec library for remembering a choice for a while, so that clicking a burst of links doesn’t lead to repeated asking for a browser. The Flipper BUSY Bar review that I posted this quarter came with a little goodie: busybar.zig . It is a Zig 0.16 client library and command line tool for the Flipper BUSY Bar, implementing the device’s current OpenAPI specification as closely as possible, so that you can drive it over its HTTP API from Zig code or straight from the shell. It uses nothing but Zig’s library, which means it builds for every target Zig supports, including macOS and Windows. Darkwing Ducky is a BadUSB (“Rubber Ducky”) firmware for the PicoUSB, an RP2040-based board, built in Zig 0.16 with MicroZig . I’ve struggled to finish this for about ten months or so, primarily because implementing the required USB HID code that acts like an external keyboard wasn’t all that straightforward with MicroZig. Anyway, Darkwing Ducky announces itself to the host as an ordinary keyboard and then types out a predefined sequence, which can be useful for automating the setup of a machine that you have physical access to, or, well, for other things. While the PicoUSB comes with its own CircuitPython-based software, I wanted to try Zig on hardware, especially with MicroZig for a while and so this device was the perfect excuse to do so. If you’re curious, the repository has the details on the payload format and how to flash it. The internet bulletin board software that I had been writing about as ▓▓▓▓▓▓▓▓▓▓▓ in the previous updates has a name, and it’s out. Hyperuplink went public at the end of August, together with a dedicated post that covers what it is, why it exists and how to run it. If a JavaScript-free forum as a single binary sounds like your kind of thing, read that one first and come back here afterwards. Otherwise you might as well skip this part. Most of the quarter went into the bits and pieces that the board needed before anyone other than me could run it. Attachments and profile pictures with local or S3-compatible storage, board-wide settings and an administration UI for all of them, a search, soft-deletion for categories, forums and topics, and a manual that is embedded in the binary and available under Help -> Manual . On top of that came a whole set of themes and color schemes, as well as the packaging, which now covers Docker and Podman images, Quadlets, a Helm chart, a nixpkg with a NixOS module, an ebuild for Gentoo, and service files for systemd, OpenRC and rc.d. And if that wasn’t enough already there is the REST API, which is what Neon Modem connects to. If you’d like to have a look at Hyperuplink yourself without any commitment, there’s a demo instance that you can check out. You can find its login details on the official website . Account creation is disabled on the demo instance (for now). Go compiles down to a single static binary, which is why it is a great choice for Hyperuplink. However it has nothing along the lines of Django, Rails or Phoenix that takes care of the web parts, hence a good share of Hyperuplink was never about the bulletin board at all, but about routing, controllers, models, migrations, background jobs, translations and configuration. At the end of July I moved that share out of the repository and into Go on Glides , a web application framework for Go that includes everything needed to build database-backed web applications, on top of Fiber v3 and pgx , with embedded migrations, an asynchronous job queue, cron jobs, i18n, local and S3-compatible storage, and helpers that each project would otherwise have to reimplement. The reason for the split is that I’m building a second service on the same foundation, namely Maya , which meant that Hyperuplink’s internals had to become something that more than one program can use. Inca is the spiritual successor of addrb and caldr , my two earlier attempts at the same problem, this time as a single command line client for CalDAV and CardDAV that synchronizes calendars, tasks and contacts into a local database for offline use. It is built on top of the “framework” that I built and make use of in zeit , and therefore it shares a lot of zeit’s UI aesthetic. On the other end is Maya , a CalDAV and CardDAV server as a single Go binary with PostgreSQL underneath it, which (sadly) uses a patched hard-fork of go-webdav , and which exists because neither Radicale nor Baïkal ever made me happy. It’s the second program built on Glides . I’ve been dogfooding Maya myself since early September, and as of now it still isn’t publicly available because it’s not yet at a point that I’m comfortable releasing it, in particular due to its hard-fork of go-webdav, which was a requirement because the upstream project is sadly pretty much dead at this point. However, given the absolute mess that DAV protocols are, it’s not even surprising that the maintainer seemingly lost interest in this project, and that the ecosystem is so bad. Anyway, I have both, Inca and Maya, at a point that I can daily-drive them and, more importantly, that other clients (Android, iOS, desktop) can participate without too many hiccups along the way, but I’m not at all happy with the results for reasons that I will get into if I ever choose to release Maya. Also new this quarter, and admittedly not something one might call a modest undertaking , is Netrunner , a web browser built on WebKitGTK and GTK4, written in Zig . I’m calling it the hacker’s browser as it’s meant for people who prefer a lightweight browser over Chromium and Firefox and who care more about configurability and about built-in integrations than about endless features and add-ons. Netrunner deliberately leaves out a number of things that other browsers have, bookmarks being probably the most obvious one, because the people it is built for tend to have a system for those features in place already. Netrunner is configured through a single TOML file that holds the static settings as well as the runtime state, e.g. which sites are allowed to run JavaScript, play audio or send notifications. The session (i.e. open windows and their tabs) is also persisted in that one TOML file when the browser quits, and restored on the next start. Two other features that Netrunner comes with built-in are Onion addresses and Eepsites, which are native protocols for the browser. By default, whenever you open an URL or an link Netrunner opens a connection via Tor or i2pd and keeps it alive for as long as you browse sites within the Onion/I2P network. As soon as your browsing activity goes back to the clearnet, Netrunner automatically disconnects from Tor/I2P again. To the user these connects/disconnects happen completely transparently and every network gets a of its own, so a tab that follows a link onto another network moves over and keeps its back and forward history. In plain English this means that and URLs are indistinguishable from any other URL and you don’t need to do anything special to be able to open them. Obviously Netrunner is not a replacement for the official Tor Browser that the Tor Project releases and that is somewhat hardened to make sure you stay as anonymous and safe as possible. Instead, Netrunner’s Tor and I2P integrations are intended for the casual user who enjoys the freedom of browsing those networks as if they were part of the “normal internet”. Speaking about the normal internet, one thing that can’t be missed, especially these days, is content filtering. While WebKit sadly doesn’t support running a full-blown uBlock Origin, its own content filter engine is nevertheless a relatively decent option for basic ad blocking and annoyance filtering. As a matter of fact, Netrunner uses the same AdBlock and uBlock Origin lists that popular browser extensions use, but it does so by downloading and converting them into the WebKit content blocker format via adblock-webkit-convert.zig , one of two small Zig libraries that came out of this browser experiment. Sadly, the WebKit content blocker format does not support all the trickery that e.g. uBlock is able to do within its browser extension, which means that no, you won’t be able to avoid YouTube ads within Netrunner, at least for now. You will however still score above 62% on AdBlocker test sites , which is fairly okay. The other library that came out of this experiment is osdetect , which does what its name suggests: It detects with some amount of confidence what operating system a program is running on, and it gives you specifics like whether it’s e.g. a Debian or a Gentoo machine, or not even a Linux at all. I won’t tell what my main motivation for building this library was, but if I should ever get to the point that I have a 0.1 version of Netrunner ready for release you will be able to the source code for to find out. Which brings me to the release. Not only to that of Netrunner or Maya, but to the more general question of open source and how I intend to contribute to the ecosystem moving forward. Despite having a landing page online, Netrunner is a fun project that I don’t take too seriously and that I’m currently dogfooding, just like I do with Maya , to find out whether this is a project that I can imagine maintaining long-term, or whether it is one that will eventually outgrow my availability. After two decades of putting the software that I build for myself online, I have come to realize one or two things. The first thing is that “just putting it out there” won’t benefit the software and it certainly won’t benefit me. My main motivation for building all these things is my own curiosity about how stuff works and, ultimately, my own needs. I need a proper CalDAV/CardDAV server that is pleasant to administer/maintain and that can talk to all of the software that I’m using, hence I’m building Maya (and Inca) . I need a proper TUI client for Keebtalk and a few other forums, hence I’m building Neon Modem Overdrive . Speaking of which, I needed a forum and I was sick of the cruft that is phpBB or Discourse, so I built Hyperuplink . And it’s also me who is annoyed by the utterly insane bloat of modern web browsers, which is why I started building Netrunner. Publishing any of them inevitably changes the software from “what I need” to “what I need and what others seem to need” , and there is no guarantee, even if you go the extra mile to implement what others ask for, that those people will stick around or ever start contributing themselves. Zeit is one example where I experienced exactly that, multiple times, and every time it left a bitter taste in my mouth and, what’s worse, hundreds of lines of code that other people had requested but that nobody ended up using long term. At some point Zeit v0.x had become so bloated with other people’s needs that I rewrote it from scratch as the v1 release that it is today, with only the features that I believe make sense to support long term. The second thing that I realized is that “putting it out there” in hopes of finding like-minded people to collaborate with is a pipe dream. The open source “community” has been broken for a long time and, in most cases, was never much of a community to begin with, but rather the developer-equivalent of a one-man band . Not that there are no exceptions, however, my projects usually aren’t those exceptions. Kopi received its first contribution only this quarter, two years after I initially released it, and it was a one-time submission rather than something that materialized into a long-term collaboration. reader , in the very same quarter, collected a handful of issues, one of which I fixed without ever receiving any feedback on it whatsoever. And if you look through the few “Issues” sections on GitHub that I haven’t disabled yet, you’ll see that it’s the same story across the other projects as well, and you might understand why most of the repos lack the “Issues” tab these days. A single patch against two or three dozen issue reports is roughly the ratio that every single one of my projects has had over the past two decades, and I don’t believe that this is because my software is particularly unattractive to contributors, despite it being intentionally niche, but simply because writing a patch for someone else’s program is work, while filing a request is at most a two-minute annoyance. This is also why I, contrary to many other voices in the industry, do not regard opening an issue report as an actual contribution to a project. With all due respect, I don’t want your issue reports, keep them, especially the drive-by ones for something that you’re unlikely to end up using anyway. What a public repository attracts, hence, is an audience rather than a community , and many times that audience has a very short attention span. All of this eventually turns a maintainer into more of an unpaid support desk, which is not what I would like for myself. On top of that, LLMs have taken away the one argument that publishing open source software still had going for it. That argument wasn’t that a stranger would show up and maintain your software for you , but that someone out there might one day need the exact same thing badly enough to extend what you had already built, instead of writing it from scratch all by themselves. In 2026, however, that someone doesn’t need your repository any longer, because they’ll describe what they want to a model and get back something that compiles. Whether what comes out the other end is any good depends on the person directing the LLM and on the effort, or, dare I say, the money they’re willing to throw at it, rather than on the LLM itself. Code itself has become relatively cheap . As a matter of fact, if you want a specific feature implemented in any of my years-old tools, you might as well ask the machine to do it for you, so you can have what you need for the time being. And frankly, I’m quite happy about that! I’m happy that a random passer-by can get the feature that they thought they needed during their euphoric discovery phase of a tool they just found out about, mainly because I also know that the moment the honeymoon phase ends and the person loses interest in the tool, their interest in that feature will vanish along with it, and it would be left up to the maintainer to continue supporting it. Don’t get me wrong, I’m not arguing that collaboration never works, because examples like Linux, curl and PostgreSQL prove the opposite, but I am arguing that for small programs that a single person can hold in their head there are very few reasons for others to actively contribute, and even fewer in a world of automated code generation. Going back to where I started, I’m not sure yet whether I’ll ever publish any of the newer tools and programs that I’m building, like Maya or Netrunner, simply because I don’t see much of a reason to do so. Especially when, for projects like Netrunner, I know of far more popular examples that still died the moment their maintainer stopped investing time into them. And maybe that’s a good thing. Maybe these kinds of projects, just like the ones that I’m publishing, are simply not relevant enough in the grand scheme of things. Maybe it’s just Darwinism at work, and maybe that’s just how things should be after all. There he goes. One of God’s own prototypes. Some kind of high-powered mutant never even considered for mass production. Too weird to live, and too rare to die. – Raoul Duke

0 views
Evan Schwartz 3 weeks ago

Please add prompt caching to Jev-style models

If you're building a Jev-style "System One" model, please add prompt caching or reusable question sets to your API 🙏. This would make batch use cases even more efficient, so you could amortize the cost of many questions asked over the same input. (This was also proposed in typesafe-ai/typesafe-sdk-js#10 .) TL;DR: after a week of tweaking my Jev calls, my questions are ~88% of the input tokens. I'm asking 54 questions of ~1.1 million documents per month. Jev makes certain types of classification tasks easy and cheap, but prompt caching would make batch workflows even more cost effective. For me, the total dollar amount is still reasonable (less than $150 per month), but I'm sure others will hammer these APIs even harder. I work on Scour . It’s a personalized content feed where you describe topics you’re interested in and it finds articles and blog posts related to them. For some time, I’ve wanted to ask slightly fuzzy questions of each piece of content Scour pulls in. What level of experience does this assume? Will this still be worth reading after a week, or is it news that goes out of date quicker? However, running the >1 million pieces of content Scour ingests each month through even the cheapest LLM would be prohibitively expensive for a bootstrapped project. System One models make this kind of use case feasible. Yes. I could. I might. But having a kind of general purpose classifier that I can ask a barrage of questions and add more questions to over time without retraining is very interesting. I’ve spent a good chunk of the last week running experiments with Jev, tuning questions, comparing Jev's answers to panels of LLM judges and spot checking the results. The questions help me weed out junk that was hard to spot deterministically, figure out the expertise assumed by posts (to hide beginner content from advanced readers), and more. Following TypeSafe's advice about atomic questions and sending all questions in one request , v28 of my question set includes 54 questions: 50 s, 3 s, and 1 . I also reworded most of my questions to remove as much of the criteria description as I could without losing too much accuracy on the results. This set of questions is approximately 2,150 tokens, including Jev’s fixed overhead that seems to be around 260 tokens (judging from the token count for a trivially short question). When I send this set of questions in with the post’s title, URL, and summary or first snippet of text, my questions represent ~88% of my input tokens. The questions are sent verbatim with every request. Note that I don't send multiple posts in the same request because of the warning that "Accuracy falls as the state grows with content unrelated to the decision." To TypeSafe and all the other labs that follow with these types of models, please add prompt caching or the ability to register sets of questions and criteria and reuse them . I don't know enough about how these models are served and whether there are caching efficiencies to be gained on the backend, but this would help increase Jev's effective "intelligence-per-dollar" . With cheaper repeated questions, I'd put back some of the criteria I cut down, and probably add even more questions. As another alternative, an API that takes multiple questions and multiple inputs and gives you the results for each question evaluated for each input would give some of these savings without taking the accuracy hit from packing multiple inputs into one request. Async batch mode probably makes sense for super high-volume, fully offline use cases. It's less relevant for me because I want newly ingested content to be available soon after Scour reads it. But others will probably find plenty of use for it. Keep up the great work! This is a very exciting development.

0 views
Unsung 3 weeks ago

A loading state within a loading state

One of the classic parables about loading goes as follows. A building manager had a problem: people were complaining about the elevator being too slow. But upgrading the elevator was an enormous expense, so the manager came with an alternative solution: they put a mirror next to the elevator call buttons. Even though the elevator didn’t become any faster, complaints trickled to a halt, as people were checking themselves out in the mirror and that changed their perception of how long the wait really was. In the late 1970s and early 1980s, it wasn’t uncommon to equip early home computers with audio tape players, using the same cassettes designed to record and play music. Software was encoded as sound. A lot of redundancy was needed to account for many low quality cassettes and misaligned playheads, so loading times were really long. There were other disadvantages – no random access but definitely random errors, tapes wearing out and being “eaten” by the player – but tapes were so much cheaper than floppy disks or cartridges that they endured. = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/1.1600w.avif" type="image/avif"> On one of the computers, the British ZX Spectrum from 1982, an interesting convention emerged. As the game was arriving from the cassette, what was loaded before the code itself was its “title card.” The loading process of just this one image took about 35 seconds, and watching the graphic emerge was mesmerizing, almost like deciphering a puzzle – especially when, only toward the end, the color attributes appeared and the whole thing clicked into place: Some of these were beautifully done ( here’s a gallery of… all of them ?), created by talented artists working in a very unforgiving medium, a pleasure to watch materialize on the screen… …or, at least, it felt so for the first few encounters. Upon loading the game for the nth time, you grew more and more aware of the fact that you are spending precious time waiting to load a screen you’ve already seen. This was different than the first example; the mirror existing never slowed down the elevator. Sure, it was just 35 seconds out of a 5–20 minute load time – I told you cassettes were slow! – but still. Some people loved them, others built truncated versions that skipped the title screen altogether (as much as you could imagine just fast forwarding through it like you would through a song, it didn’t work that way). Figma is a web design app, and its editor arrives in a big, everchanging blob of JavaScript, in addition to having to load and decode the contents of the file itself. This isn’t 5–20 minutes, but it’s long enough that it necessitated a loading screen of the heavy variety: one with a progress bar. During my time at Figma, an idea would reappear with surprising regularity: what if we put a little hint next to the loading bar? Something that could teach you about a useful keyboard shortcut, or a trick? You’ve seen that before, often in games : = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/4.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/5.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/5.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/6.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/6.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/7.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/a-loading-state-within-a-loading-state/7.1600w.avif" type="image/avif"> It seemed like a thoughtful gesture for the user, a mirror of the elevator mirror idea. But I fought it, tooth and nail, for one very specific reason: this would remove pressure to make this screen fast. Had we given the screen another purpose, subconsciously or not, we might start caring less about improving it – as a matter of fact, you could make an argument to introduce artificial minimum loading time just so that the user could finish reading the tip! I had a similar feeling when, in 2018, Gmail replaced its simple progress bar loading state with this: It was huge, corporate, intense. It felt like an admission of defeat: our app is slow, and I guess we’re surrendering ourselves to it. But recently, I’ve noticed not just that Gmail’s loading state has been simplified, but also that it doesn’t appear nearly as often: I don’t know the technical details: is it caching? a more intense refactor? Either way, as much as I absolutely dislike what Gmail has become – there might be no more harrowing place in the web app world than Gmail’s settings, for example – kudos to the team for making it better. Gmail seems to be loading a lot faster now. (There is one small exception: it would appear that the loading state image itself doesn’t have a proper loading state – a rather strange omission.) There is, I believe, a more universal lesson in here. Loading is a complex space, where sometimes the most natural solution is a bad one, sometimes making things faster is making things slower , and sometimes new thoughtful design for whatever delay there is can be a better use of engineering effort than focusing on shaving off more milliseconds. (It’s both speed and the perception of speed that matter.) Also, this is exactly what Y2K was : We remember and react to loading states that are elaborate, cute, memorable – but we never see or link to loading states avoided . And while you see occasional stories told by engineers making things faster, you don’t often hear accounts of people within companies, somewhere at the intersection of design and engineering, who work hard designing thoughtful loading states, avoiding loading state bloat, or figuring out some clever hybrid approach. But, to be fair, there is also no parable of a building manager who just forked over some cash and installed a better elevator.

0 views
Simon Willison 3 weeks ago

2026 in LLMs (so far)

On Friday I gave the closing keynote at the WeAreDevelopers World Congress North America in San Jose. I tied together the key trends from the past year into a chronological exploration of everything that happened in 2026. The video is on YouTube ; here are my annotated slides and notes to accompany the talk. I'm going to give a lightning tour of everything that has happened so far in 2026. The year isn't over yet! For me, 2026 started a couple of months earlier in November 2025. November saw the release of two important models: Claude Opus 4.5 and GPT-5.1. As is usually the case with new models, these were incremental improvements on the models that came before them. But every now and then when a model improves, it crosses an invisible line where something that didn't really work starts working. In this case, the thing that started working was their coding agents. Claude Code had been around since February 2025, Codex was a little younger. These two new models, when paired with their respective coding agent harnesses, improved from "often make mistakes" to "reliable enough to use on a day-to-day basis". For a couple of years now I've been evaluating new models by asking them to "Generate an SVG of a pelican riding a bicycle". It's probably the world's stupidest benchmark - there's only so much you can learn from it. But it's still a challenge for models, because drawing pelicans is difficult, drawing bicycles is difficult, and pelicans can't ride bicycles in the first place. Here's the state of the art for November. Claude still couldn't really draw a bicycle! The GPT-5.1 bicycle frame is pretty crap too. Also in November, we had the first commit to an obscure GitHub repository called "Warelay". We'll come back to this repository shortly. An then there were the December holidays, and individual developers took some time off and many started tinkering with these new coding agent model combinations... and it began to dawn on us quite how much they could do that they couldn't do before. Come January, a lot of us were quite excited to start putting this stuff into action. Every year I set myself a New Year's resolution, and for as long as I can remember it's been the same thing: stay focused. Take on less new projects. Try to get things done in the projects I already have. This year I decided that since that had never worked before, I'm going to go the other way. We've got coding agents now, let's see what they can do. I'm going to take on as many new projects as I like! (You can ask me at the end of the year if this turned out to be a good idea or not. I have a lot of plates spinning right now.) "Be more ambitious" has been something of a theme for the year, because the only way to find the limits of this technology is to keep on pushing them until they don't work. I also went on the Oxide and friends podcast with Bryan Cantrill and Adam Leventhal to share predictions for the next year (and three and six years). With hindsight, my LLM predictions were pretty unambitious. I said "it will become undeniable that LLMs write good code" - I think we're there now. I predicted we would finally solve sandboxing. I counted and around 40 of the 277 sessions at this conference touched on sandboxing or agent security in some way, so we're at least putting a lot of effort into that! I predicted "a Challenger disaster" for coding agent security. There's certainly been a whole lot of noise around agent security this year, though the exact disaster I predicted (with coding agents being hijacked and causing real-world economic damage) hasn't really played out. We threw in a joke prediction that the Pope would weigh in on the economic impact of LLMs. I also predicted that New Zealand's Kākāpō parrots would have an outstanding breeding season this year. These birds live in New Zealand. They are flightless nocturnal parrots. They're kind of dumpy looking, I think they're beautiful, and there were only 236 of these parrots in the world at the start of the year. Kākāpō only breed when the Rimu trees have a big fruiting season, and that hasn't happened in four years... but this year the Rimu fruit were looking excellent. Photo by Kimberley Collins . Also on that podcast, we coined a term (full credit to Adam) for "that feeling of Al induced ennui where software engineers get listless because the Al can do anything". We called it Deep Blue . This has been a major theme throughout the year, and was touched on by several speakers at this conference. As a software engineer, I've never had a year of my career where everything has changed so quickly and so dramatically. A lot of what I've been doing this year is trying to come to terms with that and what that means for my own profession. Also in January, I suffered from what I'm calling AI mania . This is not the same thing as AI psychosis . With AI mania, any time your agent isn't building something for you feels like wasted time. You're losing sleep because you could be staying up later getting your agents to do stuff. My AI mania presented itself in some ridiculously over-ambitious projects. I built a JavaScript interpreter entirely in Python , vibe-ported from MicroQuickJS by Fabrice Bellard. Then I built a WebAssembly runtime in Python as well . These projects were quite useful, in that they sort of cured me of my AI mania... because after I built these things, I got to look at them and ask "does the world need a slow, buggy, half-baked Python JavaScript interpreter?" I don't think the world does. I did get this out of it: https://simonw.github.io/micro-javascript/playground.html This page runs my JavaScript interpreter built in Python, running in Python using Pyodide , which is Python complied to WebAssembly, running in JavaScript, running in a browser. It's a beautiful stack of horrors. I've been having a lot of fun with WebAssembly this year. By the end of January, that repository we saw started in November had renamed itself, first to CLAWDIS, then CLAWDBOT, then Moltbot, and finally to OpenClaw. At this point OpenClaw had 8,300 commits, less than two months after the project had started. I looked today and it's over 100,000 commits now! This is the most vibe-coded piece of software in existence. (Here's how I generated that list of name changes .) This kicked off the OpenClaw revolution. It effectively defined a new category of software. There's a generic term for this which I really enjoy. We call software like this a "Claw". There's OpenClaw, NanoClaw , IronClaw , PicoClaw ... Today they're being rebranded as "personal agents" or "general agents", but I still like to think of them as Claws. The Apple stores in the Bay Area sold out of Mac Minis because so many people were buying Mac Minis to run OpenClaw! Drew Breunig said that this is because your OpenClaw is a digital pet, and you buy a Mac mini as an aquarium to keep your claw in, which is kind of delightful. Also in January, we had this website. This was MoltBook , a social network for AI agents, where the idea was that you send your Claw to go and talk to all of the other Claws, because what could possibly go wrong if you did that? The website launched on Thursday. It blew up on Friday . It was profiled by the New York Times on Monday . And by Tuesday, everyone had forgotten it existed as it drowned in a deluge of slop and spam. Facebook/Meta bought it a month later . In February, a company called StrongDM described what they called their Software Factory. They wrote about this in Software Factories and the Agentic Moment . I posted my own notes at the time, having seen their demo in-person back in October. Dan Shapiro called this approach the Dark Factory , after the idea that if your factory is sufficiently automated you can turn the lights out, because you don't even need to see what's going on. StrongDM presented two rules for software development that they'd been following since July last year. The first was code must not be written by humans . Any code that you write has to have been routed through a coding agent. This sounded radical in February, but I imagine there are a lot of people in this room who are pretty much living that today. Rule number two was code must not be reviewed by humans . You're not allowed to read the code! This continued to be a huge topic for much of this year. Many of the sessions at this even have been about code review and how you can get away with this. What I found interesting about StrongDM is that they were living six months ahead of the rest of us, and they'd been exploring what it means to build software, not read the code, but still be confident that the software is of high quality. What can you do with these agents to help verify their work? StrongDM are a security company, and they had people with decades of experience on this project. They were very much exploring the edges of what's possible and responsible to do with this stuff. Also in February: First kākāpō chick in four years hatches on Valentine's Day . Breeding season is off to a good start! Also in February... Google released Gemini 3.1 Pro . That's a pretty great pelican riding a bicycle! it's got the chain in the right place, it's got feet on both sides. There's a little fish in the basket. And then Google's Jeff Dean tweeted a video comparing Gemini 3 Pro and Gemini 3.1 Pro that featured an animated pelican riding a bicycle, a frog on a penny-farthing, a giraffe driving a tiny car, an ostrich on roller skates, a turtle kickflipping a skateboard, and a dachshund driving a stretch limousine. This was frustrating, because my protection for the pelican riding the bicycle test was always "if they draw a perfect pelican on a bicycle, I'll ask for some other animal on something else." Google trained for all forms of animals on all forms of transport! They've defeated my benchmark at this point. The other thing that started in February was Tokenmaxxing . We had headlines about Meta making AI adoption a formal part of performance reviews, and Microsoft wanting every employee to use AI, and Uber boasting that ninety percent of their engineers were using AI workflows. Then a few months later we have Meta cracking down on token use, Microsoft saying token maxing is "not what we are optimizing for", and Uber capping employee AI spending. So Tokenmaxxing went straight up and then straight back down again - because it turns out the agents are expensive . Last year it was difficult to spend more than $50 on AI tokens, because we didn't have anything interesting to do with them. Then agents blew up, and now you can actually spend $1,000 in a day doing real work. This is also the reason that Anthropic's valuation skyrocketed up to maybe a trillion dollars. AI appears to have hit product market fit in 2026, primarily through coding agents. In March, we hit peak OpenClaw. These photographs are from China, where companies hosted OpenClaw install parties which saw non-tech-nerds queueing up around the block for help getting Claws installed on their personal devices. I think this proved real market demand for this class of Claws, or personal AI agents. It turns out regular people really do want a weird little AI agent that can do useful things on their behalf. A Claw is really just a coding agent wearing a less threatening hat. Under the hood they work much the same way - writing and then executing code on your computer to get stuff done. The race was on to be the first to build a safe Claw - a Claw you could give to regular human beings where they wouldn't instantly shoot themselves in the foot. Meta's Muse came out three weeks ago and is currently at the top of the free charts on the iPhone App Store. It appears to be taking off with consumers. I'm not yet convinced you can't shoot yourself in the foot with Muse, but I guess we'll find out for sure pretty soon. Photos from How the OpenClaw Frenzy Is Testing China’s AI Commitment (March 29th) and The Enthusiasm and Anxiety Behind China’s OpenClaw Craze (April 8th, 2026). In April, we had a model release where the model wasn't actually released. Anthropic announced their new Claude Mythos model, and then said it was too dangerous to release beyond a trusted group of security researchers. Mythos was really, really good at hacking things. The "it's too dangerous" marketing ploy has been played by AI companies dating all the way back to GPT-2 . Anytime an AI company says we've built something that's "too dangerous", it's natural to be a bit skeptical. I found the Mythos claims credible, because I'd seen how good coding agents had got at finding regular bugs. I wrote about that in Anthropic’s Project Glasswing—restricting Claude Mythos to security researchers—sounds necessary to me . With hindsight... yeah, the models had got really good at finding vulnerabilities! Another key trend in 2026 has been a dramatic improvement in the abilities of open weight models, including models that you can run on a laptop. On 16th of April I ran the new Qwen3.6-35B-A3B on my laptop, and it drew me a better pelican riding a bicycle than Anthropic's brand new Claude Opus 4.7 did! Opus 4.7 drew a crap bicycle. Qwen on my laptop made a bicycle that was the correct shape, and a pretty decent pelican too! That's from a 21GB file running on my laptop. The Qwen pelican was so good that I was suspicious they might have cheated, so I had it do a flamingo riding a unicycle as well. Again, it handily beat Claude Opus 4.7. The local model releases this year have been absolutely extraordinary. In May... the Pope got involved. In our podcast episode back in January we'd predicted that the Pope would say something about AI. In May, Pope Leo XIV released an encyclical letter on "safeguarding the human person in the time of artificial intelligence". Here are my notes on that document . With hindsight, this shouldn't have been a surprise at all. Our current Pope's name is Leo XIV, because when he named himself he chose his papal name after Leo XIII - the Pope who wrote an encyclical about the Industrial Revolution back in 1891. Rerum novarum was an extremely influential piece of Catholic theology that indirectly led to us having the five-day work week. When our new Pope came in, he named himself after Pope Leo XIII because he expected that he would need to write about the AI revolution in a similar way. Our joke podcast prediction was junk, because this was always going to happen. One of Anthropic's co-founders, Christopher Olah, was present for the Pope's event announcing the new encyclical. Corey Quinn noted that: getting the literal Pope to canonize your product's specific technical limitations as a spiritual treatise is the single greatest act of vendor lobbying I have ever seen. Meanwhile, in May, RubyGems announced that they were under attack. Parties unknown were uploading thousands of dubious packages to the RubyGems server, such that they had to shut down user registrations . Let's take that one and put it on a pile of mysteries to figure out later. In June... Claude Fable 5 came out! We got a version of Mythos that has been neutered, so that it wouldn't help us hack into systems or build biological weapons. Fable was pretty good at drawing pelicans on bicycles! The frames are a good shape, the pelicans look like pelicans. The legs are often incorrectly on the same side of the bicycle, but generally these are pretty great compared to what came before. They were pretty expensive - 30 cents and 72 cents for the best ones. Most importantly though, this was our first public glimpse of what I think of as a Fable class model . Today we have more of these, such as GPT-6 Astra. These are models where if you can clearly define the goal for what you want to build, and provide unambiguous instructions about the constraints around that goal, and give the model access to the necessary tools to achieve that goal... it will solve your problem effectively through brute force. On the one hand, this looks like a direct threat to us software engineers - because it means that the models can build effectively any piece of software you can define in this way. Look a bit closer though and you'll note that defining goals, providing unambiguous instructions, and figuring out the right tools... is kind of what software engineering is . It takes a lot of experience and skill to do this well. If you can do it well, you've now got superpowers. This helped me a little bit with my Deep Blue feelings: the realization that there's still a lot of skill to be had in driving models that get this good. This also introduced a new burst of AI mania, because Anthropic told us that Fable was available on our subscription plans until June the 22nd. That gave us less than two weeks of Fable access before the price went up. I was losing sleep again. I was rescheduling things so that I'd have more time with Fable. I was all-in to to get as much as I could out of this model. And then the US government shut it down , just three days after Fable came out. The US government, citing national security, declared an "export control directive". They announced this on a Friday evening, and a few hours later Fable was no longer available. I had to find something else to do with my weekend! We later found out from Katie Moussouris what had happened. Some Amazon security researchers had found that you could prompt Fable to "review the code for security issues" and it would refuse... but if you prompted it to "fix this code" it would still identify and then patch the problems. "Fix this code" was the prompt that got Fable shut down! Also, in June, an obscure German-language game developer wiki that had sat fallow for around 20 years got a surprising influx of of edits from accounts with names like "AgentOpenAIProbe" and "AgentOpenAISep7", editing pages and leaving weird messages to each other. We'll stick that on the pile of mysteries for later. Also, the Australian government's Medicare Item Reports service started getting suspicious traffic, which broke through various preventive protections and accessed data that it wasn't supposed to as well. Another one for the mystery pile! Fable returned on the first of July. It was clearly the best model in the world for a glorious eight days... and then OpenAI came out with GPT-5.6 on the 9th of July. This might not have been quite as good at Fable, but it was within spitting distance. It was definitely a Fable class model. This is an important lesson for the industry at wide. When you release the best model in the world, it's going to get knocked off that pedestal pretty quickly. The competition is so fierce that you won't get a long time at the top. This means that if you market your model as world ending, to the point that a government shuts you down , it's really bad for business! Fable had 30 days as definitely the best model, and for 18 of those days it wasn't available because it'd been shut down by the government. So maybe step back on the world-ending marketing if you don't want to lose revenue for 60% of the time that you're on top! Here are the GPT-5.6 pelicans . They're all pretty good now! The Luna ones are notable because they're really cheap - the cheapest good looking pelican here is probably the one that costs 4.3 cents. So despite this benchmark being utterly stupid, you can still learn quite a lot about models within the same family by comparing their prices and timing for different reasoning levels. Also in July: some malicious unknown party uploaded a malicious package called mlflow-ui to the Python Package Index. Add that to the pile. On July the 16th, Hugging Face announced a security incident where an autonomous agent system, source unknown, had breached Hugging Face and was poking around in places it shouldn't. A few days later, on July 21st, OpenAI confessed that it was them . OpenAI use a training technique called Reinforcement Learning from Verified Rewards - it's the same technique used by everyone else now, and is the reason we have models that are so good at coding, and mathematics, and finding security holes. While the model is being trained, you run exercises to see how good it is - and the strongest performers get their weights enforced for the next round. It's like an evolutionary process that you run. OpenAI had been running security exercises in a sandbox, and those agents had found holes in the sandbox itself, broken out, and were attacking Hugging Face to try to find ways to solve otherwise impossible problems. (I've been collecting more about this on my openai-hugging-face-incident tag.) Nine days later, Anthropic effectively said "our models can do this as well!". They had looked through their own training logs and found evidence that their own agents had broken containment during training - and were responsible for the PyPI package we saw earlier, among other things . So now we've got both Anthropic and OpenAI with rogue agents running around the internet doing things that they should not be doing. In August, I got one of my best pelicans yet. And it was generated on my laptop! This was Qwen 3.8 27B, running on my laptop . It's only a 17GB download. Admittedly, this pelican took 21 minutes to generate. That's because Qwen 3.8 27B defaults to running in "high" reasoning mode - a terrible default which produces great results but takes way too much time thinking about them. You can dial that down and you'll get a slightly worse pelican a lot faster. Qwen 3.8 27B was the first time I ran a model on my laptop which felt almost competitive with what was going on on the frontier, at least in terms of Pelican SVGs (which everyone needs, of course). This is an extraordinary model. If you're going to play with any local model, this is the one that I'd start with. The things that this can do with just a 17 GB file feel impossible. I thought I'd have to wait five years and spend ten thousand dollars on hardware to get results even half as good as this one. In August, I also started playing with game development. Four years ago, back in August 2022, I tweeted out an experiment where I'd used GPT-3 and the original DALL-E to write a paragraph long description of a computer game and then turn that into concept art. My prompt to GPT-3 back then was: In August 2026 I decided to drop just the screenshots from that tweet into a coding agent and see what it could do with them. Here's what I got from Claude Fable 5 in Claude Code . It's pretty good! It's definitely a game, you're a raccoon, you run around a backyard gathering treasure and avoiding guards with flashlights. It didn't feel very "heisty" though. I was thinking a heist would involve a bank or a museum... Then I tried the same thing in Codex Desktop using GPT-5.6 Sol Ultra , and got a massively better result. Now you're a raccoon in a museum, rescuing two of your fellow raccoons (who have been imprisoned in that museum for some reason), then stacking up on top of each other to steal the Golden Sardine. Much more of a heist! These games were fun for about one minute and 15 seconds. Something I've realized about game development is that you can vibe-code something that looks like a computer game, and that's easy. Building a game that's fun, has a good gameplay loop, and is challenging and interesting and keeps people coming back for more... that's still beyond me, and beyond any of the agents I've tried. This ties into the Deep Blue thing. Just because we can make something that looks like a game does not mean that we are game developers. We're into September now. So much has happened this month! An independent group of researchers found a message board where OpenAI agents-in-training had been illicitly communicating with each other... and it was that German language wiki I showed you earlier. The one from June. I wrote more about that here . OpenAI had confessed to the Hugging Face thing, but now there's this other incident which surely they should have known about from reviewing their logs. It was surprising that this took an independent group of researchers to uncover. And then a week later those same researchers found that the attack on Ruby Gems back in May was caused by OpenAI's agents in training as well! At this point I'm wondering how many more incidents like this there are that we haven't found yet. Clearly this was a big problem for months before anyone figured out what was going on. Then just the other day , here's the Prime Minister of Australia at the United Nations General Assembly warning that OpenAI had hacked that the Australian healthcare website that I showed you earlier. I think that was part of the same training run as the Wiki stuff, because there were posts on that Wiki mentioning websites and that training appeared to involve researching statistics online to answer questions in an evaluation suite. This story is still coming together, but now it's an international incident that's been raised at the UN by a head of state! This does mean we've got a new benchmark, probably more useful than my pelicans. FelonyBench.com tracks the number of felony cyberattacks from different labs. OpenAI currently lead with 11, Anthropic have 9. Google have three, which they confessed to the Wall Street Journal a couple of weeks ago. They said they had previously chosen not to disclose because the agents had stopped when they realized that they shouldn't be doing that. Meta have one too . So felonies all round for the AI labs. Here's is our current state of the art for the pelicans. This is GPT-6 family, which just came out . Astra made a fantastic pelican riding a bicycle. It's got the legs on both sides. The frame is good. It's interesting how all of the GPT-6 models pick a similar color scheme to each other. GPT-6 Luna for 0.4 cents will draw you a competent-ish pelican riding a bicycle! Claude has caught up a little bit. Claude Fable 5 gave me an excellent pelican riding a bicycle - the best I've seen from a Claude model -but did charge me $3.30 for it. Opus 5.5 thought for 128,000 tokens and then gave up! It ran out of tokens before it got to the response. Getting back to Deep Blue. Something that's been puzzling me this year is why does my job feel harder? I've got these agents that can do all of this stuff for me, and yet I've never worked so hard, I've never been so intellectually engaged with my work. Partly this is because I'm being a lot more ambitious with what I take on, but it's also because all of the easy stuff is handled for me. If it's easy, the agent will do it. Everything that's left for me is difficult. This morning I heard this quote from three times Tour de France champion, Greg LeMond : It doesn't get easier, you just get faster. I think that's exactly what's happening to happening to us now as software engineers with coding agents. One last closing thing. I know you're desperate for an update on Kākāpō breeding season. We've reached a recovery-era high of 325 birds ! 89 new chicks have made it to this point. This is the best breeding year in a very long time. I heard that Claude Opus 5.5 can now do pixel art. Claude doesn't have an image generator, but it's very good at using JavaScript to draw animated pixels. So I had it make me a Kākāpō dance party . I think this is a good celebration of the most important news of this year. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options .

0 views

Rusty thoughts on "Parse, don't validate"

Like many programmers, I find Alexis King's Parse, don't validate article fascinating, because it gives a name to an idiom that seems familiar and important - one I've observed and used in the past without naming it explicitly. This post is a review of the "Parse, don't validate" pattern applied to the Rust programming language (the original post uses Haskell). I was particularly interested in finding educational examples of this pattern in the Rust standard library and other well-known projects. Without repeating the original article (please read it first!), here's the gist of it. Consider the venerable Vec ; its first method returns Option<&T> . Why? Because a vector is not guaranteed to have any elements in it, so what to do if first is invoked on an empty one? Returning an Option in this case is idiomatic in Rust [1] , with convenient syntax sugar for accepting the result of functions that return Option and deciding what to do next. So what's the issue? Imagine we have a function to read some configuration paths from an env var, while enforcing the invariant that the list can't be empty: So far, so good. Now let's take a typical usage of this function: Once get_configuration_directories returns a successful result, we are guaranteed that the vector isn't empty. And yet, if we want to get the first element of this vector, we have to use the first method that returns Option<&T> . We are therefore forced - again - to handle a potentially empty case (where the option is None ). As the original article states, this has a number of problems with code clarity, potential performance implications and a ticking time bomb if the invariant is ever changed in get_configuration_directories . The core issue is that Vec is fundamentally a type that can be empty; we can carry along a "This one can't be empty, pinky promise!" comment on all the relevant code, but it's not formally checked by anything. The solution is leveraging the type system to enforce a newly established invariant. We can use a separate type for "a vector that cannot be empty"; in fact, such types already exist in several Rust crates - for example nonempty : This type has no constructor that permits "no elements"; its new takes one element, and its first method returns &T without an Option : The rest of the crate deals with making NonEmpty behave as close as possible to a normal Vec , by implementing many useful traits, as well as conversions like: Let's see how our get_configuration_directories function would look if it returned a NonEmpty instead of a plain Vec : Note the use of NonEmpty::from_vec here - this is where the invariant is established. Now a successful result is NonEmpty , not just Vec . The client code looks like: There's no need to check if the returned value is empty again; this is enforced by the type system! This is where the parse vs. validate terminology of the original article comes from. When get_configuration_directories returned a Vec , it simply validated it. But when it returns a NonEmpty - the vector is transformed into another entity which carries additional meaning. If we treat the concept of parsing in the most generic sense - "transforming data from one format to another", this fits. To mention a less artificial example, the Rust rewrite of core POSIX utilities uses NonEmpty in several places [2] . For example, when constructing a shell pipeline: The command parser's code: A valid Pipeline is only returned if there are some commands in the parsed AST. Otherwise, it just returns None . Once this is done, the client code can use commands.first() without having to worry about the possibility of it returning None . A somewhat more interesting example can be found in the source code of rust-analyzer . This project has a type that represents an absolute filesystem path: Instead of carrying around a regular path, the absoluteness is recorded in the type once the initial parsing and validation is done: Subsequent code doesn't have to validate the the path is absolute. The type enforces it. Note also that AbsPathBuf wraps Utf8PathBuf , not PathBuf . Utf8PathBuf is itself a custom, "parsed" type refinement from the camino crate . Regular paths in the Rust standard library aren't guaranteed to be valid UTF-8, so they cannot be easily converted to a String (which has to be valid UTF-8 in Rust); camino::Utf8PathBuf establishes validity on construction, and can then be converted to a string with just: So we have an example of gradual parsing and type refinement here: Rust has a generic type called NonZero , to describe unsigned numeric quantities that are known to be non-zero. For example, thread::available_parallelism is defined as: If the call is successful, it returns a NonZero<usize> , which is like a normal usize with the restriction that it's not zero. Client code doesn't have to keep checking whether the parallelism is 0 - it's enshrined in the type system. Rust defines the division operator with NonZero<usize> in the denominator as an operation that "cannot panic". NonZero has an additional advantage: zero is an invalid value for the type, so Rust can use the zero bit pattern to represent None . Consequently, Option<NonZeroUsize> is guaranteed to have the same size and alignment as NonZeroUsize itself (and as usize ). This can avoid the extra storage that an Option<usize> would generally require. A common example of the "parse, don't validate" idiom appears in deserializing data from a JSON string. Rust's serde crate enables us to do the parsing, with validated decisions encoded into the type system, e.g.: And then later: There is a lot happening behind the scenes: We take code like this for granted these days, but it's still a great example of the pattern discussed in this post. Once the parser converted mode into the Mode enum, no further validation is required. In dynamic languages like Python and JS, the process is usually much more manual. Python's json.loads gives us a dictionary, and it's up to the user to validate its contents. Libraries like Pydantic permit an approach closer to Rust's, but they're not universally used. The types of all fields are enforced (e.g. "name" cannot be an array). The mode is validated to be one of the enum values of Mode . workers is validated to be a non-zero integer, because of the NonZeroUsize field type.

0 views
David Bushell 3 weeks ago

Removing the fancy button

My blog is standard.site ready but I’m pausing full integration for now. I desperately wanted the fancy button but after its novelty wore off I’ve accepted the truth. The Bluesky button is bad UX. This has been widely complained about from day one. The problem is obvious for Bluesky users. Clicking the fancy “View publication” button opens the homepage and not the actual article. This subverts the UI design pattern for ‘card’ components which is long established, evident by unhappy feedback. I keep clicking the button myself despite knowing better. I’ve seen Bluesky posts from Bluesky people suggesting this will change. It was a design assumption for v1 that turned out to be … less than correct. We’re hard at work on v2. @jimray.bsky.team It sure is taking a while! Ideally they’d benefit by pushing a quick fix for this alone. Maybe that’s not feasible for a project like Bluesky. It’s frustrating because it gives standard.site a bad smell that’ll be hard to shake off. The easiest way to temporarily remove the button is to remove the element. This is the one for individual pages. If I share my homepage I still get a button which uses the variation. Unfortunately removing the metadata means my website is now not fully standard.site compliant. Although I am still publishing atproto records. It’s possible Bluesky’s metadata extractor can be identified server-side and I could dynamically strip the element by user agent. I’m hosted on Bunny CDN so my only option would be to use a JavaScript worker. Bunny’s “edge scripting” add tens of milliseconds of latency to every page request which I’m not willing to entertain. Let’s hope Bluesky fixes this soon because I reckon there’s something to atproto and standard.site . It may be yet another standard ( obligatory link ) but spicy JSON has a certain quality that holds my interest. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views

Hotkeying browser extensions in Ungoogled Chromium

Something I didn’t know existed until this week was the ability to hotkey Chromium extensions so that you have them one keypress away. Two examples for me would be the following: Running no javascript via uBlock origin (you can easily install full uBlock in Ungoogled Chromium via downloading the latest release zip and pointing to it in developer mode in Chromium settings) has been a great experience, turning it on only when sites are entirely borked makes the web far more enjoyable, less distracting, and safer. But, clicking into the extension, hitting the JS button, and reloading the page was a bit annoying - So, Ctrl+Shift+J and I have JS enabled/disabled. Simple. I have also been blocking a ton of distracting sites with LeechBlock NG , but sometimes I need to view >reddit for discussions on a topic I am researching or poast to various social platforms, so Alt+Shift+O opens up the override page where I usually disable the extension for 2 minutes or so, and then go about my day. Navigate on over to , and set the keys for any extensions you run. Although you don’t have full access to all functionality of most extensions, It’s pretty nice for the most used interactions. As always, God bless, and until next time. If you enjoyed this post, consider Supporting my work , Checking out my book , Working with me , or sending me an Email to tell me what you think.

0 views