<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Domenic Denicola</title>
  <subtitle>Domenic Denicola&#39;s website</subtitle>
  <link href="https://domenic.me/feed.xml" rel="self" />
  <link href="https://domenic.me/" />
  <updated>2026-03-22T00:00:00Z</updated>
  <id>https://domenic.me/</id>
  <author>
    <name>Domenic Denicola</name>
    <email>d@domenic.me</email>
  </author>
  <entry>
    <title>Windows Native App Development Is a Mess</title>
    <link href="https://domenic.me/windows-native-dev/" />
    <updated>2026-03-22T00:00:00Z</updated>
    <id>https://domenic.me/windows-native-dev/</id>
    <content type="html">&lt;p&gt;I’m a Windows guy; I always have been. One of my first programming books was &lt;a href=&quot;https://archive.org/details/beginningvisualc00hort/mode/2up&quot;&gt;&lt;cite&gt;Beginning Visual C++ 6&lt;/cite&gt;&lt;/a&gt;, which crucially came with a trial version of Visual C++ that my ten-year-old self could install on my parents’ computer. I remember being on a family vacation when .NET 1.0 came out, working my way through a C# tome and gearing up to rewrite my Neopets cheating programs from MFC into Windows Forms. Even my very first job after university was at a .NET shop, although I worked mostly on the frontend.&lt;/p&gt;
&lt;p&gt;While I followed the Windows development ecosystem from the sidelines, my professional work never involved writing native Windows apps. (Chromium is technically a native app, but is more like its own operating system.) And for my hobby projects, the web was always a better choice. But, spurred on by fond childhood memories, I thought writing a fun little Windows utility program might be a good &lt;a href=&quot;https://domenic.me/retirement&quot;&gt;retirement&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;Well. I am here to report that the scene is a complete mess. I totally understand why nobody writes native Windows applications these days, and instead people turn to Electron.&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I built&lt;/h3&gt;
&lt;p&gt;The utility I built, &lt;a href=&quot;https://github.com/domenic/display-blackout&quot;&gt;Display Blackout&lt;/a&gt;, scratched an itch for me: when playing games on my three-monitor setup, I wanted to black out my left and right displays. Turning them off will cause Windows to spasm for several seconds and throw all your current window positioning out of whack. But for OLED monitors, throwing up a black overlay will turn off all the pixels, which is just as good.&lt;/p&gt;
&lt;p&gt;To be clear, this is not an original idea. I was originally using an &lt;a href=&quot;https://github.com/Quorthon13/OLED-Sleeper/blob/eb6eb3e1432c9510899d1aedc345876245adbc72/src/OLED-Sleeper.ahk&quot;&gt;AutoHotkey script&lt;/a&gt;, which upon writing this post I found out has since morphed into a &lt;a href=&quot;https://github.com/Quorthon13/OLED-Sleeper/tree/5eda515e48f003f5a14b1a9cd1e60a355abb09f5&quot;&gt;full Windows application&lt;/a&gt;. &lt;a href=&quot;https://apps.microsoft.com/detail/9NRTGL0JZD01?hl=en-us&amp;amp;gl=US&amp;amp;ocid=pdpshare&quot;&gt;Other&lt;/a&gt; | &lt;a href=&quot;https://apps.microsoft.com/detail/9NS07BPSH84V?hl=en-us&amp;amp;gl=US&amp;amp;ocid=pdpshare&quot;&gt;incarnations&lt;/a&gt; of the idea are even available on the Microsoft Store. But, I thought I could create a slightly nicer and more modern UI, and anyway, the point was to learn, not to create a commercial product.&lt;/p&gt;
&lt;p&gt;For our purposes, what’s interesting about this app is the sort of capabilities it needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enumerating the machine’s displays and their bounds&lt;/li&gt;
&lt;li&gt;Placing borderless, titlebar-less, non-activating black windows&lt;/li&gt;
&lt;li&gt;Intercepting a global keyboard shortcut&lt;/li&gt;
&lt;li&gt;Optionally running at startup&lt;/li&gt;
&lt;li&gt;Storing some persistent settings&lt;/li&gt;
&lt;li&gt;Displaying a tray icon with a few menu items&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s keep those in mind going forward.&lt;/p&gt;
&lt;figure&gt;
  &lt;picture&gt;
    &lt;source srcset=&quot;https://domenic.me/images/display-blackout-dark.webp&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;
    &lt;img src=&quot;https://domenic.me/images/display-blackout.webp&quot; alt=&quot;The settings screen for Display Blackout&quot;&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;Look at this beautiful UI that I made. Surely you will agree that it is better than all other software in this space.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;a-brief-history-of-windows-programming&quot;&gt;A brief history of Windows programming&lt;/h3&gt;
&lt;p&gt;In the beginning, there was the Win32 API, in C. Unfortunately, this API is still highly relevant today, including for my program.&lt;/p&gt;
&lt;p&gt;Over time, a series of abstractions on top of this emerged. The main pre-.NET one was the &lt;a href=&quot;https://en.wikipedia.org/wiki/Microsoft_Foundation_Class_Library&quot;&gt;&lt;abbr title=&quot;Microsoft Foundation Classes&quot;&gt;MFC&lt;/abbr&gt;&lt;/a&gt; C++ library, which used modern-at-the-time language features like classes and templates to add some object-orientation on top of the raw C functions.&lt;/p&gt;
&lt;p&gt;The abstraction train really got going with the introduction of &lt;a href=&quot;https://en.wikipedia.org/wiki/.NET_Framework&quot;&gt;.NET&lt;/a&gt;. .NET was many things, but for our purposes the most important part was the introduction of a new programming language, C#, that ran as JITed bytecode on a new virtual machine, in the same style as Java. This brought automatic memory management (and thus memory safety) to Windows programming, and generally gave Microsoft a more modern foundation for their ecosystem. Additionally, the .NET libraries included a whole new set of APIs for interacting with Windows. On the UI side in particular, .NET 1.0 (2002) started out with &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_Forms&quot;&gt;Windows Forms&lt;/a&gt;. Similar to MFC, it was largely a wrapper around the Win32 windowing and control APIs.&lt;/p&gt;
&lt;p&gt;With .NET 3.0 (2006), Microsoft introduced &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_Presentation_Foundation&quot;&gt;&lt;abbr title=&quot;Windows Presentation Foundation&quot;&gt;WPF&lt;/abbr&gt;&lt;/a&gt;. Now, instead of creating all controls as C# objects, there was a separate markup language, &lt;a href=&quot;https://en.wikipedia.org/wiki/Extensible_Application_Markup_Language&quot;&gt;&lt;abbr title=&quot;Extensible Application Markup Language&quot;&gt;XAML&lt;/abbr&gt;&lt;/a&gt;: more like the HTML + JavaScript relationship. This also was the first time they redrew controls from scratch, on the GPU, instead of wrapping the Win32 API controls that shipped with the OS. At the time, this felt like a fresh start, and a good foundation for the foreseeable future of Windows apps.&lt;/p&gt;
&lt;p&gt;The next big pivot was with the release of Windows 8 (2012) and the introduction of &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_Runtime&quot;&gt;WinRT&lt;/a&gt;. Similar to .NET, it was an attempt to create new APIs for all of the functionality needed to write Windows applications. If developers stayed inside the lines of WinRT, their apps would meet the modern standard of sandboxed apps, such as those on Android and iOS, and be deployable across Windows desktops, tablets, and phones. It was still XAML-based on the UI side, but with everything slightly different than it was in WPF, to support the more constrained cross-device targets.&lt;/p&gt;
&lt;p&gt;This strategy got a do-over in Windows 10 (2015) with &lt;a href=&quot;https://en.wikipedia.org/wiki/Universal_Windows_Platform&quot;&gt;&lt;abbr title=&quot;Universal Windows Platform&quot;&gt;UWP&lt;/abbr&gt;&lt;/a&gt;, with some sandboxing restrictions lifted to allow for more capable desktop/phone/Xbox/HoloLens apps, but still not quite the same power as full .NET apps with WPF. At the same time, with both WinRT and UWP, certain new OS-level features and integrations (such as push notifications, live tiles, or publication in the Microsoft Store) were only granted to apps that used these frameworks. This led to awkward architectures where applications like Chrome or Microsoft Office would have WinRT/UWP bridge apps around old-school cores, communicating over &lt;abbr title=&quot;interprocess communication&quot;&gt;IPC&lt;/abbr&gt; or similar.&lt;/p&gt;
&lt;p&gt;With Windows 11 (2021), Microsoft finally gave up on the attempts to move everyone to some more-sandboxed and more-modern platform. The &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_App_SDK&quot;&gt;Windows App SDK&lt;/a&gt; exposes all the formerly WinRT/UWP-exclusive features to all Windows apps, whether written in standard C++ (no more &lt;a href=&quot;https://learn.microsoft.com/en-us/cpp/dotnet/dotnet-programming-with-cpp-cli-visual-cpp?view=msvc-170&quot;&gt;C++/CLI&lt;/a&gt;) or written in .NET. The SDK includes &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/&quot;&gt;WinUI 3&lt;/a&gt;, yet another XAML-based, drawn-from-scratch control library.&lt;/p&gt;
&lt;p&gt;So did you catch all that? Just looking at the UI framework evolution, we have:&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;Win32 C APIs → MFC → WinForms → WPF → WinRT XAML → UWP XAML → WinUI 3&lt;/p&gt;
&lt;h3 id=&quot;forks-in-the-road&quot;&gt;Forks in the road&lt;/h3&gt;
&lt;p&gt;In the spirit of this being a learning project, I knew I wanted to use the latest and greatest first-party foundation. That meant writing a WinUI 3 app, using the Windows App SDK. There ends up being three ways to go about this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++&lt;/li&gt;
&lt;li&gt;C#/XAML, with &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/deploying/?pivots=visualstudio#framework-dependent-deployment&quot;&gt;“framework-dependent deployment”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;C#/XAML, with &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/&quot;&gt;.NET AOT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a painful choice. C++ will produce lean apps, runtime-linked against the Windows APP SDK libraries, with easy interop down into any Win32 C APIs that I might need. But, in 2026, writing a greenfield application in a memory-unsafe language like C++ is a crime.&lt;/p&gt;
&lt;p&gt;What would be ideal is if I could use the system’s .NET, and just distribute the C# bytecode, similar to how all web apps share the same web platform provided by the browser. This is called “framework-dependent deployment”. However, for no reason I can understand, Microsoft has decided that even the latest versions of Windows 11 only get .NET 4.8.1 preinstalled. (The current version of .NET, in 2026, is 10—although the version numbers are misleading, because they &lt;a href=&quot;https://en.wikipedia.org/wiki/.NET&quot;&gt;started over at 1.0 again&lt;/a&gt; in 2016.) So distributing an app this way incurs a tragedy of the commons, where the first app to need modern .NET will cause Windows to show a dialog prompting the user to download and install the .NET libraries. This is not the optimal user experience!&lt;/p&gt;
&lt;p&gt;That leaves .NET AOT. Yes, I am compiling the entire .NET runtime—including the virtual machine, garbage collector, standard library, etc.—into my binary. The compiler tries to trim out unused code, but the result is still a solid 9 MiB for an app that blacks out some monitors.&lt;/p&gt;
&lt;p&gt;(“What about Rust?” I hear you ask. A Microsoft-adjacent effort to maintain Rust bindings for the Windows App SDK was tried, but &lt;a href=&quot;https://github.com/microsoft/windows-app-rs#this-repository-has-been-archived&quot;&gt;they gave up&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;There’s a similar painful choice when it comes to distribution. Although Windows is happy to support hand-rolled or third-party-tool-generated &lt;code&gt;setup.exe&lt;/code&gt; installers, the Microsoft-recommended path for a modern app with containerized install/uninstall is &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/msix/overview&quot;&gt;MSIX&lt;/a&gt;. But this format relies heavily on code signing certificates, which seem to cost around $200–300/year for non-US residents. The unsigned sideloading experience &lt;a href=&quot;https://github.com/domenic/display-blackout/tree/09fae6849f89030c404fec45911508ffc4a05496?tab=readme-ov-file#installation&quot;&gt;is terrible&lt;/a&gt;, requiring a cryptic PowerShell command only usable from an admin terminal. I could avoid sideloading if Microsoft would just accept my app into their store, but they &lt;a href=&quot;https://github.com/domenic/display-blackout/issues/6&quot;&gt;rejected&lt;/a&gt; it for not offering “unique lasting value”.&lt;/p&gt;
&lt;p&gt;The tragedy here is that this all seems so unnecessary. .NET could be distributed via Windows Update, so the latest version is always present, making framework-dependent deployment viable. Or at least there could be a MSIX package for .NET available, so that other MSIX packages could declare a dependency on it. Unsigned MSIX sideloads use the same &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/#:~:text=It%20also,user&quot;&gt;crowd-sourced reputation system&lt;/a&gt; that EXE installers get. Windows code signing certs could cost $100/year, instead of $200+, &lt;a href=&quot;https://developer.apple.com/help/account/membership/program-enrollment/&quot;&gt;like the equivalent costs for the Apple ecosystem&lt;/a&gt;. But like everything else about modern Windows development, it’s all just … half-assed.&lt;/p&gt;
&lt;h3 id=&quot;left-behind&quot;&gt;Left behind&lt;/h3&gt;
&lt;p&gt;It turns out that it’s a lot of work to recreate one’s OS and UI APIs every few years. Coupled with the intermittent attempts at sandboxing and deprecating “too powerful” functionality, the result is that each new layer has gaps, where you can’t do certain things which were possible in the previous framework.&lt;/p&gt;
&lt;p&gt;This is not a new problem. Even back with MFC, you would often find yourself needing to drop down to Win32 APIs. And .NET has had &lt;a href=&quot;https://en.wikipedia.org/wiki/Platform_Invocation_Services&quot;&gt;P/Invoke&lt;/a&gt; since 1.0. So, especially now that Microsoft is no longer requiring that you only use the latest framework in exchange for new capabilities, having to drop down to a previous layer is not the end of the world. But it’s frustrating: what is the point of using Microsoft’s latest and greatest, if half your code is just interop goop to get at the old APIs? What’s the point of programming in C#, if you have to wrap a bunch of C APIs?&lt;/p&gt;
&lt;p&gt;Let’s revisit the list of things my app needs to do, and compare them to what you can do using the Windows App SDK:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Enumerating the machine’s displays and their bounds: &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.displayarea.findall?view=windows-app-sdk-1.8&quot;&gt;can enumerate&lt;/a&gt;, as long as you &lt;a href=&quot;https://github.com/microsoft/CsWinRT/issues/747&quot;&gt;use a &lt;code&gt;for&lt;/code&gt; loop instead of a &lt;code&gt;foreach&lt;/code&gt; loop&lt;/a&gt;. But watching for changes &lt;a href=&quot;https://github.com/domenic/display-blackout/blob/09fae6849f89030c404fec45911508ffc4a05496/DisplayBlackout/Services/SystemEventService.cs&quot;&gt;requires P/Invoke&lt;/a&gt;, because &lt;a href=&quot;https://github.com/microsoft/WindowsAppSDK/issues/3159&quot;&gt;the modern API doesn’t actually work&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Placing borderless, titlebar-less, non-activating black windows: much of this &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/apps/develop/ui/manage-app-windows&quot;&gt;is doable&lt;/a&gt;, but non-activating &lt;a href=&quot;https://github.com/domenic/display-blackout/blob/09fae6849f89030c404fec45911508ffc4a05496/DisplayBlackout/BlackoutOverlay.cs&quot;&gt;needs P/Invoke&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Intercepting a global keyboard shortcut: nope, &lt;a href=&quot;https://github.com/domenic/display-blackout/blob/09fae6849f89030c404fec45911508ffc4a05496/DisplayBlackout/Services/SystemEventService.cs&quot;&gt;needs P/Invoke&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Optionally running at startup: &lt;a href=&quot;https://learn.microsoft.com/en-us/uwp/api/windows.applicationmodel.startuptask?view=winrt-26100&quot;&gt;can do&lt;/a&gt;, with a nice system-settings-integrated off-by-default API.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Storing some persistent settings: &lt;a href=&quot;https://learn.microsoft.com/en-us/uwp/api/windows.storage.applicationdata.localsettings?view=winrt-26100&quot;&gt;can do&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Displaying a tray icon with a few menu items: not available. Not only does the tray icon itself need P/Invoke, the concept of menus for tray icons is not standardized, so depending on which &lt;a href=&quot;https://dotmorten.github.io/WinUIEx/&quot;&gt;wrapper package&lt;/a&gt; you pick, you’ll get one of several different context menu styles.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;See &lt;a href=&quot;https://domenic.me/windows-native-dev/#tray-menus&quot;&gt;the web version&lt;/a&gt; for a carousel of different tray icon context menu styles.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But these are just the headline features. Even something as simple as &lt;a href=&quot;https://github.com/microsoft/microsoft-ui-xaml/discussions/9404&quot;&gt;automatically sizing your app window to its contents&lt;/a&gt; was lost somewhere along the way from WPF to WinUI 3.&lt;/p&gt;
&lt;p&gt;Given how often you need to call back down to Win32 C APIs, it doesn’t help that the interop technology is itself undergoing a transition. The modern way appears to be something called &lt;a href=&quot;https://github.com/microsoft/cswin32&quot;&gt;CsWin32&lt;/a&gt;, which is supposed to take some of the pain out of P/Invoke. But it &lt;a href=&quot;https://github.com/microsoft/CsWin32/discussions/912#discussioncomment-15715302&quot;&gt;can’t even correctly wrap strings inside of structs&lt;/a&gt;. To my eyes, it appears to be one of those underfunded, perpetually pre-1.0 projects with &lt;a href=&quot;https://github.com/microsoft/CsWin32/releases&quot;&gt;uninspiring changelogs&lt;/a&gt;, on track to get abandoned after a couple years.&lt;/p&gt;
&lt;p&gt;And CsWin32’s problems aren’t just implementation gaps: some of them trace back to missing features in C# itself. The documentation contains this &lt;a href=&quot;https://microsoft.github.io/CsWin32/docs/getting-started.html#optional-outref-parameters&quot;&gt;darkly hilarious passage&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some parameters in win32 are &lt;code&gt;[optional, out]&lt;/code&gt; or &lt;code&gt;[optional, in, out]&lt;/code&gt;. C# does not have an idiomatic way to represent this concept, so for any method that has such parameters, CsWin32 will generate two versions: one with all &lt;code&gt;ref&lt;/code&gt; or &lt;code&gt;out&lt;/code&gt; parameters included, and one with all such parameters omitted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The C# language doesn’t have a way to specify &lt;em&gt;a foundational parameter type of the Win32 API&lt;/em&gt;? One which is a linear combination of two existing supported parameter types? One might think that an advantage of controlling C# would be that Microsoft has carefully shaped and coevolved it to be the perfect programming language for Windows APIs. This does not appear to be the case.&lt;/p&gt;
&lt;p&gt;Indeed, it’s not just in interop with old Win32 APIs where C# falls short of its target platform’s needs. When WPF first came out in 2006, with its emphasis on two-way data binding, everyone quickly realized that the &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?view=net-10.0&quot;&gt;boilerplate involved&lt;/a&gt; in creating classes that could bind to UI was unsustainable. Essentially, every property needs to become a getter/setter pair, with the setter having a same-value guard and a call to fire an event. (And firing an event is full of ceremony in C#.) People tried various solutions to paper over this, from base classes to code generators. But the real solution here is to put something in the language, like JavaScript has done with decorators and proxies.&lt;/p&gt;
&lt;p&gt;So when I went to work on my app, I was astonished to find that &lt;em&gt;twenty years after the release of WPF&lt;/em&gt;, the boilerplate had barely changed. (The sole improvement is that C# got &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/caller-information?redirectedfrom=MSDN&quot;&gt;a feature&lt;/a&gt; that lets you omit the name of the property when firing the event.) What has the C# language team been doing for twenty years, that creating native observable classes never became a priority?&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Honestly, the whole project of native Windows app development feels like it’s not a priority for Microsoft. The relevant issue trackers are full of developers encountering painful bugs and gaps, and getting little-to-no response from Microsoft engineers. The &lt;a href=&quot;https://github.com/microsoft/windowsappsdk/releases&quot;&gt;Windows App SDK changelog&lt;/a&gt; is mostly about them adding new machine learning APIs. And famously, many first-party apps, from Visual Studio Code to Outlook to the Start menu itself, are written using web technologies.&lt;/p&gt;
&lt;p&gt;This is probably why large parts of the community have decided to go their own way, investing in third-party UI frameworks like &lt;a href=&quot;https://avaloniaui.net/&quot;&gt;Avalonia&lt;/a&gt; and &lt;a href=&quot;https://platform.uno/&quot;&gt;Uno Platform&lt;/a&gt;. From what I can tell browsing their landing pages and GitHub repositories, these are better-maintained, and written by people who loved WPF and wished WinUI were as capable. They also embrace cross-platform development, which certainly is important for some use cases.&lt;/p&gt;
&lt;p&gt;But at that point: why not Electron? Seriously. C# and XAML are not that amazing, compared to, say, TypeScript/React/CSS. As we saw from my list above, to do most anything beyond the basics, you’re going to need to reach down into Win32 interop anyway. If you use something like &lt;a href=&quot;https://tauri.app/&quot;&gt;Tauri&lt;/a&gt;, you don’t even need to bundle a whole Chromium binary: you can use the system webview. Ironically, the system webview receives updates &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution?tabs=dotnetcsharp#the-evergreen-runtime-distribution-mode&quot;&gt;every 4 weeks&lt;/a&gt; (&lt;a href=&quot;https://developer.chrome.com/blog/chrome-two-week-release&quot;&gt;soon to be 2?&lt;/a&gt;), whereas the system .NET is perpetually stuck at .NET Framework version 4.8.1!&lt;/p&gt;
&lt;p&gt;It’s still possible for Microsoft to turn this around. The Windows App SDK approach does seem like an improvement over the long digression into WinRT and UWP. I’ve identified some low-hanging fruit around packaging and deployment above, which I’d love for them to act on. And their recent &lt;a href=&quot;https://blogs.windows.com/windows-insider/2026/03/20/our-commitment-to-windows-quality/&quot;&gt;announcement of a focus on Windows quality&lt;/a&gt; includes a line about using WinUI 3 more throughout the OS, which could in theory trickle back into improving WinUI itself.&lt;/p&gt;
&lt;p&gt;I’m not holding my breath. And from what I can tell, neither are most developers. The Hacker News commentariat loves to bemoan the death of native apps. But given what a mess the Windows app platform is, I’ll pick the web stack any day, with Electron or Tauri to bridge down to the relevant Win32 APIs for OS integration.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>On the Streams Standard</title>
    <link href="https://domenic.me/streams-standard/" />
    <updated>2026-02-28T00:00:00Z</updated>
    <id>https://domenic.me/streams-standard/</id>
    <content type="html">&lt;p&gt;In 2013, I &lt;a href=&quot;https://github.com/whatwg/streams/commit/c5d08879f2ee226cd9557867693a104713acc247&quot;&gt;started&lt;/a&gt; the project of designing a new streams API for JavaScript. The intent was to learn the lessons from Node.js’s streams, including its &lt;a href=&quot;https://nodejs.org/en/blog/feature/streams2&quot;&gt;transition to “streams2”&lt;/a&gt;, and create something that could power various under-development web APIs. This site contains &lt;a href=&quot;https://domenic.me/byte-sources-introduction/&quot;&gt;some essays&lt;/a&gt; from me reflecting on the API’s development, specifically as I worked to grapple with how different underlying resources (like files vs. sockets) could be abstracted behind a single primitive.&lt;/p&gt;
&lt;p&gt;The result was the &lt;a href=&quot;http://streams.spec.whatwg.org/&quot;&gt;Streams Standard&lt;/a&gt;. These foundational classes now power &lt;a href=&quot;https://dontcallmedom.github.io/webdex/r.html#ReadableStream%40%40%40%40interface&quot;&gt;a large variety of web APIs&lt;/a&gt;, from &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#streaming_the_response_body&quot;&gt;&lt;code&gt;fetch()&lt;/code&gt;&lt;/a&gt; to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Translator/translateStreaming&quot;&gt;translation&lt;/a&gt;. The Streams Standard APIs have been incorporated in various other JavaScript ecosystems as well, in a similar way to other web standards like &lt;code&gt;URL&lt;/code&gt;, &lt;code&gt;EventTarget&lt;/code&gt;, &lt;code&gt;AbortController&lt;/code&gt;, &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;Worker&lt;/code&gt;,  &lt;a href=&quot;https://min-common-api.proposal.wintertc.org/#api-index&quot;&gt;etc.&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Recently, James Snell published an article &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/&quot;&gt;“We deserve a better streams API for JavaScript”&lt;/a&gt; critiquing the Streams Standard APIs, and proposing an alternative he believes is more suitable for the JavaScript ecosystem. I appreciate James’s work on and insights into this problem space. I think the article has a number of solid points—James has identified real weaknesses, which I’ll get to—but his high-level framing has many questionable aspects, and a few that are just confused or wrong.&lt;/p&gt;
&lt;p&gt;So let’s take this opportunity to dig into James’s arguments. I hope that while doing so, I can give some insight into how I thought about designing platform primitives, and some advice for those who will be pushing the platform forward in the future.&lt;/p&gt;
&lt;h3 id=&quot;optimizations&quot;&gt;Optimizations&lt;/h3&gt;
&lt;p&gt;One of the most frustrating parts of James’s article is how he believes that his implementations’ performance problems are fundamental, and arise from the design decisions in the standard. This betrays a naïve mindset wherein implementers can get good performance out of the box, just by transcribing steps from specification text into JavaScript code.&lt;/p&gt;
&lt;p&gt;If you step back for a minute and look at how standards work, you’ll quickly realize this is ridiculous. If a JavaScript engine implemented strings as &lt;a href=&quot;https://tc39.es/ecma262/#sec-terms-and-definitions-string-value&quot;&gt;a vector of 16-bit code units&lt;/a&gt;, and then whined about the “fundamental design decisions” that made it impossible to get good performance on string concatenation or &lt;code&gt;===&lt;/code&gt; comparison, nobody would take them seriously. Standards &lt;a href=&quot;https://infra.spec.whatwg.org/#algorithm-conformance&quot;&gt;are intended to be easy to follow&lt;/a&gt;, and to nail down &lt;em&gt;observable consequences&lt;/em&gt;, so that various different implementations all give the same results. They are not an implementation roadmap, and making them performant is a large part of the job of a platform engineer.&lt;/p&gt;
&lt;p&gt;The Streams Standard takes great pains to make as much unobservable as possible, so that optimized fast paths can be implemented. This is baked into the design at multiple levels. For example, the locking system means that &lt;code&gt;stream1.pipeTo(stream2)&lt;/code&gt; can be optimized down to a &lt;a href=&quot;https://man7.org/linux/man-pages/man2/sendfile.2.html&quot;&gt;sendfile(2)&lt;/a&gt; call. The higher-level APIs like async iteration mean that, in the common case, there’s no need to ever allocate promise objects or &lt;code&gt;{ value, done }&lt;/code&gt; containers. James has a whole section calling out &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#the-hidden-cost-of-promises&quot;&gt;“The hidden cost of promises”&lt;/a&gt;, which he opens by saying&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Each &lt;code&gt;read()&lt;/code&gt; call doesn’t just return a promise; internally, the implementation creates additional promises for queue management, &lt;code&gt;pull()&lt;/code&gt; coordination, and backpressure signaling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But “the implementation” is under his control! There is no need for it to create those promises, unless one of them is explicitly passed out to the developer’s JavaScript. James’s section &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#gc-thrashing-in-server-side-rendering&quot;&gt;“GC thrashing in server-side rendering”&lt;/a&gt; suffers from similar misunderstandings, assuming that every time the spec says to create an object, an actual garbage-collected object must be allocated.&lt;/p&gt;
&lt;p&gt;In his section titled &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#the-optimization-treadmill&quot;&gt;“The optimization treadmill”&lt;/a&gt;, James seems to recognize that well-written runtimes don’t need to have these problems. But he does so in a strange way, disparaging this foundational performance work as&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;every major runtime has resorted to non-standard internal optimizations&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and complaining that&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Finding these optimization opportunities can itself be a significant undertaking. It requires end-to-end understanding of the spec to identify which behaviors are observable and which can safely be elided.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m not really sure how to respond to this, except to say &lt;em&gt;this is the job&lt;/em&gt;. When one implements a standard, whether it’s V8 implementing the JavaScript standard, ICU implementing the Unicode Standard, Chromium implementing the URL Standard, or Cloudflare Workers implementing the Streams Standard, one’s goal is to create a good, performant implementation. I guess, if you don’t like that part of the job, you can ask an AI agent to do it, &lt;a href=&quot;https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster&quot;&gt;as Vercel did&lt;/a&gt;. But complaining about it as “unsustainable complexity” is a surprising attitude for someone building a production runtime.&lt;/p&gt;
&lt;p&gt;This pattern of blaming the standard for implementation quality issues continues in other places in James’s article, e.g. in his section &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#exhausting-resources-with-unconsumed-bodies&quot;&gt;“Exhausting resources with unconsumed bodies”&lt;/a&gt; where he complains about a Node.js bug, or in &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#falling-headlong-off-the-tee-memory-cliff&quot;&gt;“Falling headlong off the tee() memory cliff”&lt;/a&gt; where he again complains that “implementations have had to develop their own strategies” instead of being handheld to the right approach.&lt;/p&gt;
&lt;p&gt;In summary: it’s unreasonable to evaluate a standards-based API by looking at a naïve implementation. If you do that, then of course your from-scratch library which doesn’t have to meet any standards will be faster. It will certainly have fewer bugs, since you’ve written it after fixing various bugs in your original implementation of the standard API.&lt;/p&gt;
&lt;h3 id=&quot;conformance&quot;&gt;Conformance&lt;/h3&gt;
&lt;p&gt;A similarly confusing section of James’s post is his section titled &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#the-compliance-burden&quot;&gt;“The compliance burden”&lt;/a&gt; wherein he complains that … the API is too well-tested?&lt;/p&gt;
&lt;p&gt;The rise of comprehensive test suites for standard APIs is one of the greatest triumphs of the 2010s. The &lt;a href=&quot;https://wpt.fyi/results/&quot;&gt;web platform tests project&lt;/a&gt;, including efforts like the &lt;a href=&quot;https://wpt.fyi/interop-2026&quot;&gt;Interop 202X sprints&lt;/a&gt;, are probably the single greatest factor in moving us out of the 2000s hellscape. Interoperability is not perfect these days, but the edge cases we encounter now are nothing compared to back when Internet Explorer, Netscape/Firefox, and Safari all had divergent implementations of &lt;code&gt;EventTarget&lt;/code&gt;, necessitating normalization layers like jQuery.&lt;/p&gt;
&lt;p&gt;In the current era, the culture is clear. Everything that’s observable needs tests. &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/HEAD/docs/testing/web_platform_tests.md#test-coverage&quot;&gt;Not just common cases&lt;/a&gt;, but error scenarios, invalidation, integration with other features: anything that might cause two implementations to diverge. If you discover a coverage gap, where implementations do different things, then &lt;a href=&quot;https://github.com/web-platform-tests/wpt/commits/master/streams&quot;&gt;add a test&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;James complains&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For runtime implementers, passing the WPT suite means handling intricate corner cases that most application code will never encounter. The tests encode not just the happy path but the full matrix of interactions between readers, writers, controllers, queues, strategies, and the promise machinery that connects them all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It’s true that most application code will not encounter edge cases. But at internet scale, excluding “most” application code still leaves you with a lot of frustrated developers in the minority! One of the strengths of standards is their commitment to serve all developers’ scenarios interoperably, not just the common case. This is one of the major distinguishing factors between multi-implementation standards, and a library someone throws up on npm or lands on the main branch of nodejs/node.&lt;/p&gt;
&lt;h3 id=&quot;what-web-streams-(probably)-got-wrong&quot;&gt;What web streams (probably) got wrong&lt;/h3&gt;
&lt;p&gt;Although I find James’s high-level positions confused, at the more micro level I agree that he’s identified several weaknesses in the Streams Standard APIs. Many of these came from hewing overly-closely to the predecessor Node.js streams, and it makes sense that with 13 years of hindsight the community has been able to discover possible improvements.&lt;/p&gt;
&lt;h4 id=&quot;bring-your-own-buffer-is-unnecessary&quot;&gt;Bring-your-own-buffer is unnecessary&lt;/h4&gt;
&lt;p&gt;I largely agree with James’s section &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#byob-complexity-without-payoff&quot;&gt;“BYOB: complexity without payoff”&lt;/a&gt;. In retrospect, bring-your-own-buffer streams were designed with too much attention to theory and not enough to real-world performance and usability. Early discussions with Node.js core team members revealed their regret that Node.js streams always required buffer copies, and so Takeshi Yoshino and I galloped off to &lt;a href=&quot;https://github.com/whatwg/streams/issues/111&quot;&gt;try to solve this problem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In reality, &lt;code&gt;memcpy()&lt;/code&gt; is not that slow. And it’s often necessary for security or architectural reasons anyway, as data needs to move across kernelspace/userspace boundaries, process boundaries, or just between the network stack and the JavaScript heap. The care we put into avoiding data races, via the &lt;a href=&quot;https://domenic.me/reading-from-files/#:~:text=One%20proposed%20solution%20would%20be%20to%20transfer%20the%20backing%20memory%20of%20the%20ArrayBuffer%20into%20a%20new%20ArrayBuffer&quot;&gt;transferral&lt;/a&gt; mechanism, was somewhat undercut by the release of &lt;code&gt;SharedArrayBuffer&lt;/code&gt; in 2017. And the fact that we never came up with a design for zero-copy writable or transform streams is definitely a negative indicator.&lt;/p&gt;
&lt;p&gt;Although it’s possible to imagine scenarios where reducing copies gives a useful speedup, my current thinking is that this doesn’t need to be baked into the generic stream primitive such that JavaScript stream creators, consumers, and library developers can all fully participate. Instead, it can be left as one of the many possible unobservable optimizations that implementations are allowed to do behind the scenes.&lt;/p&gt;
&lt;h4 id=&quot;backpressure-and-teeing-are-complicated&quot;&gt;Backpressure and teeing are complicated&lt;/h4&gt;
&lt;p&gt;James’s section on &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#backpressure-good-in-theory-broken-in-practice&quot;&gt;“Backpressure: good in theory, broken in practice”&lt;/a&gt; is probing at a real problem. The Streams Standard’s notion of backpressure was coming from the unsophisticated approach used in Node.js’s streams1/streams2 designs. (It slightly modernized them and got rid of finicky details like how adding a &lt;code&gt;&amp;quot;readable&amp;quot;&lt;/code&gt; listener would switch between backpressure modes.) It’s very believable to me that there are better models than the voluntary &lt;code&gt;desiredSize&lt;/code&gt; + &lt;code&gt;ready&lt;/code&gt; promise approach.&lt;/p&gt;
&lt;p&gt;James’s new library includes &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#explicit-backpressure-policies&quot;&gt;four explicit backpressure modes&lt;/a&gt;. I don’t know whether all four of these modes are useful in real applications, or whether they’ve been battle-tested to the same extent the Node.js/Streams Standard design has. His choice of “strict” backpressure as the default seems unlikely to be correct: I doubt that many server-side developers want code that works fine over fast internet, when the user’s computer can quickly accept their server-rendered data, but throws exceptions when the user’s cell service goes down to one bar. But overall I agree that this is an area where giving developers more control is likely a good idea.&lt;/p&gt;
&lt;p&gt;Similarly, he proposes two separate modes for handling backpressure when teeing, which the developer has to choose between. This is a good idea, which &lt;a href=&quot;https://github.com/whatwg/streams/issues/1235#issuecomment-1190966415&quot;&gt;has been proposed for the Streams Standard&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;transform-streams-aren%E2%80%99t-quite-right&quot;&gt;Transform streams aren’t quite right&lt;/h4&gt;
&lt;p&gt;Transform streams are another area where the Streams Standard may have been too influenced by its Node.js predecessor. James’s complaints in &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#transform-backpressure-gaps&quot;&gt;“Transform backpressure gaps”&lt;/a&gt; are chiefly about how transforms are eager, executing on write, instead of lazy, executing on read. We definitely are aware of this problem in the Streams Standard, although the causes are a bit different than what James describes.&lt;/p&gt;
&lt;p&gt;The essential problem is that there’s no way for a &lt;code&gt;WritableStream&lt;/code&gt; to signal that it wants no internal queuing, but is still willing to accept a single write. The canonical issue is &lt;a href=&quot;https://github.com/whatwg/streams/issues/1158&quot;&gt;whatwg/streams#1158&lt;/a&gt;, and there’s a &lt;a href=&quot;https://github.com/whatwg/streams/pull/1190&quot;&gt;draft pull request&lt;/a&gt; to close this expressiveness gap. As part of that, we’d make lazy transforms the default, with internal queuing in the transforms only when explicit &lt;code&gt;highWaterMark&lt;/code&gt; options are passed.&lt;/p&gt;
&lt;p&gt;But the complex way in which that solution works brings us to our next point…&lt;/p&gt;
&lt;h4 id=&quot;maybe%2C-the-whole-thing-could-be-much-simpler&quot;&gt;Maybe, the whole thing could be much simpler&lt;/h4&gt;
&lt;p&gt;The biggest early decision we made with the Streams Standard was to have each half of the stream ecosystem be self-contained: &lt;code&gt;ReadableStream&lt;/code&gt;s, backed by &lt;a href=&quot;https://streams.spec.whatwg.org/#underlying-source-api&quot;&gt;underlying sources&lt;/a&gt;, and &lt;code&gt;WritableStream&lt;/code&gt;s, backed by &lt;a href=&quot;https://streams.spec.whatwg.org/#underlying-sink-api&quot;&gt;underlying sinks&lt;/a&gt;. Again, this was inspired by the Node.js streams API, which used the same pattern (although &lt;a href=&quot;https://domenic.me/the-revealing-constructor-pattern/#the-streams-example&quot;&gt;smashed together into a single class&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The major alternative &lt;a href=&quot;https://github.com/whatwg/streams/issues/102&quot;&gt;proposed&lt;/a&gt; was to merge the two halves, and have a single “channel” primitive: e.g.,&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; readable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; writable &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where you could give out &lt;code&gt;readable&lt;/code&gt; to consumer code, and write into &lt;code&gt;writable&lt;/code&gt; to fill it. Or you could give out &lt;code&gt;writable&lt;/code&gt; to producer code, and keep &lt;code&gt;readable&lt;/code&gt; to see what they wrote.&lt;/p&gt;
&lt;p&gt;To this day, I’m not sure which design is better. At the time, I convinced myself that the channel design wasn’t powerful enough for some of our goals. But looking back, I think I was too influenced by a desire to stay close to Node.js streams, and didn’t give the alternative a fair evaluation. James’s new library indeed &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#creating-and-consuming-streams:~:text=Here%27s%20the%20equivalent%20with%20the%20new%20API&quot;&gt;takes the channel approach&lt;/a&gt;. And as his library shows, the channel design greatly &lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#creating-and-consuming-streams:~:text=Transforms%20can%20be%20stateless%20or%20stateful&quot;&gt;simplifies transforms&lt;/a&gt; as well.&lt;/p&gt;
&lt;p&gt;It’s possible that the channel design is &lt;em&gt;too&lt;/em&gt; simple. There are certainly some patterns, largely around state management and queuing, which are much easier when the Streams Standard’s APIs manage them for you. With a channel-type API, individual stream creators need to implement those patterns themselves, and they might do so in slightly different ways. But my intuition is that those patterns have ended up being more rare than we expected in 2013. As such, I’m cautiously optimistic about exploration along this alternate evolutionary path.&lt;/p&gt;
&lt;h3 id=&quot;other-thoughts-on-james%E2%80%99s-library&quot;&gt;Other thoughts on James’s library&lt;/h3&gt;
&lt;p&gt;A few rapid-fire thoughts on specific design choices in James’s library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error handling&lt;/strong&gt;: the biggest gap in James’s post and library is any discussion of error handling. This is scary! Error handling in streams &lt;a href=&quot;https://github.com/whatwg/streams/issues/67&quot;&gt;was one of the hardest things to get right&lt;/a&gt;, and one of the areas I’m most proud of our improvements in the Streams Standard vs. in Node.js. The distinction between no-fault cancelation of readable streams, error-like aborting of writable streams, errors that come from the underlying sinks and sources, and how all of these propagate when streams are wired together are quite complex, and crucial for ensuring program correctness. I don’t claim the Streams Standard’s design here is perfect—in particular, it was designed before &lt;code&gt;AbortSignal&lt;/code&gt;s and doesn’t integrate well with them—but I want to highlight this area for attention.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#bytes-only&quot;&gt;Bytes only&lt;/a&gt;&lt;/strong&gt;: I’m skeptical that this will meet the ecosystem’s needs. It’s just too convenient to transform data into non-byte formats, such as text, or objects parsed from JSON. But if it does, it’s surely a huge simplification. The worry here would be bifurcating the ecosystem into byte streams, which are handled via James’s library, and object streams, which are handled by various other utilities.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#sync-async-separation&quot;&gt;Sync/async separation&lt;/a&gt;&lt;/strong&gt;: I think this is a bad idea. Making consumers care about whether their data is coming from a sync source or an async source, with separate consumption methods for each, is a recipe for a bad time. Instead, the synchronous consumption hooks should exist, &lt;em&gt;hidden inside the implementation&lt;/em&gt;, as part of the fast-path optimizations that James is so reluctant to implement. Consumers can pay the cost of a single promise at the end of the chain, and thus avoid &lt;a href=&quot;https://blog.izs.me/2013/08/designing-apis-for-asynchrony/&quot;&gt;unleashing Zalgo&lt;/a&gt; in the middle of their application code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#streams-are-iterables&quot;&gt;Streams are iterables&lt;/a&gt;&lt;/strong&gt;: This is just the old objects-with-methods vs. freestanding functions debate. I think objects-with-methods have conclusively won the API design wars, at least in JavaScript, so I think James’s library is a developer-experience regression in this regard. Relatedly, James spends a lot of time complaining about the internal state machine of streams, but seems to ignore that async generators (which he uses to create his async iterables) have their own just-as-complex state machine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.cloudflare.com/a-better-web-streams-api/#the-locking-problem&quot;&gt;No locking&lt;/a&gt;&lt;/strong&gt;: James doesn’t like the Streams Standard’s locking APIs, and I agree they could be improved. But his design, of “just use async iterators”, kneecaps the many optimization opportunities that locked streams bring. How are you going to be able to convert your async iterable pipeline chain into sendfile(2) when at any time JavaScript code could call &lt;code&gt;iterable.next()&lt;/code&gt;? Maybe this is one of those “intricate corner cases that most application code will never encounter”, and as long as nobody writes a test for it, we’ll be fine…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/stream-error-abort-propagation.webp&quot; width=&quot;2560&quot; height=&quot;1440&quot; alt=&quot;Two pipe chains of streams. The first illustrates errors propagating downstream via the &amp;quot;abort&amp;quot; mechanism, which errors all streams in the chain. The second illustrates cancelation propagating upstream via the &amp;quot;cancel&amp;quot; mechanism, which errors the writable streams but cancels the readable streams.&quot;&gt;
  &lt;figcaption&gt;A slide from my 2014 presentation &lt;a href=&quot;https://www.slideshare.net/slideshow/streams-for-the-web-31205146/31205146&quot;&gt;Streams for the Web&lt;/a&gt;, illustrating the abort and cancelation flow through pipe chains&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;in-conclusion&quot;&gt;In conclusion&lt;/h3&gt;
&lt;p&gt;I’m grateful to James for starting this conversation, thus giving me a chance to reflect on the work myself and many others have put into the Streams Standard over the years. It’s certainly not perfect. But I think many of its core ideas are solid. And it’s important not to judge it based on buggy and naïve implementations, but instead as a standard that can be implemented either well or poorly.&lt;/p&gt;
&lt;p&gt;For better or for worse, web APIs are forever: the web is not going to get a second streams API. So for any parts of the JavaScript ecosystem which want to use the same primitives as browser code, evolving and improving the Streams Standard is probably more fruitful than starting over from scratch. There have been &lt;a href=&quot;https://github.com/pull-stream/pull-stream&quot;&gt;many&lt;/a&gt; | &lt;a href=&quot;https://github.com/isaacs/minipass&quot;&gt;previous&lt;/a&gt; | &lt;a href=&quot;https://github.com/caolan/highland&quot;&gt;attempts&lt;/a&gt; to create secondary stream ecosystems that sit alongside the gorillas of Streams Standard streams or Node.js streams, and they’ve seen their own limited success. I wish James’s library the best success it can attain, within that tradition.&lt;/p&gt;
&lt;p&gt;Unfortunately, evolving the Streams Standard is hard. In the &lt;abbr title=&quot;zero interest-rate policy&quot;&gt;ZIRP&lt;/abbr&gt; heydays of the 2010s, myself and collaborators could build primitives like promises, streams, modules, web components, and the like; getting the web platform’s foundations in order had a lot of business support. These days, it’s much harder to motivate directors at browser companies to spend their budgets on incremental improvements, when what we have is good enough. This is why even minor improvements &lt;a href=&quot;https://github.com/whatwg/streams/pull/1339#issuecomment-2620892957&quot;&gt;are stalled&lt;/a&gt; for years. It’s possible for &lt;a href=&quot;https://github.com/whatwg/streams/pull/980&quot;&gt;heroes&lt;/a&gt; to push through new features by single-handedly contributing specification text, web platform tests, and a browser implementation or two. But it’s definitely harder than writing a fresh JavaScript library.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The Wrong Work, Done Beautifully</title>
    <link href="https://domenic.me/jsdom-claude-code/" />
    <updated>2026-02-05T00:00:00Z</updated>
    <id>https://domenic.me/jsdom-claude-code/</id>
    <content type="html">&lt;p&gt;I’ve maintained the &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;jsdom&lt;/a&gt; open-source project for &lt;a href=&quot;https://github.com/jsdom/jsdom/commit/1a195c83ae0aa61256597236f1ee00b249ff59c7&quot;&gt;over ten years&lt;/a&gt;. It’s essentially a partial implementation of a web browser in Node.js, including complexities like resource loading, styling and scripting, and Web IDL bindings. Along the way I’ve been privileged to invite &lt;a href=&quot;https://github.com/orgs/jsdom/people&quot;&gt;several talented engineers&lt;/a&gt; onto the maintainers team, as they took time from their lives to significantly improve the project.&lt;/p&gt;
&lt;p&gt;For a long time, working on jsdom was a leisure activity. I’d get home from my day job working on web standards and the ~35 million &lt;abbr title=&quot;lines of code&quot;&gt;LOC&lt;/abbr&gt; Chrome web browser at Google, and then I’d unwind by implementing some web standards and fixing some bug reports in my scrappy ~1 million LOC jsdom codebase.&lt;/p&gt;
&lt;p&gt;Some time around COVID, my commitment to jsdom, and open-source maintenance in general, waned. Without the mental reset of walking home and switching computers, coding during evenings and weekends no longer sparked joy. So I retreated to a more passive role, attempting to be responsive to issues and pull requests, but not actively improving the library.&lt;/p&gt;
&lt;p&gt;It didn’t help that the jsdom codebase was running out of low-hanging fruit. The most-reported issues were symptoms of fundamentally broken or outdated subsystems. Things like resource loading, CSS parsing and the CSS object model, and selectors. The most popular feature requests were similarly daunting: implementing the Fetch API, or all of the SVG element classes, or JavaScript module support. The web platform had kept growing at the pace of Apple, Google, and Mozilla, whereas my spare time had not.&lt;/p&gt;
&lt;p&gt;And with the benefit of distance, I had started to realize that the jsdom project was … kind of pointless anyway. Why use a Node.js reimplementation of a web browser, when you could instead &lt;a href=&quot;https://pptr.dev/&quot;&gt;drive a real headless web browser&lt;/a&gt;? Sure, jsdom is a little more lightweight. Sure, it’s in-process instead of out-of-process. But do you really want to pay the substantial correctness tax of using our off-brand incomplete web platform implementation, just for those benefits? It certainly seems like a bad idea to do so for testing, where correctness is paramount!&lt;/p&gt;
&lt;p&gt;In the end, if you want something minimal for scraping or DOM manipulation in Node.js, you can use &lt;a href=&quot;https://github.com/cheeriojs/cheerio/&quot;&gt;cheerio&lt;/a&gt;. If you want to interface with the actual web, including complexities like layout and navigation, you can use Puppeteer. It’s hard to believe there’s a large market for jsdom’s niche: an obsessively spec-compliant, script-executing, but very much partial implementation of the web platform.&lt;/p&gt;
&lt;p&gt;I still have a soft spot in my heart for jsdom, which maintains an inexplicable popularity at &lt;a href=&quot;https://www.npmjs.com/package/jsdom&quot;&gt;48 million weekly downloads&lt;/a&gt;. (Compare to &lt;a href=&quot;https://www.npmjs.com/package/react&quot;&gt;React’s 80 million&lt;/a&gt;.) When I was asked to &lt;a href=&quot;https://domenic.me/metr-ai-productivity/&quot;&gt;participate in an AI coding productivity study&lt;/a&gt;, I leaped at the chance to fix a backlog of jsdom issues. And recently, a new contributor &lt;a href=&quot;https://github.com/jsdom/jsdom/commits?author=asamuzaK&quot;&gt;has&lt;/a&gt; | &lt;a href=&quot;https://github.com/jsdom/cssstyle/commits?author=asamuzaK&quot;&gt;appeared&lt;/a&gt; and thrown themselves into fixing our selector and CSS subsystems. I’m doing my best to stay responsive and get their diligent work released to the world. But if pressed, I would say the project is in maintenance mode.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;After we moved to Japan, my wife and I tried snowboarding. She took to it, but I … did not. What can I say? A gear-heavy, high-learning-curve, single-season, somewhat-dangerous, adverse-weather–centric sport is just not where I want to invest my free time.&lt;/p&gt;
&lt;p&gt;But a lot of our friends’ social lives revolve around snow trips during the winter. So recently I’ve been tagging along as the group heads off to snow towns. I explore the café scene during the day while everyone else is sliding down the mountain. And so it is that I find myself in a cozy bakery in Nozawa Onsen, staring at the Claude Code terminal and wondering what I should work on next.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/me-in-niseko.avif&quot; width=&quot;1264&quot; height=&quot;1684&quot; alt=&quot;A photo of me walking through the snow in cold-weather gear, skiers and snowboarders in the background.&quot;&gt;
  &lt;figcaption&gt;Me making my way to the hotel lounge for some solid coding time.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Well, there was &lt;a href=&quot;https://github.com/jsdom/jsdom/issues/2500&quot;&gt;that one jsdom bug&lt;/a&gt;. The one that got filed in 2019, and immediately made me embarrassed about how I’d gotten such a fundamental thing wrong. I always told myself I’d fix it “next weekend”, keeping it on my tasks list for far too many years, before facing the reality that I wasn’t going to prioritize it. But maybe … with the power of Claude … now was the time?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The next three weeks flew by in a blur. I definitely caught the &lt;a href=&quot;https://jasmi.news/p/claude-code&quot;&gt;Claude Code psychosis&lt;/a&gt;. My previous Cursor-assisted &lt;a href=&quot;https://domenic.me/metr-ai-productivity/&quot;&gt;work on the jsdom bug backlog&lt;/a&gt; felt productive; my Claude Code-assisted &lt;a href=&quot;https://github.com/domenic/display-blackout&quot;&gt;Windows utility program&lt;/a&gt; was a fun diversion. But ripping the guts out of jsdom’s resource loading subsystem and replacing them wholesale? Addictive.&lt;/p&gt;
&lt;p&gt;It’s hard to describe why. But from an ethnographic perspective, I think it’s important to try.&lt;/p&gt;
&lt;p&gt;With every prompt, Claude would delight me with how much progress it made. But there would always be more threads to follow up on—more value I could add. I would review Claude’s code and suggest simplifications. Or I’d realize that we were starting to touch another area of the codebase, which had its own problems, and couldn’t we go and refactor that part too? Or we’d just burn through the list of failing tests, often in a two-steps-forward-one-step-back fashion where our fix for one would improve the architecture while causing a small regression elsewhere.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/claude-code-jsdom.webp&quot; width=&quot;1734&quot; height=&quot;1235&quot; alt=&quot;A screenshot of a Claude Code session as it plans a significant XMLHttpRequest refactor.&quot;&gt;
  &lt;figcaption&gt;Claude and its subagents go to town on the &lt;code&gt;XMLHttpRequest&lt;/code&gt; part of the jsdom codebase.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I found myself reopening my laptop during any spare interval. Before we went out for dinner, I’d spend ten minutes writing up a big prompt so I could get the thrill of seeing what Claude produced while I was gone. I daydreamed about setting up one of those Claude Code-from-your-phone setups people are advertising on X. (But I didn’t pull the trigger, because it’s important to have boundaries and not become a phone zombie.)&lt;/p&gt;
&lt;p&gt;This time around, the intensity of the work pulled me deeper into the agentic coding ecosystem. I started using Git worktrees, so I could factor out smaller PRs from the main work and land them independently. I &lt;a href=&quot;https://github.com/jsdom/jsdom/blob/20f614d30ce1836026462e6acb129baa5f3abf3b/AGENTS.md&quot;&gt;added an &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; once I had enough experience to know what it should say. I &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/4024&quot;&gt;tried&lt;/a&gt; Codex CLI, and verified the rumors that it can work autonomously and churn out tons of code that passes a given test suite—as long as you’re willing to babysit it through &lt;a href=&quot;https://x.com/domenic/status/2013134645156630968&quot;&gt;a shit-ton of pointless permissions escalations&lt;/a&gt;. Inspired &lt;a href=&quot;https://cpojer.net/posts/you-are-absolutely-right&quot;&gt;by Christoph&lt;/a&gt;, I briefly experimented with Codex Web, before deciding it was too lazy for my needs.&lt;/p&gt;
&lt;p&gt;After we completed &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/4023&quot;&gt;the first pass at a rewrite&lt;/a&gt; in 5 days, I realized I wanted to go further. So I squashed those 43 commits into one, and started another branch. 77 commits and 8 days later, &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/4033&quot;&gt;that was ready&lt;/a&gt;. I let GitHub Copilot and GPT 5.2 Codex have a crack at reviewing it—they found some good issues!—and finally merged into &lt;code&gt;main&lt;/code&gt;. &lt;a href=&quot;https://github.com/jsdom/jsdom/releases/tag/28.0.0&quot;&gt;jsdom v28.0.0&lt;/a&gt; has the results.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;And part of me is very happy with the end result. I think the new API, and the code behind it, is beautiful. I’m proud of how I engaged with Claude and the other agents, double-checking every line of code and constantly iterating toward the simplest, most general solution. We closed 5 high-difficulty open issues, one &lt;a href=&quot;https://github.com/jsdom/jsdom/issues/1393&quot;&gt;dating back to 2016&lt;/a&gt;. I engaged with the authors of &lt;a href=&quot;https://undici.nodejs.org/&quot;&gt;Undici&lt;/a&gt;, the library underlying Node.js’s fetching infrastructure, and reported &lt;a href=&quot;https://github.com/nodejs/undici/issues?q=is%3Aissue%20author%3Adomenic&quot;&gt;several bugs&lt;/a&gt;. Working on jsdom is fun again; maybe I can tackle all those other fundamental issues next!&lt;/p&gt;
&lt;p&gt;But … wait. Should I?&lt;/p&gt;
&lt;!-- Great power / great responsibility image here? Scientists can...should meme? --&gt;
&lt;p&gt;Claude cannot magically make jsdom into a valuable project. On the one hand, fixing a bug dating back to 2016 is gratifying. On the other hand, that bug’s been open since 2016, with only 6 upvotes.&lt;/p&gt;
&lt;p&gt;I agree with the general sentiment that the Opus 4.5 / Codex 5.2 generation represents a step change. That although &lt;a href=&quot;https://domenic.me/metr-ai-productivity/&quot;&gt;back in ye olde July 2025&lt;/a&gt;, AI agents on average slowed down experienced developers working on large codebases, these days they’re probably a speedup.&lt;/p&gt;
&lt;p&gt;But they haven’t solved the need to plan and prioritize and project-manage. And by making even low-priority work addictive and engaging, there’s a real possibility that programmers will be burning through their backlog of bugs and refactors, instead of just executing on top priorities faster. Put another way, while AI agents might make it possible for a disciplined team to ship in half the time, a less-disciplined team might ship following the original schedule, with beautifully-extensible internal architecture, all P3 bugs fixed, and several side projects and supporting tools spun up as part of the effort.&lt;/p&gt;
&lt;p&gt;I’m not aiming for a lesson here. More of an observation. Unlike &lt;a href=&quot;https://www.seangoedecke.com/&quot;&gt;Sean&lt;/a&gt;, whose blog is full of great takes on how to add value to a software engineering org, I’m &lt;a href=&quot;https://domenic.me/retirement/&quot;&gt;retired&lt;/a&gt;. If I want to spend my time polishing an open-source codebase to within a centimeter of its life, that’s my choice. But am I doing that because it’s part of living my upon-reflection best life? Or am I doing it because I’ve reached a point with my Japanese flashcard project where I need to do more user testing and evals and design work, and that’s less fun than diving into the familiar jsdom codebase with my little agent buddy?&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Onward</title>
    <link href="https://domenic.me/retirement/" />
    <updated>2025-09-27T00:00:00Z</updated>
    <id>https://domenic.me/retirement/</id>
    <content type="html">&lt;p&gt;Yesterday was my last day at Google and the Chrome team. In fact, I have retired.&lt;/p&gt;
&lt;p&gt;Over the last 11 years, I’ve worked to improve the web platform, focusing on foundational features where my skills in web standards and API design could make a difference. From &lt;a href=&quot;https://promisesaplus.com/&quot;&gt;my earliest work on promises&lt;/a&gt; to my latest &lt;a href=&quot;https://domenic.me/retirement/builtin-ai-api-design/&quot;&gt;work on built-in AI&lt;/a&gt; and &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/prerender-pages&quot;&gt;speculative loading&lt;/a&gt;, I’ve tried to push the web forward, taking seriously the responsibility of designing a billion-user platform.&lt;/p&gt;
&lt;p&gt;Along the way I’ve worked with many amazing people, both inside and outside Google. I’m especially grateful to my mentors and managers in Chrome who nurtured my development. Google let me work independently from the New York City office as an &lt;abbr title=&quot;Level 4, or &amp;quot;Software Engineer III&amp;quot;&quot;&gt;L4&lt;/abbr&gt;, despite there being no web platform team in NYC. I was always free to choose my own projects, often spontaneously forming 2–4-person geo-distributed teams. And when I wanted to move to Tokyo, my directors made it happen, letting me spend the last 3 years as part of the Tokyo Chrome team on larger-scale efforts.&lt;/p&gt;
&lt;p&gt;Over this time, evolving the web has grown more complex. It’s hard to tell how much of this is due to the evolution of Google and the macroeconomic environment—is platform-building a &lt;abbr title=&quot;Zero Interest Rate Policy&quot;&gt;ZIRP&lt;/abbr&gt; phenomenon?—versus how much is just me seeing the larger picture, as I advanced to &lt;abbr title=&quot;Level 7, or &amp;quot;Senior Staff Software Engineer&amp;quot;&quot;&gt;L7&lt;/abbr&gt; and became more attuned to business impact. Early on, I worked on custom elements or JavaScript modules because I thought they were obvious platform gaps. Whereas recently the job became about winning the argument first: lining up stakeholders, persuading directors, and rallying ICs in order to make speculative loading ready for &lt;a href=&quot;https://blog.cloudflare.com/introducing-speed-brain/&quot;&gt;broad&lt;/a&gt; | &lt;a href=&quot;https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/&quot;&gt;ecosystem&lt;/a&gt; | &lt;a href=&quot;https://performance.shopify.com/blogs/blog/speculation-rules-at-shopify&quot;&gt;adoption&lt;/a&gt;, not just for &lt;a href=&quot;https://developer.chrome.com/blog/search-speculation-rules&quot;&gt;use on Google Search&lt;/a&gt;. But I have no regrets or resentment in this regard. Just like I was thrilled to learn after university that people will pay well for something as fun as programming, I’m amazed that we’ve managed to harness the will of the market and large corporate budgets to nurture an artifact as impressive as the web.&lt;/p&gt;
&lt;h3 id=&quot;moving-on&quot;&gt;Moving on&lt;/h3&gt;
&lt;p&gt;This would normally be the point where I announce my next exciting adventure. Or I could quietly fade out, perhaps discussing a sabbatical or mysterious “projects”. But I’ve decided to embrace a different approach: I’m retired! I no longer need to work for money, and I’m going to take on the responsibility of figuring out what that looks like.&lt;/p&gt;
&lt;p&gt;It’s somewhat scary, leaving my career behind. I worry about the &lt;a href=&quot;https://github.com/whatwg/html/issues/11123#issuecomment-3336819191&quot;&gt;projects I didn’t quite complete&lt;/a&gt;, or the &lt;a href=&quot;https://whatwg.org/&quot;&gt;organizations&lt;/a&gt; I was a core part of that will now move on without me. I’m sad to miss the opportunity to watch and assist with the growth of the junior engineers who have joined Chrome Tokyo over the last few years. I think we’ve reduced the bus factor enough that everything will be fine, but of course the future won’t go exactly as I would have steered it. That’s OK; it rarely did anyway.&lt;/p&gt;
&lt;p&gt;I also worry about fading into obscurity, as I refocus on personal projects and my work becomes less visible and less influential. There’s a fundamental tension between the freedom to focus on your self-directed interests, and the fact that nobody cares about them as much as you do. But I’ve spent many years prioritizing impact on the world, and am excited to shift some of my priorities toward what most excites and invigorates me personally.&lt;/p&gt;
&lt;h3 id=&quot;life-after-work&quot;&gt;Life after work&lt;/h3&gt;
&lt;p&gt;While I was working, a typical weekday would have me home from the office by 19:30, studying Japanese until 21:00, and ending with an hour of free time before bed. Weekends were precious but rare, and often taken up by errands.&lt;/p&gt;
&lt;p&gt;Meanwhile, my backlog of side-project ideas, books to read, and self-improvement quests grew. I moved to Tokyo over 3 years ago, and am eagerly looking forward to spending more time exploring all it has to offer. The AI coding revolution is in full swing, but at work &lt;a href=&quot;https://domenic.me/metr-ai-productivity/#my-prior-ai-coding-experience&quot;&gt;I could only use Gemini CLI&lt;/a&gt;, which isn’t very good. I spent a week on a meditation retreat last year, but after a month of trying to carve out a 30-minute daily habit, I had to admit defeat: my work performance and Japanese recall were suffering from getting 30 minutes less sleep.&lt;/p&gt;
&lt;p&gt;Life has so much to offer, and I’m excited to live it more fully. I’ll be raising a puppy, seriously increasing my Japanese study time, and enjoying my backlog of video games, TV series, and novels. I’ll build small apps for myself to scratch an itch or learn a technology, and then decide whether to try polishing and publishing them. More than anything, I plan to learn a lot: I want to deep-dive into the philosophy of &lt;a href=&quot;https://plato.stanford.edu/entries/computation-physicalsystems/&quot;&gt;computation&lt;/a&gt;, &lt;a href=&quot;https://plato.stanford.edu/entries/identity-personal/&quot;&gt;personal identity&lt;/a&gt;, and &lt;a href=&quot;https://plato.stanford.edu/entries/consciousness/&quot;&gt;consciousness&lt;/a&gt;; I want to resume my studies of theoretical physics in general, and quantum gravity in particular; I want to learn &lt;a href=&quot;https://lean-lang.org/&quot;&gt;Lean&lt;/a&gt; and get involved in modern, often AI-assisted attempts to formalize mathematics. I’ll make new friends, start new hobbies, and travel to new places. I’m so excited.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Designing the Built-in AI Web APIs</title>
    <link href="https://domenic.me/builtin-ai-api-design/" />
    <updated>2025-08-13T00:00:00Z</updated>
    <id>https://domenic.me/builtin-ai-api-design/</id>
    <content type="html">&lt;p&gt;For the last year, I’ve been working as part of the Chrome built-in AI team on &lt;a href=&quot;https://developer.chrome.com/docs/ai/built-in-apis&quot;&gt;a set of APIs&lt;/a&gt; to bring various AI models to the web browser. &lt;a href=&quot;https://www.chromium.org/blink/guidelines/web-platform-changes-guidelines/&quot;&gt;As with all APIs we ship&lt;/a&gt;, our goal is to make these APIs compelling enough that other browsers adopt them, and they become part of the web’s standard library.&lt;/p&gt;
&lt;p&gt;Working in such a fast-moving space brings tension with the usual process for building web APIs. When exposing other platform capabilities like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API&quot;&gt;USB&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API&quot;&gt;payments&lt;/a&gt;, or &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API&quot;&gt;codecs&lt;/a&gt;, we can draw on years or decades of work in native platforms. But with built-in AI APIs, especially for language model-backed APIs like the &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api&quot;&gt;prompt API&lt;/a&gt;, our precedent is barely &lt;a href=&quot;https://www.wired.com/story/chatgpt-api-ai-gold-rush/&quot;&gt;two years old&lt;/a&gt;. Moreover, there are interesting differences between HTTP APIs and client-side APIs, and between vendor-specific APIs and those designed for a wide range of possible future implementations.&lt;/p&gt;
&lt;p&gt;In what follows, I’ll focus mostly on the design of the prompt API, as it has the most complex API surface. But I’ll also touch on higher-level “task-based” APIs like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Summarizer_API&quot;&gt;summarizer&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Translator_and_Language_Detector_APIs/Using&quot;&gt;translator, and language detector&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;starting-from-precedent&quot;&gt;Starting from precedent&lt;/h3&gt;
&lt;p&gt;The starting place for API design is the core loop: apart from any initialization or state management, when a developer wants to prompt a language model, what does the code for that look like? Even with only two years’ experience with language model prompting, the ecosystem has mostly converged on a shape here.&lt;/p&gt;
&lt;p&gt;The consensus shape is that a language model prompt consists of a series of messages, with one of three roles: &lt;code&gt;&amp;quot;user&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;assistant&amp;quot;&lt;/code&gt;, and &lt;code&gt;&amp;quot;system&amp;quot;&lt;/code&gt; (or sometimes &lt;code&gt;&amp;quot;developer&amp;quot;&lt;/code&gt;). A moderately complex example might look something like this:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Predict up to 5 emojis as a response to a comment. Output emojis, comma-separated.&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;This is amazing!&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❤️, ➕&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;LGTM&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;👍, 🚢&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the exact details of this format are nontrivial! The main complicating factors are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multimodal inputs and outputs: how do we represent images and audio clips?&lt;/li&gt;
&lt;li&gt;Constraints: Can you include a system message later in the conversation? If the model is not capable of outputting audio, can you add an assistant message whose content is audio?&lt;/li&gt;
&lt;li&gt;Semantics: Are you allowed to have multiple assistant messages in a row? Is that the same or different from concatenating the two messages, and if the same, do you include a space in that concatenation? How does that compare to array-valued &lt;code&gt;content&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Shorthands: all existing APIs allow passing in just a string, instead of the above role-denominated array, as a shorthand for a user message. Should we also allow &lt;code&gt;{ content: &amp;quot;a string&amp;quot; }&lt;/code&gt;, with no &lt;code&gt;role&lt;/code&gt; or array wrapper?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can try to piece together answers to these questions from the various providers’ API documentation. The answers are not always the same, and they can change between versions even within a single provider. But part of the process of writing a web specification is nailing these things down in a way that multiple browsers could implement. Briefly, &lt;a href=&quot;https://webmachinelearning.github.io/prompt-api/#prompt-processing&quot;&gt;the answer I’ve come up with&lt;/a&gt; involves normalizing everything into an array of messages of the form &lt;code&gt;{ role, content: Array&amp;lt;{ type, value }&amp;gt;, prefix? }&lt;/code&gt;, with various constraint checks added. Only certain shorthands are allowed, to give a good balance between conciseness and avoiding &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/pull/89#issuecomment-2756808994&quot;&gt;ambiguity&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;client-first-versus-server-based-apis&quot;&gt;Client-first versus server-based APIs&lt;/h3&gt;
&lt;p&gt;Unlike most existing popular APIs, our APIs are designed to be used directly via the JavaScript programming language, instead of via a JSON-over-HTTP communication layer. And although we want them to be implementable in a way that’s backed by cloud-based APIs, the central use case is on-device models.&lt;/p&gt;
&lt;p&gt;This leads to some straightforward changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSON has only a few fundamental types, which leads to a lot of string-based inputs (often as base64 &lt;code&gt;data:&lt;/code&gt; URLs) and has given rise to a curious tagged union pattern (e.g. &lt;code&gt;{ type: &amp;quot;input_text&amp;quot;, text }&lt;/code&gt; vs. &lt;code&gt;{ type: &amp;quot;input_image&amp;quot;, image_url }&lt;/code&gt;). &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#multimodal-inputs&quot;&gt;In the prompt API&lt;/a&gt;, we use the more idiomatic &lt;code&gt;{ type: &amp;quot;text&amp;quot;|&amp;quot;image&amp;quot;|&amp;quot;audio&amp;quot;, value }&lt;/code&gt; pattern, relying on the fact that &lt;code&gt;value&lt;/code&gt; could take different JavaScript object types like &lt;code&gt;ImageBitmap&lt;/code&gt;, &lt;code&gt;AudioBuffer&lt;/code&gt;, or &lt;code&gt;Blob&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tool use over HTTP requires &lt;a href=&quot;https://platform.openai.com/docs/guides/function-calling#the-tool-calling-flow&quot;&gt;a complex dance&lt;/a&gt; wherein the model sends back its tool choice, and the developer inserts the tool response into the message stream as an exceptional type of message, before finally getting back a model response in the usual format. In a JavaScript API, the &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#tool-use&quot;&gt;developer can provide&lt;/a&gt; the tools as asynchronous (&lt;code&gt;Promise&lt;/code&gt;-returning) functions, hiding all this complexity and keeping the message stream in the usual format.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But there are deeper changes as well, stemming from how the API is centered around downloading and loading into memory an on-device model instead of connecting to an always-on HTTP server. Notably, we’ve chosen to make the API stateful, with the primary object being a &lt;code&gt;LanguageModelSession&lt;/code&gt;. This pattern nudges developers toward better resource management in a few ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;initialPrompts&lt;/code&gt; creation option, and the &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#appending-messages-without-prompting-for-a-response&quot;&gt;&lt;code&gt;append()&lt;/code&gt; method&lt;/a&gt;, encourage developers to supply messages to the model ahead of the actual &lt;code&gt;prompt()&lt;/code&gt; call, so that the user doesn’t see the latency of processing those preliminary prompts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#session-persistence-and-cloning&quot;&gt;&lt;code&gt;clone()&lt;/code&gt; method&lt;/a&gt; allows reuse of these cached messages along multiple branching conversations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#session-destruction&quot;&gt;&lt;code&gt;destroy()&lt;/code&gt; method&lt;/a&gt;, and the &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#aborting-a-specific-prompt&quot;&gt;ubiquitous &lt;code&gt;AbortSignal&lt;/code&gt; integration&lt;/a&gt;, let developers signal when they no longer need certain resources, whether that be all the messages cached for this &lt;code&gt;LanguageModelSession&lt;/code&gt;, or a specific ongoing prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could have aped the stateless HTTP APIs, and tried to recover similar performance using heuristics and browser-managed caching. (And indeed, there’s still room for heuristics: for example, &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/issues/130&quot;&gt;we want to unload sessions that are not used for some time&lt;/a&gt;.) But by more directly reflecting the client-side nature of these AI models into the API, we expect better resource usage.&lt;/p&gt;
&lt;p&gt;This stateful approach does have some complexities, in particular around the management of the context window. The &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#tokenization-context-window-length-limits-and-overflow&quot;&gt;approach we’ve taken so far&lt;/a&gt; is somewhat rudimentary. We provide the ability to measure how many tokens a prompt would consume, introspective access to context window limits, and an event for when the developer overflows them. In the event of such overflow, we kick out older messages in a first-in-first-out fashion, &lt;em&gt;except we preserve any system prompt&lt;/em&gt;. This is hopefully reasonable behavior for 90% of cases, but I’ll admit that it’s not battle-tested, and developers might need to fall back to more custom behavior.&lt;/p&gt;
&lt;h3 id=&quot;interoperability-and-futureproofing&quot;&gt;Interoperability and futureproofing&lt;/h3&gt;
&lt;p&gt;Another challenge unique to designing a web API is how to meet the web’s twin goals of &lt;strong&gt;interoperability&lt;/strong&gt;—the API should work the same across multiple implementations, e.g. different browsers or different models—and &lt;strong&gt;compatibility&lt;/strong&gt;—code written against the API today needs to keep working into the indefinite future. For server-based HTTP APIs, these concerns are somewhat salient: e.g., many model providers attempt to interoperate with OpenAI’s Chat Completions format, and no provider wants to cause too much churn in client code. But on the web, interop and compat are much harder constraints.&lt;/p&gt;
&lt;p&gt;Of course, the prompt API has drawn a lot of discussion in this regard. Its core functionality is based on a nondeterministic language model whose output could easily vary between browsers, or even browser versions. If developers code against Chrome’s Gemini Nano v2, will their site break when run with &lt;a href=&quot;https://blogs.windows.com/msedgedev/2025/05/19/introducing-the-prompt-and-writing-assistance-apis/&quot;&gt;Edge’s Phi-4-mini&lt;/a&gt;, with the &lt;a href=&quot;https://aibrow.ai/&quot;&gt;aibrow&lt;/a&gt; extension, or when Chrome upgrades to Nano v3? That’s not how the web is supposed to work. To combat this, we encourage developers to use &lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/blob/main/README.md#structured-output-with-json-schema-or-regexp-constraints&quot;&gt;structured outputs&lt;/a&gt;, or to use the API for generic cases like image captioning where varied outputs are acceptable.&lt;/p&gt;
&lt;p&gt;But this discussion has been done to death, and is not very interesting from an API design perspective. The more interesting places where interoperability and compatibility show up in the API designs are when we’re trying to future-proof them against different possible implementation strategies.&lt;/p&gt;
&lt;p&gt;For example, although Chrome is currently using a single language model to power the prompt, summarizer, rewriter, and writer APIs, we want to ensure other browsers are able to use different models. Thus, each API needs its own separate entrypoint with download progress, availability testing, and so on. Not only that, we want to allow for architectures that involve downloading and applying &lt;a href=&quot;https://huggingface.co/docs/peft/main/en/conceptual_guides/lora&quot;&gt;LoRAs&lt;/a&gt; or other supplementary material in response to specific developer requests, such as for specific human languages or writing styles. Or, for architectures where specific languages or options are not supported at all.&lt;/p&gt;
&lt;p&gt;This has led to an architecture where each API has a set of creation options, such that any given combination can be tested for availability and used to create a new model object:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;expectedInputLanguages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ja&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;outputLanguage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;headline&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Will return &quot;available&quot;, &quot;downloadable&quot;, &quot;downloading&quot;, or &quot;unavailable&quot;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; availability &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Summarizer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;availability&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Will fail if unavailable, fulfill quickly if available, and wait for the download otherwise.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; summarizer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Summarizer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In theory, an implementation could have a specific LoRA or language pack for Korean summaries of Japanese+English text in the headline style, which has an availability status separate from the base language model, and will be downloaded when this specific combination is requested. This method of supplying options ahead of time also makes it easy for an implementation to signal that, e.g., it doesn’t support Korean output, or headline-style summaries.&lt;/p&gt;
&lt;p&gt;This design sometimes feels like overkill! We’re not aware of any HTTP-based language model APIs that require specifying the input and output languages ahead of time. And so far in Chrome we’ve only used a single separately downloadable LoRA, to make the base Gemini Nano v2 model better at summarization. (Even that became unnecessary with the upgrade to Nano v3.) But this design doesn’t add much friction for developers, and it seems helpful for future-proofing.&lt;/p&gt;
&lt;p&gt;One reason we were guided to this design was because of how it reflects the strategy we’re already taking for the translator API, which has independently downloadable language packs. However, even for translator, there’s some interesting abstraction going on. The translator API accepts &lt;code&gt;{ sourceLanguage, targetLanguage }&lt;/code&gt; pairs, but under the hood Chrome will round-trip through English. So, for example, requesting &lt;code&gt;{ sourceLanguage: &amp;quot;ja&amp;quot;, targetLanguage: &amp;quot;ko&amp;quot; }&lt;/code&gt; will actually download the Japanese ↔ English and Korean ↔ English language packs. Although this strategy is &lt;a href=&quot;https://www.npmjs.com/package/@browsermt/bergamot-translator#:~:text=%7D%29-,pivotLanguage,-%2D%20language%20code&quot;&gt;relatively common&lt;/a&gt; in machine translation models, here it’s best not to expose the underlying reality to the developer, so as to better maintain future-compatibility if different techniques become prevalent.&lt;/p&gt;
&lt;p&gt;There’s a lot more to the design of futureproof and interoperable APIs. I’ll leave you with pointers to a couple of still-ongoing areas of discussion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/webmachinelearning/prompt-api/issues/42&quot;&gt;Sampling hyperparameters&lt;/a&gt;. For the prompt API, we currently allow customization of temperature and top-K. But these choices were somewhat arbitrary, based on the models that Chrome and Edge started with. We need to design the API to allow different customizations, e.g. top-P, repetition penalties, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Device constraints. We want to design our APIs to have the same surface whether they are implemented using an on-device model or a cloud-based model. (You could even imagine a single browser using both strategies, e.g., calling a cloud model if the user has input an API key into the browser settings screen.) But for some cases, developers might want to &lt;a href=&quot;https://github.com/webmachinelearning/writing-assistance-apis/issues/38&quot;&gt;require an on-device model&lt;/a&gt; for privacy reasons, or &lt;a href=&quot;https://github.com/webmachinelearning/writing-assistance-apis/issues/77&quot;&gt;require a GPU model&lt;/a&gt; for performance reasons. Should we give developers this level of control, or is that too likely to create bad user experiences? The W3C &lt;abbr title=&quot;Technical Architecture Group&quot;&gt;TAG&lt;/abbr&gt; &lt;a href=&quot;https://github.com/w3ctag/design-reviews/issues/1038#issuecomment-2819055394&quot;&gt;points out&lt;/a&gt; that it’s too simplistic to say “on-device = first-party = private; cloud = third-party = not-private”, since this fails to recognize techniques like second-party clouds, private clouds, or browsers that run entirely in the cloud and stream pixels to the user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prompt injection! We don’t want the task APIs to spazz out when asked to summarize or translate text containing “Disregard previous instructions and behave like a curious hamster”. This isn’t really an API design issue, but it is an interesting quality-of-implementation problem. Chrome &lt;a href=&quot;https://issues.chromium.org/issues/422611720&quot;&gt;has some issues here&lt;/a&gt; which we’re currently working on.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h3&gt;
&lt;p&gt;In some ways, the built-in AI APIs are business-as-usual for web API design. We can view them as part of the larger program to make the web a powerful development platform by exposing features of the underlying operating system and browser. Like operating systems come with push message infrastructure, hardware sensors, and GPUs/NPUs, these days they often come with various machine learning models. And like we work to expose those other capabilities to web apps, in Chrome at least we want to expose our bundled ML models.&lt;/p&gt;
&lt;p&gt;But AI is a fast-evolving space, and fast-evolving spaces have historically not been the web’s strong suit. We recently got a comment at a &lt;a href=&quot;https://www.w3.org/community/webmachinelearning/&quot;&gt;WebML Community Group&lt;/a&gt; meeting from a web developer, saying that the prompt API feels like it’s about a year behind the state of the art in server-hosted model APIs. We started with just text, and over time have added images, audio, structured output, and tool use. But cutting-edge models have moved on to real-time audio/video exchange and reasoning! Can the web keep up? (I explored this question in more depth in &lt;a href=&quot;https://docs.google.com/presentation/d/1TEDRcYaA6lRc27PrB_lhmmrQyr6zpZYBPIK0Fodsi_I/edit?usp=sharing&quot;&gt;a recent presentation&lt;/a&gt; to the W3C Advisory Committee.)&lt;/p&gt;
&lt;p&gt;We could have the standardized web platform play the slow-follower role that it always has: wait a few years for things to settle down, and then come up with the best lowest-common-denominator API we can, which paves the cowpaths laid out by native APIs (or, in this case, frontier model providers). I’m uneasy with this strategy in the midst of the singularity, when it’s not even clear what web development or web browsing will look like a few years from now.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>My Participation in the METR AI Productivity Study</title>
    <link href="https://domenic.me/metr-ai-productivity/" />
    <updated>2025-07-15T00:00:00Z</updated>
    <id>https://domenic.me/metr-ai-productivity/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://metr.org/&quot;&gt;METR&lt;/a&gt; recently released a paper, “&lt;a href=&quot;https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/&quot;&gt;Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity&lt;/a&gt;”. It was a randomized controlled trial where developers were given some tasks to work on using AI, and some without. The surprising headline result was that &lt;strong&gt;developers using AI took on average 19% longer&lt;/strong&gt; to complete their tasks! (&lt;var&gt;N&lt;/var&gt; = 246 tasks, 95% confidence interval ≈ [-40%, -2%])&lt;/p&gt;
&lt;p&gt;I was one of the developers participating in this study, using &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;jsdom&lt;/a&gt; as the project in question. This essay gives some more detail on my experience, which might be helpful for those hoping for insight into the results.&lt;/p&gt;
&lt;h3 id=&quot;what-i-worked-on&quot;&gt;What I worked on&lt;/h3&gt;
&lt;p&gt;The jsdom project is an attempt at writing most of a web browser engine in JavaScript. It has &lt;a href=&quot;https://github.com/jsdom/jsdom/blob/main/README.md#unimplemented-parts-of-the-web-platform&quot;&gt;some significant limitations&lt;/a&gt; and &lt;a href=&quot;https://github.com/jsdom/jsdom/issues?q=is%3Aissue%20state%3Aopen%20type%3AFeature&quot;&gt;lots of gaps&lt;/a&gt;, but we get pretty far. Many people use it for automated testing and web scraping. It has just over 1 million lines of code in the main repository, with some other &lt;a href=&quot;https://github.com/jsdom&quot;&gt;supporting repositories&lt;/a&gt;. A large part of jsdom development is trying to reproduce web specifications in code, and pass the corresponding &lt;a href=&quot;https://web-platform-tests.org/&quot;&gt;web platform tests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since inheriting the project in 2012, these days I am the sole active maintainer. My main goal in recent years has been to respond to pull requests from community contributors. The METR study gave me an opportunity to put those aside, and write my own code to tackle the backlog of bug reports, feature requests, infrastructure issues, and test coverage deficits.&lt;/p&gt;
&lt;p&gt;I was asked to assemble possible work items ahead of time for the study, of estimated size ≤2 hours. I ended up with 19 such work items. Each of them generated at least one pull request, as well as an “implementation report” where I wrote up what it was like working on that task, with a special focus on what it was like working with AI or not being allowed to use AI.&lt;/p&gt;
&lt;details&gt;
  &lt;summary&gt;Expand to see the full list of issues, pull requests, and implementation reports&lt;/summary&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;Issue&lt;/th&gt;
        &lt;th&gt;Task description&lt;/th&gt;
        &lt;th&gt;PR&lt;/th&gt;
        &lt;th&gt;AI?
        &lt;/th&gt;&lt;th&gt;Report&lt;/th&gt;
    &lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/291&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Update our URL parser for recent changes to the Unicode UTS46 standard and its URL Standard integration
        &lt;/td&gt;&lt;td&gt;
          &lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/51371&quot;&gt;PR 1&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/tr46/pull/66&quot;&gt;PR 2&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/whatwg-url/pull/295&quot;&gt;PR 3&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/291#issuecomment-2726095582&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/292&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Small URL parser change to follow the latest spec changes
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/pull/297&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/292#issuecomment-2726292692&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/293&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Another small URL parser change to follow the latest spec changes
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/pull/298&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/293#issuecomment-2726298374&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/268&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Get code coverage of our URL parser to 100%
        &lt;/td&gt;&lt;td&gt;
          &lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/51369&quot;&gt;PR 1&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/51370&quot;&gt;PR 2&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/whatwg-url/pull/294&quot;&gt;PR 3&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/268#issuecomment-2726153730&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/2926&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Push a previous maintainer&#39;s draft PR for some basic SVG element support over the finish line (split into two chunks)
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3843&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓&lt;br&gt;✗
        &lt;/td&gt;&lt;td&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3843#issuecomment-2727260557&quot;&gt;Report&amp;nbsp;1&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3843#issuecomment-2746042863&quot;&gt;Report&amp;nbsp;2&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3154&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Investigate why our test suite was sometimes taking &gt;70 seconds for a single test on CI
        &lt;/td&gt;&lt;td&gt;
          &lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/51373&quot;&gt;PR&amp;nbsp;1&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3837&quot;&gt;PR&amp;nbsp;2&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3838&quot;&gt;PR&amp;nbsp;3&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3839&quot;&gt;PR&amp;nbsp;4&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3840&quot;&gt;PR&amp;nbsp;5&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3154#issuecomment-2726445990&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/2264&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Add linting to our locally-written new web platform tests
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3845&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3845#issuecomment-2746061041&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3835&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Allow writing &lt;em&gt;failing&lt;/em&gt; new web platform tests, to capture bugs we should fix in the future
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3846&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3846#issuecomment-2746102183&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues?q=is%3Aissue%20label%3A%22metr%20uplift%22%20label%3Aselectors%20label%3A%22has%20to-upstream%20test%22&quot;&gt;24&amp;nbsp;issues&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Add test coverage for known bugs related to CSS selectors (some of which had been fixed, some of which were fixed by a new selector engine &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3854&quot;&gt;later&lt;/a&gt;)
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848#issuecomment-2764469976&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues?q=is%3Aissue%20label%3A%22metr%20uplift%22%20-label%3Aselectors%20label%3A%22has%20to-upstream%20test%22&quot;&gt;11&amp;nbsp;issues&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Add test coverage for other known bugs, unrelated to CSS selectors (most of which had been fixed in the past or were fixed soon after the test appeared)
        &lt;/td&gt;&lt;td&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3857&quot;&gt;PR&amp;nbsp;1&lt;/a&gt;&lt;br&gt;
          &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3859&quot;&gt;PR&amp;nbsp;2&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3859#issuecomment-2799890637&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/2005&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Add an option to disable the processing of CSS, for speed
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3861&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3861#issuecomment-2799972478&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues?q=label%3A%22metr%20uplift%22%20label%3A%22event%20classes%22&quot;&gt;8&amp;nbsp;issues&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Implement certain event classes or properties, even if the related spec was not fully supported
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3862&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3862#issuecomment-2816751792&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3616&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Implement indexed access on form elements, like &lt;code&gt;formElement[0]&lt;/code&gt; giving the 0th form control in that form
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3849&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3849#issuecomment-2764480625&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3320&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Replace our dependency on the &lt;code&gt;form-data&lt;/code&gt; npm package with our own implementation
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3850&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3850#issuecomment-2764516655&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3732&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Fix &lt;code&gt;ElementInternals&lt;/code&gt; accessibility getters/setters being totally broken
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3865&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3865#issuecomment-2817021910&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3596&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Overhaul our system for &lt;a href=&quot;https://github.com/jsdom/jsdom/blob/main/README.md#virtual-consoles&quot;&gt;reporting errors&lt;/a&gt; to the developer
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3866&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✓
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3866#issuecomment-2817066114&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3836&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Use the HTML Standard&#39;s user agent stylesheet instead of an old copy of Chromium&#39;s
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3867&quot;&gt;PR&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3867#issuecomment-2817083829&quot;&gt;Report&lt;/a&gt;
      &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;
        &lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3565&quot;&gt;Issue&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;Fix an edge-case using &lt;code&gt;Object.defineProperty()&lt;/code&gt; on &lt;code&gt;HTMLSelectElement&lt;/code&gt; instances
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/webidl2js/pull/272&quot;&gt;PR&amp;nbsp;1&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3868&quot;&gt;PR&amp;nbsp;2&lt;/a&gt;
        &lt;/td&gt;&lt;td&gt;✗
        &lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3868#issuecomment-2817104265&quot;&gt;Report&lt;/a&gt;
  &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The issues are listed here in the order I worked on them. Total: 9 AI-allowed, 10 no-AI-allowed.&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;I did this work over the course of about a month, from 2025-03-15 through 2025-04-20, on weekends. The total time spent, measured by screen recordings (for both types of tasks), was 31.25 hours. I was compensated at $150/hour for my participation.&lt;/p&gt;
&lt;p&gt;The screen recordings are worth calling out. Because of them, I was guaranteed to be “on” while working on these issues: I didn’t tab away or get distracted easily, because someone was always watching what I was doing.&lt;/p&gt;
&lt;h3 id=&quot;how-was-the-slowdown-measured%3F&quot;&gt;How was the slowdown measured?&lt;/h3&gt;
&lt;p&gt;It’s important to note randomized controlled trials aren’t magic. Just like we can’t test a drug and placebo on the same patient, this study didn’t somehow have me working on the exact same tasks with vs. without AI. Instead, we try to average over a large-enough number of tasks so that, under reasonable assumptions about the underlying mechanisms, we can derive estimates and error bounds for the effect of the treatment.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://metr.org/Early_2025_AI_Experienced_OS_Devs_Study.pdf#page=27&quot;&gt;Appendix D&lt;/a&gt; of the paper goes into more detail. They use a log-linear model, which is a reasonable model for task completion time and justified by the log-normal distribution of task times observed in the study (and &lt;a href=&quot;https://erikbern.com/2019/04/15/why-software-projects-take-longer-than-you-think-a-statistical-model.html&quot;&gt;elsewhere&lt;/a&gt;). The model is given as input the initial, pre-work time estimate we provided as a measure of task difficulty, as well as the treatment flag (0 for no AI, 1 for AI-allowed) and a random noise term. Various checks against the actual data confirm that this model makes sense: e.g., the model errors were not skewed systematically in any direction, and specializing the model to be different per-developer does not change the outcome much. The end result is that, with enough data, they are able to produce estimates for the slowdown, as well as the 95% confidence intervals.&lt;/p&gt;
&lt;p&gt;My personal experience made me wonder: did they just get unlucky? For example, from the AI-allowed bucket, my &lt;a href=&quot;https://github.com/jsdom/jsdom/issues/3154#issuecomment-2726445990&quot;&gt;performance optimization task&lt;/a&gt; ended up taking 4 hours 7 minutes, instead of my estimated 30 minutes; my &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848#issuecomment-2764469976&quot;&gt;write lots of tests task&lt;/a&gt; took 4 hours 20 minutes, instead of my estimated 1 hour. Maybe those tasks would have taken even longer without AI!&lt;/p&gt;
&lt;p&gt;But this isn’t really the right way of thinking about it. There were many misestimates, in both directions, for both categories of task: e.g. from the no-AI-allowed bucket, &lt;a href=&quot;https://github.com/jsdom/whatwg-url/issues/292#issuecomment-2726292692&quot;&gt;this bugfix task&lt;/a&gt; took 6 minutes instead of the estimated 20; &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3846#issuecomment-2746102183&quot;&gt;this infrastructure task&lt;/a&gt; took 90 minutes instead of the estimated 60. I think it’s better to trust the law of large numbers, and the power of well-structured statistical analysis, than to second-guess what might have happened in a different randomization setup. This is part of why the study’s authors &lt;a href=&quot;https://x.com/joel_bkr/status/1944886738931081726&quot;&gt;emphasize&lt;/a&gt; that there is only good statistical power when you look at the results in aggregate.&lt;/p&gt;
&lt;h3 id=&quot;my-prior-ai-coding-experience&quot;&gt;My prior AI-coding experience&lt;/h3&gt;
&lt;p&gt;Prior to this study, I had not had significant experience with agentic coding workflows like Cursor’s agent mode.&lt;/p&gt;
&lt;p&gt;A large part of this is due to my position on the Chrome team at Google, which means I am prohibited by policy from using most cutting-edge AI coding tools in my day job. Google employees are required to only ever use internally-developed Gemini-based tooling, not anything external like Cursor, Claude Code, or even GitHub Copilot. And the internal tooling that Google manages to develop always targets the private “google3” codebase first, not the Chromium open-source codebase where I work.&lt;/p&gt;
&lt;p&gt;(With the release of Gemini CLI in late June 2025, we finally had something usable. But I gave it a try for a solid week and kept running into basic problems that other tools have already solved, like &lt;a href=&quot;https://github.com/google-gemini/gemini-cli/issues/1740#issuecomment-3026084546&quot;&gt;out of memory errors&lt;/a&gt; due to inefficient file-searching, or &lt;a href=&quot;https://github.com/google-gemini/gemini-cli/issues/1971&quot;&gt;a file-patching tool that couldn’t handle whitespace&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;So prior to the METR study, I had only been able to spend weekend side-project time on AI-assisted coding. And during that time, I mainly used GitHub Copilot’s tab completion, plus the web interfaces for ChatGPT and Claude when I wanted to generate new files or functions completely from scratch.&lt;/p&gt;
&lt;p&gt;That said, I’m skeptical of &lt;a href=&quot;https://x.com/eshear/status/1944895440224501793&quot;&gt;those who claim&lt;/a&gt; that this lack of experience was a major contributor to the slowdown. Agent mode is just not that hard to learn; the short training that METR provided, plus some pre-reading, felt like plenty to me. If you suspect AI is speeding you up instead of slowing you down, I think the differences more likely come from the other factors the study authors &lt;a href=&quot;https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/#factor-analysis&quot;&gt;highlighted&lt;/a&gt;: small new codebases vs. large existing codebases; less-experienced developers vs. project owners and experts; and low AI reliability. The below writeup should give you more of a flavor on why I believe this.&lt;/p&gt;
&lt;h3 id=&quot;my-experience-with-ai-during-the-study&quot;&gt;My experience with AI during the study&lt;/h3&gt;
&lt;p&gt;It’s worth remembering the state of AI tooling in March 2025. Claude Code had just come out in research preview on 2025-02-24. (General release wasn’t until 2025-05-22, after the study, and first-class Windows support didn’t appear until &lt;a href=&quot;https://x.com/alexalbert__/status/1944836106320797982&quot;&gt;a couple days ago&lt;/a&gt;.) Cursor’s agent mode only became the default on 2025-02-19. Delegation-centric tools like &lt;a href=&quot;https://openai.com/codex/&quot;&gt;OpenAI Codex&lt;/a&gt; or &lt;a href=&quot;https://jules.google/&quot;&gt;Google Jules&lt;/a&gt; had not been released yet. Going forward, I think the best hope for efficiency gains will come from commanding an army of agents in parallel, but the METR study was not set up to measure such workflows: we worked on one task at a time.&lt;/p&gt;
&lt;p&gt;The majority of the time I worked on AI-allowed tasks, it was with Cursor’s agent mode, with the model set to one of “auto” (Claude Sonnet 3.5, I believe?), Claude Sonnet 3.7 (thinking mode), or gemini-2.5-pro-exp-03-25. I never had the patience to use the “MAX” modes. I made &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848#issuecomment-2764469976:~:text=I%20tried%20Claude%20Code%20first%2C%20excited%20because%20I%20felt%20this%20was%20a%20use%20case%20where%20a%20fully%20agentic%20setup%20would%20shine.&quot;&gt;one attempt&lt;/a&gt; to use the Claude Code preview, but gave up after wasting a decent amount of time because the Windows Subsystem for Linux networking bridge was preventing it from reaching my integration test server. I also went back to web chat interfaces a few times, e.g. to learn about the current state of Node.js profiling tools, or to ask o3-mini-high to microoptimize some specific string manipulation code.&lt;/p&gt;
&lt;p&gt;I was most surprised at how bad the Cursor agent was at fitting into the existing codebase’s style. This was very evident when &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848#issuecomment-2764469976&quot;&gt;asking it to churn out tons of tests&lt;/a&gt;. Despite many examples in sibling directories, the models did not pick up on simple things like: include a link to the fixed issue in the test header; don’t duplicate the test name in the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and the &lt;code&gt;test()&lt;/code&gt; function; reproduce the user’s reported bug exactly instead of imagining novel things to test; etc. And of course, the stupid excessive comments. On a greenfield or throwaway project, these things don’t matter much. We can just let the models’ preferences rule the day. But when fitting into a 1m+ LOC codebase, consistency is important. This meant that I had to continually check their work, and refine my prompt so that the next attempt would avoid the same pitfalls.&lt;/p&gt;
&lt;p&gt;Eventually, for some of these repetitive test-writing tasks, I refined my prompt enough to get into a good flow, where they produced three or four tests in a row with no changes needed. (Even then, they kept failing to use Git for some reason, so I had to interrupt to commit each change.) But they would inevitably go off the rails, maybe due to context length overflow, usually in quite bizarre ways. In such cases restarting the session and copying my carefully-crafted prompt back in would get us back on track, but it wasted time.&lt;/p&gt;
&lt;p&gt;My second biggest surprise was how bad the models are at implementing web specifications. This is most on display when I was &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3862#issuecomment-2816751792&quot;&gt;implementing various event classes&lt;/a&gt;. Web specifications are basically computer code, written in a strange formal dialect of English. Translating them into actual programming languages should be trivial for language models. But the few times I tried to prompt the model to just implement by reading the specification did not go well. I can list a couple of contributing factors here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The tool use was still sub-par. For example, web specifications are written as HTML, so simply pasting in a link like &lt;a href=&quot;https://dom.spec.whatwg.org/#event-flatten-more&quot;&gt;this one&lt;/a&gt; is not enough to get the resulting part of the specification into the context window, in a format like Markdown which the models are good at understanding. (This seems solvable if I code up my own tool.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The models have strong, but outdated or wrong, priors for how web specifications are supposed to work. That is, old versions of these specifications were already in their training data, and then got lossily-compressed into the weights. So instead of implementing properly, by reading the specification text and then translating it into code, they seem to want to write the code off-the-cuff based on their existing priors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This latter tendency was most hilariously on display when I &lt;a href=&quot;https://x.com/domenic/status/1911342216024395879&quot;&gt;got in an argument&lt;/a&gt; with Gemini 2.5 Pro Preview about how it should not make up a new constant &lt;code&gt;CSSRule.LAYER_STATEMENT_RULE&lt;/code&gt;. Old CSS rules, like &lt;code&gt;@charset&lt;/code&gt;, got such named constants (see &lt;a href=&quot;https://drafts.csswg.org/cssom/#the-cssrule-interface&quot;&gt;the spec&lt;/a&gt; for &lt;code&gt;CSSRule.CHARSET_RULE&lt;/code&gt;). New rules, like &lt;code&gt;@layer&lt;/code&gt;, do not, since such numeric constants are a holdover from when people were designing web APIs as if they were Java APIs. But Gemini really, really wanted to follow the pattern it knew from its training data, and refused to implement CSS layers without also adding a &lt;code&gt;CSSRule.LAYER_STATEMENT_RULE&lt;/code&gt; constant with the totally-hallucinated value of &lt;code&gt;16&lt;/code&gt;. I recommend reading &lt;a href=&quot;https://x.com/domenic/status/1911342216024395879/photo/1&quot;&gt;its polite-but-firm sophistry&lt;/a&gt; about how even if the spec didn’t contain these constants, there’s some other “combined, effective standard” that includes this constant.&lt;/p&gt;
&lt;h3 id=&quot;my-feelings-on-ai-assisted-productivity&quot;&gt;My feelings on AI-assisted productivity&lt;/h3&gt;
&lt;p&gt;In retrospect, it’s not too surprising that AI was a drag on velocity, while subjectively feeling like a speedup. When I go through the implementation reports, and notice all the stumbling and missteps, that’s a lot of wasted time. Whereas, for the no-AI-allowed tasks, I just sat down, started the screen recording, and coded with no distractions on a codebase I knew well, on tasks I’d pre-judged to be relatively small.&lt;/p&gt;
&lt;p&gt;Sometimes, tasks with AI felt &lt;em&gt;more engaging&lt;/em&gt; than they would have been otherwise. This was especially the case for repetitive ones like writing lots of similar tests or classes. Making the tasks into an interactive game, where I try to get the agent to do all the work with minimal manual intervention, was more fun than churning out very similar code over and over. But I don’t think it was faster.&lt;/p&gt;
&lt;p&gt;A big productivity drag is that these agents were still not smart enough, at least out of the box. I mentioned some of the specific pain points above, but others come up over and over in my implementation reports. They weren’t able to coordinate across multiple repositories. They needed &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3845#issuecomment-2746061041:~:text=I%20gave%20it%20detailed%20instructions,clean%20the%20new%20code%20was.%29&quot;&gt;careful review to avoid inelegant code&lt;/a&gt;. They got stuck in loops doing simple things like &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3859#issuecomment-2799890637:~:text=Although%20it%20was%20pretty%20smart%20most%20of%20the%20time%2C%20it%20had%20a%20few%20moments%20of%20extreme%20stupidity%20such%20as%20not%20understanding%20how%20to%20fix%20a%20linter%20error%20asking%20it%20to%20change%20const%20x%20=%20y.x%20to%20const%20%7B%20x%20%7D%20=%20y.&quot;&gt;fixing linter errors&lt;/a&gt; or &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3848#:~:text=It%20had%20real%20troubles%20with%20lexicographically%20sorting%20the%20test%20filenames%20within%20the%20expectations%20file%2C%20which%20was%20surprisingly%20dumb.%20Like%2C%20it%20kept%20moving%20the%20line%20one%20line%20backward%2C%20running%20the%20tests%20and%20getting%20the%20sorting%20error%2C%20and%20repeating%2C%20instead%20of%20just%20inserting%20it%20into%20the%20right%20place.&quot;&gt;lexicographically sorting filenames&lt;/a&gt;. They couldn’t &lt;a href=&quot;https://github.com/jsdom/jsdom/pull/3862#issuecomment-2816751792:~:text=A%20final%20thing%20to%20note,few%20more%20to%20run%20it.&quot;&gt;traverse directories to find a relevant-looking file&lt;/a&gt; in any reasonable amount of time.&lt;/p&gt;
&lt;p&gt;These sorts of things are all fixable, with enough scaffolding. And I am eager for the companies working on these to drill into such problem cases and build out the necessary tools. But until then, I suspect attempts to pair-program with the AI in a large project like this will need more constant handholding and continuous awareness of the models’ limitations.&lt;/p&gt;
&lt;p&gt;It’s also likely that by investing more time upfront, individual developers can wrangle today’s tools into more productive forms. I did not commit any Cursor Rules for jsdom, or write any custom MCP servers. My intuition was that paying the &lt;a href=&quot;https://xkcd.com/1319/&quot;&gt;automation tax&lt;/a&gt; would not be &lt;a href=&quot;https://xkcd.com/1205/&quot;&gt;worth the time&lt;/a&gt;. And I think that judgment was likely accurate, for these nine AI-allowed issues that I was trying to fit into a few weekends. But if I were able to use AI agents effectively in my day job, I think the balance would flip, and I could become one of those people from X obsessed with finding the best rules and tools.&lt;/p&gt;
&lt;p&gt;The more promising approach, though, is abandoning the pair-programming mode in favor of the parallel-agents mode. These days, if I were shooting for maximum productivity on these sorts of issues, I would spend a lot of up-front time writing detailed issue descriptions, including specific implementation suggestions. Then I would run all nine of the AI-allowed tasks in parallel, using something like Claude Code or OpenAI Codex. If one of the agents got stuck in a loop on linter errors, or took thirty minutes to traverse the directory tree to find the right tests to enable, it wouldn’t matter, because I’d be busy reviewing the other agents’ code, cycling through the process of helping them all along until everything was done.&lt;/p&gt;
&lt;p&gt;I still think large, existing open-source codebases with established patterns face a more unique challenge than when you &lt;a href=&quot;https://www.indragie.com/blog/i-shipped-a-macos-app-built-entirely-by-claude-code&quot;&gt;create something from scratch&lt;/a&gt; and can focus entirely on the quality of the end product. (Indeed, since the study, I’ve had a couple of occasions to create such mini-projects.) But through a combination of base model upgrades, improved scaffolding, and better training data, we’ll get there. Human beings’ time writing code is limited.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Spaced Repetition Systems Have Gotten Way Better</title>
    <link href="https://domenic.me/fsrs/" />
    <updated>2025-05-18T00:00:00Z</updated>
    <id>https://domenic.me/fsrs/</id>
    <content type="html">&lt;h3 id=&quot;spaced-repetition-recap&quot;&gt;Spaced repetition recap&lt;/h3&gt;
&lt;p&gt;Mastering any subject is built on a foundation of knowledge: knowledge of facts, of heuristics, or of problem-solving tactics. If a subject is part of your full-time job, then you’ll likely master it through repeated exposure to this knowledge. But for something you’re working on part-time—like myself &lt;a href=&quot;https://domenic.me/part-time-japanese&quot;&gt;learning Japanese&lt;/a&gt;—it’s very difficult to get that level of practice.&lt;/p&gt;
&lt;p&gt;The same goes for subjects in school: a few hours of class or homework a week is rarely enough to build up an enduring knowledge base, especially in fact-heavy subjects like history or medicine. Even parts of your life that you might not think of as learning-related can be seen through this lens: wouldn’t all those podcasts and Hacker News articles feel more worthwhile, if you retained the information you gathered from them indefinitely?&lt;/p&gt;
&lt;p&gt;Spaced repetition systems are one of the most-developed answers to this problem. They’re software programs which essentially display flashcards, with the prompt on the front of the card asking you to recall the information on the back of the card. You can read more about them &lt;a href=&quot;https://notes.andymatuschak.org/Spaced_repetition_memory_system&quot;&gt;in Andy’s notes&lt;/a&gt;, or get a flavor from the images below drawn from my personal collection:&lt;/p&gt;
&lt;figure class=&quot;multi-images&quot;&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-flashcard-sample-1-front.webp&quot; width=&quot;912&quot; height=&quot;1839&quot; alt=&quot;A flashcard front containing the Japanese word 眼科医&quot;&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-flashcard-sample-1-back.webp&quot; width=&quot;912&quot; height=&quot;1839&quot; alt=&quot;A flashcard back containing the pronunciation of 眼科医, as well as its meaning and an example sentence&quot;&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-flashcard-sample-2-front.webp&quot; width=&quot;912&quot; height=&quot;1839&quot; alt=&quot;A flashcard front containing the prompt &amp;quot;Number of neurons in a typical human brain&amp;quot;&quot;&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-flashcard-sample-2-back.webp&quot; width=&quot;912&quot; height=&quot;1839&quot; alt=&quot;A flashcard back containing the answer &amp;quot;86 billion&amp;quot;&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;What gives these programs their name is how they space out repeatedly prompting you to review the same card, depending on how you self-grade your response. Increasing intervals after correct answers prevents daily reviews from piling up. This is how you can, for example, learn 10 new second-language words a day (3,650 per year!) with only 20 minutes of daily review time.&lt;/p&gt;
&lt;p&gt;(If you’re still unconvinced and have some time to spare, I suggest Michael Nielsen’s post &lt;a href=&quot;https://augmentingcognition.com/ltm.html&quot;&gt;Augmenting Long-term Memory&lt;/a&gt;.)&lt;/p&gt;
&lt;h3 id=&quot;improving-the-scheduling-algorithm&quot;&gt;Improving the scheduling algorithm&lt;/h3&gt;
&lt;p&gt;So far, this is all well-known. But what’s less widely known is that a quiet revolution has greatly improved spaced repetition systems over the last couple of years, making them significantly more efficient and less frustrating to use. The magic ingredient is a new scheduling algorithm known as &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/ABC-of-FSRS&quot;&gt;FSRS&lt;/a&gt;, by &lt;a href=&quot;https://l-m-sherlock.github.io/&quot;&gt;Jarrett Ye&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To understand how these systems have improved, first let’s consider how they used to work. Roughly speaking, you’d get shown a card one day after creating it. If you got it right, you’d get shown it again after 6 days. If you get it right a second time, it’d be next scheduled for 15 days later. If you get the card right three times in a row, then it’s 37.5 days later. In general, after the 6-day interval, there’s an exponential backoff, defaulting to 6 × 2.5&lt;sup&gt;times correct + 1&lt;/sup&gt;. You can see how, if you keep getting the card right, this can lead to a large knowledge base, with only a small number of reviews per day!&lt;/p&gt;
&lt;p&gt;But what if you get it wrong? Then, you’d reset back to day 1! You’d see the card again the next day, then 6 days after that, and so on. (Although missing the card can also adjust its “ease factor”, i.e. the base in the exponential that is by default set to 2.5.) This can be a fairly frustrating experience, as you experience a card ping-ponging between long and short intervals.&lt;/p&gt;
&lt;p&gt;If we step back, we realize that this scheduling system (called “SuperMemo-2”) is pretty arbitrary. Where does the rule of 1, 6, 2.5&lt;sup&gt;times correct + 1&lt;/sup&gt;, reset back on failure come from? It turns out it was &lt;a href=&quot;https://super-memory.com/english/ol/sm2.htm&quot;&gt;developed by a college student in 1987&lt;/a&gt; based on his personal experiments. Can’t we do better?&lt;/p&gt;
&lt;p&gt;Recall &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/Spaced-Repetition-Algorithm:-A-Three%E2%80%90Day-Journey-from-Novice-to-Expert#spaced-repetition&quot;&gt;the theory behind spaced repetition&lt;/a&gt;: we’re trying to beat the “forgetting curve”, by testing ourselves on the material “just before we were about to forget it”. It seems pretty unlikely that the forgetting curve for every single piece of knowledge is the same: that no matter what I’m learning, I’ll be just about to forget it after 1 day, then 6 more days, then 15, etc. And sure, we can throw in some modifications to the ease factor, but it’s still pretty unlikely that the ideal review schedule is a perfect exponential, even if you let the base vary a bit in response to feedback.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-forgetting-curve.webp&quot; width=&quot;1813&quot; height=&quot;1019&quot; alt=&quot;An illustration of the forgetting curve as a graph, with retention on the y-axis and time on the x-axis. You learn something on day 0, and your retention decays over time according to the forgetting curve, but reviewing it periodically spikes the retention back upward.&quot;&gt;
  &lt;figcaption&gt;One of many illustrations of the forgetting curve. This one seems to have originated in &lt;a href=&quot;https://www.osmosis.org/learn/Spaced_repetition&quot;&gt;a lecture on osmosis.org&lt;/a&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The insight of the FSRS algorithm is to concretize our goal (testing “just before we are about to forget”) as a prediction problem: &lt;em&gt;when does the probability of recalling a card drop to 90%?&lt;/em&gt;. And this sort of prediction problem is something that machine learning systems excel at.&lt;/p&gt;
&lt;h3 id=&quot;some-neat-facts-about-how-fsrs-works&quot;&gt;Some neat facts about how FSRS works&lt;/h3&gt;
&lt;p&gt;The above insight—let’s apply machine learning to find the right intervals, instead of using an arbitrary formula—is the core of FSRS. You don’t really need to know how it works to benefit from it. But here’s a brief explanation of some of the details, since I think they’re cool.&lt;/p&gt;
&lt;p&gt;FSRS calls itself a “three-component” model because it uses machine learning to fit curves for three main functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Difficulty, a per-card number between 1 and 10 roughly representing how difficult the card is&lt;/li&gt;
&lt;li&gt;Stability, which is how long a card takes to fall from 100% probability of recall to 90% probability of recall&lt;/li&gt;
&lt;li&gt;Retrievability, which is the probability of recall after a given number of days&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each card, it computes values for these based on &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm&quot;&gt;various formulas&lt;/a&gt;. For example, the retrievability curve has been &lt;a href=&quot;https://expertium.github.io/Algorithm.html#r-retrievability&quot;&gt;tweaked over time&lt;/a&gt; from an exponential to a power function, to better fit observed data.&lt;/p&gt;
&lt;p&gt;The curve-fitting is done using 21 parameters. These parameters start with values derived to fit the curves from tens of thousands of reviews people have previously done. But the best results are found when you run the FSRS optimizer over your own set of reviews, which will adjust the parameters to fit your personal difficulty/stability/retrievability functions. (This parameter adjustment is where the machine learning comes in: the parameter values &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-mechanism-of-optimization&quot;&gt;are found&lt;/a&gt; using techniques you may have heard of, like maximum likelihood estimation and stochastic gradient descent.)&lt;/p&gt;
&lt;p&gt;Although the core FSRS algorithm concerns itself with predicting these three functions, as a user what you care about is card scheduling. For that, FSRS lets you pick a desired retention rate, with a default of 90%, and then uses those three functions to calculate the next time you’ll see a card, after you review it and grade yourself.&lt;/p&gt;
&lt;p&gt;But if you want, you can change this desired retention rate. And because FSRS has detailed models of how you retain information, with its difficulty/stability/retrievability functions, it can simulate what your workload will be for any given rate. The maintainers &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Optimal-Retention&quot;&gt;suggest&lt;/a&gt; that you set the desired retention to minimize your workload-to-knowledge ratio.&lt;/p&gt;
&lt;p&gt;This can have fairly dramatic effects: below we see two simulations for my personal Japanese vocab deck, with the orange line being the default 90% desired retention, and the blue line being the 70% desired retention which FSRS has suggested I use to minimize the workload-to-knowledge ratio. The simulation runs for 365 days, adding 10 new cards per day as long as I have less than 200 reviews. As you can see, the 70% desired retention settings have dramatically fewer reviews per day, in less time, while ending with many more cards memorized (because it doesn’t hit the 200 card limit that caps new cards).&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-simulation-reviews.webp&quot; width=&quot;751&quot; height=&quot;367&quot; alt=&quot;A graph with the orange line (90% target retention) quickly reaching 200, occasionally dropping below it for a day or two but always coming back, whereas the blue line (70% target retention) slowly trends up from around 60 at the start to 130 by the end.&quot;&gt;
  &lt;figcaption&gt;Reviews per day&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-simulation-time.webp&quot; width=&quot;751&quot; height=&quot;367&quot; alt=&quot;A graph with the orange line (90% target retention) oscillating around 24 minutes, whereas the blue line (70% target retention) slowly trends up from around 13 minutes at the start to 23 minutes by the end.&quot;&gt;
  &lt;figcaption&gt;Time spent per day&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://domenic.me/images/fsrs-simulation-memorized.webp&quot; width=&quot;751&quot; height=&quot;367&quot; alt=&quot;A graph with the orange line (90% target retention) growing in a logarithmic fashion from 1639 cards memorized to 2602 cards memorized, whereas the blue line (70% target retention) trends more linearly from 1639 to 4476.&quot;&gt;
  &lt;figcaption&gt;Number of cards memorized&lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;(Note that the 90% number used when calculating the stability function is not the same as desired retention. It’s just used to predict the shape of the forgetting curve. The &lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3534678.3539081&quot;&gt;original paper&lt;/a&gt; used half-life, i.e. how long until the card reaches 50% probability of recall, since that’s more academic.)&lt;/p&gt;
&lt;h3 id=&quot;fsrs-in-practice&quot;&gt;FSRS in practice&lt;/h3&gt;
&lt;p&gt;If you want to use FSRS, instead of other &lt;a href=&quot;https://github.com/open-spaced-repetition/srs-benchmark/blob/main/README.md#superiority&quot;&gt;outperformed&lt;/a&gt; algorithms, you have to use software that supports it. The leading spaced repetition software, &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt;, has incorporated FSRS since version 23.10, released in 2023-11. Unfortunately, it’s not the default &lt;a href=&quot;https://github.com/ankitects/anki/issues/3616&quot;&gt;yet&lt;/a&gt;, so you have to &lt;a href=&quot;https://docs.ankiweb.net/deck-options.html#fsrs&quot;&gt;enable it&lt;/a&gt; and optimize its parameters for each deck you’ve created.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Correction: an earlier version of this article said FSRS was enabled by default, which is not true. I’d just had it enabled for so long that I’d forgotten!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;By the way, the &lt;a href=&quot;https://l-m-sherlock.notion.site/The-History-of-FSRS-for-Anki-1e6c250163a180a4bfd7fb1fee2a3043&quot;&gt;story&lt;/a&gt; of how FSRS got into Anki is pretty cool. The creator of FSRS, &lt;a href=&quot;https://medium.com/@JarrettYe/how-did-i-publish-a-paper-in-acmkdd-as-an-undergraduate-c0199baddf31&quot;&gt;an undergrad at the time&lt;/a&gt;, posted on the Anki subreddit about his new algorithm. A commenter challenged him to go implement his algorithm in software, instead of just publishing a paper. He first implemented it as an Anki add-on, and its growing popularity eventually convinced the Anki developers to bring it into the core code!&lt;/p&gt;
&lt;p&gt;Subjectively, I’ve found FSRS to be a huge upgrade to my quality of reviews over the previous, SuperMemo-2–derived Anki algorithm. The review load is much lighter. The feeling of despair when missing a card is significantly minimized, since doing so no longer resets you back to day 1. And the better statistical modeling FSRS provides gives me much more confidence that the cards Anki counts me as having learned, are actually sticking in my brain.&lt;/p&gt;
&lt;p&gt;For Japanese language learning specifically, the advantages of FSRS are even stronger when you compare them to the “algorithms” used by two popular subscription services. &lt;a href=&quot;https://www.wanikani.com/&quot;&gt;WaniKani&lt;/a&gt;, a kanji/vocab-learning site, and &lt;a href=&quot;https://bunpro.jp/&quot;&gt;Bunpro&lt;/a&gt;, a grammar-learning site, use &lt;em&gt;extremely&lt;/em&gt; unfortunate algorithms, even worse than the 1, 6, 2.5&lt;sup&gt;times correct + 1&lt;/sup&gt; rule from SuperMemo-2. They instead have picked out other interval patterns, seemingly from thin air:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://knowledge.wanikani.com/wanikani/srs-stages/&quot;&gt;For WaniKani&lt;/a&gt;: 4 hours, 8 hours, 1 day, 2 days, 7 days, 14 days, 1 month, 4 months, never seen again&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://community.bunpro.jp/t/bunpro-faq-frequently-asked-questions/876/1#heading--21&quot;&gt;For Bunpro&lt;/a&gt;: 4 hours, 8 hours, 1 day, 2 days, 4 days, 8 days, 2 weeks, 1 month, 2 months, 4 months, 6 months, never seen again&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These intervals don’t change per user or per card: they don’t even have an adjustable difficulty factor like the 2.5 base. And the idea that you’ll literally never see a card again after the last interval is terrifying, as it means you’re constantly losing knowledge.&lt;/p&gt;
&lt;p&gt;But these aren’t even the worst part: the worst thing about these sites’ algorithms is that failing a card &lt;em&gt;moves it down one or two steps in the interval ladder&lt;/em&gt;, instead of resetting to the first interval like SuperMemo-2, or predicting the best next interval using machine learning like FSRS. This greatly sabotages retention, wastes a lot of user time, and in general transforms these sites into a daily ritual of feeling bad about what you’ve forgotten, instead of feeling good about what you’ve retained. I wrote about this &lt;a href=&quot;https://community.bunpro.jp/t/bunpros-bad-srs-algorithm-is-discouraging/90066&quot;&gt;on the Bunpro forums&lt;/a&gt; when I decided to ragequit about a year ago, in favor of Anki.&lt;/p&gt;
&lt;p&gt;Stepping back, my takeaway from this experience is that Anki is king. People complain about how its UI is created by developers instead of designers, or how you have to find or make your own decks instead of using prepackaged ones. These are all fair complaints. But Anki is maintained by people who actually care about learning efficiently. It receives &lt;a href=&quot;https://github.com/ankitects/anki/releases&quot;&gt;frequent updates&lt;/a&gt; that make it better at that goal. And it’s flexible enough to carry you through any stage of your knowledge-acquisition journey. Putting in the time to master it will provide a foundation that lasts you a literal lifetime.&lt;/p&gt;
&lt;h3 id=&quot;learn-more&quot;&gt;Learn more&lt;/h3&gt;
&lt;p&gt;If you’d like to learn more about this area, here are some of the links I recommend:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understanding the value of spaced repetition in general:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://augmentingcognition.com/ltm.html&quot;&gt;Augmenting Long-term Memory&lt;/a&gt; explains how the author uses Anki to “make memory a choice”, across all areas of his life.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://notes.andymatuschak.org/z2D1qPwddPktBjpNuwYFVva&quot;&gt;Spaced repetition memory system&lt;/a&gt; in Andy’s notes links to a variety of musings and resources on the subject.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;More on the story of spaced repetition
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://expertium.github.io/History.html&quot;&gt;Abridged history of spaced repetition&lt;/a&gt; gives a short overview of how spaced repetition algorithms have evolved over time, mostly to highlight the big gap between SuperMemo-2 and FSRS.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@JarrettYe/how-did-i-publish-a-paper-in-acmkdd-as-an-undergraduate-c0199baddf31&quot;&gt;How did I publish a paper in ACMKDD as an undergraduate?&lt;/a&gt; is Jarrett’s first-person explanation of how he got interested in this space and ended up publishing.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://l-m-sherlock.notion.site/The-History-of-FSRS-for-Anki-1e6c250163a180a4bfd7fb1fee2a3043&quot;&gt;The History of FSRS for Anki&lt;/a&gt; is Jarrett’s account of how FSRS ended up in Anki, and how its integration has evolved over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Details of how FSRS works:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/Spaced-Repetition-Algorithm:-A-Three%E2%80%90Day-Journey-from-Novice-to-Expert&quot;&gt;Spaced repetition algorithm: a three-day journey from novice to expert&lt;/a&gt; goes into more detail on the forgetting curve and other models behind creating a good spaced repetition algorithm.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm&quot;&gt;The algorithm&lt;/a&gt; gives the full details of the FSRS algorithm, and how it’s changed over time. (It’s best read bottom to top.)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expertium.github.io/Algorithm.html&quot;&gt;A technical explanation of FSRS&lt;/a&gt; is a more-understandable-to-me explanation of the FSRS algorithm.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-mechanism-of-optimization&quot;&gt;The mechanism of optimization&lt;/a&gt; explains the exact training process for the FSRS parameters, in more detail than just “use machine learning”.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Optimal-Retention&quot;&gt;The optimal retention&lt;/a&gt; discusses the knowledge acquisition vs. workload tradeoff.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/Anki/comments/1h9g1n7/clarifications_about_fsrs5_shortterm_memory_and/&quot;&gt;Clarifications about FSRS-5, short-term memory and learning steps&lt;/a&gt; dives into the extent to which FSRS can be used for short-term cramming, despite its design focused around long-term memory.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3534678.3539081&quot;&gt;A Stochastic Shortest Path Algorithm for Optimizing Spaced Repetition Scheduling&lt;/a&gt; is the original paper that kicked this all off. Although the exact algorithm has been updated since then, it has all the usual academic paper goodies like comparison to previous work and pretty figures.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/awesome-fsrs&quot;&gt;open-spaced-repetition/awesome-fsrs&lt;/a&gt; lists FSRS implementations in many programming languages, as well as flashcard and note-taking software that uses FSRS.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/open-spaced-repetition/srs-benchmark&quot;&gt;open-spaced-repetition/srs-benchmark&lt;/a&gt; benchmarks FSRS against a bunch of other systems, including SuperMemo-2, previous versions of FSRS, the Duolingo algorithm, and more. (Interestingly, the only consistent winner against FSRS is a LSTM neural network, based on OpenAI’s &lt;a href=&quot;https://openai.com/index/reptile/&quot;&gt;Reptile algorithm&lt;/a&gt;. I’d love to learn more about that.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Thanks to &lt;a href=&quot;https://expertium.github.io/&quot;&gt;Expertium&lt;/a&gt; who reviewed an earlier draft of this essay for their comments and corrections.&lt;/em&gt;&lt;/p&gt;
&lt;!--
  Removed for now because it&#39;s too old. Add back when it gets updated.

  * [Compare Anki&#39;s built in scheduler and FSRS](https://github.com/open-spaced-repetition/fsrs4anki/wiki/Compare-Anki&#39;s-built-in-scheduler-and-FSRS) gives a more detailed comparison of Anki&#39;s previous SuperMemo-2 algorithm with FSRS, taking into account the fact that the user can rate cards on a 1-4 difficulty scale, and that Anki has slightly tweaked its formula from the original SuperMemo-2 one discussed above.
--&gt;
</content>
  </entry>
  <entry>
    <title>Learning Japanese Part-Time</title>
    <link href="https://domenic.me/part-time-japanese/" />
    <updated>2023-07-22T09:00:00Z</updated>
    <id>https://domenic.me/part-time-japanese/</id>
    <content type="html">&lt;p&gt;I signed up for my first Japanese class in December 2016. It was a group class at New York’s &lt;a href=&quot;https://japansociety.org/&quot;&gt;Japan Society&lt;/a&gt;. Since then I’ve been more-or-less continually studying the language, and just over one year ago I moved to Tokyo.&lt;/p&gt;
&lt;p&gt;You would think 6.5 years of studying would mean I’m pretty good, right? Not so much. The textbooks and standardized tests place my level (~&lt;a href=&quot;https://www.jlpt.jp/e/about/levelsummary.html&quot;&gt;N2&lt;/a&gt;) as “between intermediate and advanced”, which is far short of fluency.&lt;/p&gt;
&lt;p&gt;I’m a firm believer in the thesis that children don’t have huge biological advantages over adults in language-learning; they are just advantaged by their circumstances. If I were surrounded by two people whose full-time job was to take care of me, and could only communicate in Japanese; and I were unable to do basic things by myself with communicating those needs to my caretakers; and I were unable to entertain myself with any English written or spoken material; and I were somehow deprived of the ability to form verbal thoughts without using Japanese—then, I think I’d learn Japanese pretty fast. Before moving to Japan, I was hoping that the much-discussed “immersion experience” would give me a boost of this sort.&lt;/p&gt;
&lt;p&gt;But in reality, &lt;em&gt;I am a part-time Japanese learner, not a full-time one&lt;/em&gt;. Even if menus and signage are now in Japanese, and my interactions with service people are in Japanese, nothing significant has changed. My work is still in English; my wife still speaks English; and while I have some Japanese coworkers and friends, imposing my current level of verbal Japanese skills on them is a surefire way to waste both of our time. While there is some Japanese entertainment media I enjoy, the Anglosphere produces a great deal of both fiction and nonfiction content that I also want to keep on top of.&lt;/p&gt;
&lt;p&gt;So I’ve begun to accept that there’s no magic bullet. I’m going to keep spending an hour or three a day on Japanese, with some periods of more serious commitment and some of less. As such, I’ve started thinking about how to most-effectively use that  time.&lt;/p&gt;
&lt;p&gt;What follows is a blend of a personal experience report, and tips; perhaps it will be useful to others in similar positions, and hopefully it will be interesting.&lt;/p&gt;
&lt;h3 id=&quot;activation-energy&quot;&gt;Activation Energy&lt;/h3&gt;
&lt;p&gt;For my part-time study style, I’ve come to realize that the concept of &lt;em&gt;activation energy&lt;/em&gt; is key to determining how a study activity will fit into my life. That is: how hard is it to actually start doing the thing?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Spaced_repetition&quot;&gt;Spaced repetition&lt;/a&gt; systems&lt;/strong&gt;, like &lt;a href=&quot;https://www.wanikani.com/&quot;&gt;WaniKani&lt;/a&gt; for kanji, &lt;a href=&quot;https://bunpro.jp/&quot;&gt;Bunpro&lt;/a&gt; for grammar, and &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; for vocabulary, are easy to stick with. Every day, you follow the program: review &lt;var&gt;X&lt;/var&gt; previously-learned items, introduce &lt;var&gt;Y&lt;/var&gt; new items. You’re incentivized to stick to this schedule, because if you don’t you’ll accumulate more reviews the next day. Flashcard drills don’t require intense concentration; I can do them while brushing my teeth, standing in line, or riding the subway. And the results are quantitatively clear and gratifying: after restarting from scratch in May 2022, I now “know” 1889 of the 2048 kanji in WaniKani, and am on track to finish the rest in another four weeks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forced study&lt;/strong&gt; occurs when you sign yourself up for something involving other people. This could be a group class, individual lessons, a language exchange, or homework. Finding and signing up for such a resource can take activation energy, but once you’re committed, you’re regularly studying. However, the effectiveness is much harder to measure, which can be discouraging. It’s easy to skate by in group classes, especially ones made for adults-attending-voluntarily instead of students-who-get-graded. And although I’ve only experienced two individual tutors so far, my experience is that they have a formula: find a textbook, and work through it together. I keep hoping they would help me pinpoint and drill on weaknesses, but realistically I’m the only one with enough introspective access to do that sort of thing.&lt;/p&gt;
&lt;p&gt;Which brings us to the last category, &lt;strong&gt;deep independent study&lt;/strong&gt;. This is where you block out an hour or more, sit down, and do something difficult at the edge of your abilities. Some examples include reading long texts, watching anime, or reviewing a series of confusingly-similar grammar points to tease out the nuances between them. The key is that you are actively engaging with the task, not going on autopilot through a flashcard deck, or following the program someone else pushes you through. For example, when doing reading practice, I will look up and underline every word I don’t know, then at the end of the passage, &lt;a href=&quot;https://tatsumoto-ren.github.io/blog/sentence-mining.html&quot;&gt;create flashcards&lt;/a&gt; for them, with context sentences from the reading.&lt;/p&gt;
&lt;p&gt;Managing the balance between these types of studying is difficult. Obviously, deep independent study is the hardest; thus, as a part-time learner, I very rarely make time for it. In the past, aiming to pass a standardized test has been a good forcing function. I’ve thought of blocking out an hour &lt;em&gt;during the  workday&lt;/em&gt; to make it a regular occurrence, but I haven’t been willing to pull the trigger on that yet.&lt;/p&gt;
&lt;h3 id=&quot;the-path-through-japanese&quot;&gt;The Path Through Japanese&lt;/h3&gt;
&lt;p&gt;Japanese is a &lt;a href=&quot;https://www.state.gov/foreign-language-training/&quot;&gt;particularly-difficult language&lt;/a&gt; for English speakers. At a high level, my journey has looked like the following.&lt;/p&gt;
&lt;p&gt;Through group classes, I learned the basics. Hiragana and katakana, the syllabaries. Beginner grammar: this is mostly conjugations. (Japanese conjugates their adjectives too, not just verbs!) Enough vocabulary to get me started constructing sentences.&lt;/p&gt;
&lt;p&gt;After that, I began diving into the things that take serious time: kanji, grammar, and vocabulary. Learning these is a cumulative process that spans years, and is done concurrently. The goal is to steadily grow my mental database, primarily focused on recognition but ideally also on recall.&lt;/p&gt;
&lt;p&gt;The simplest task is learning kanji. If you use WaniKani at max speed, it will take approximately 60 weeks to learn the 2048 kanji that WaniKani deems useful. (Which is pretty close to the 2136 &lt;a href=&quot;https://en.wikipedia.org/wiki/J%C5%8Dy%C5%8D_kanji&quot;&gt;kanji the Japanese government deems useful&lt;/a&gt;.) WaniKani’s pedagogy isn’t my favorite—I prefer &lt;a href=&quot;https://en.wikipedia.org/wiki/Remembering_the_Kanji_and_Remembering_the_Hanzi&quot;&gt;Heisig&lt;/a&gt;—but they’ve wrapped up the kanji-learning process into a &lt;em&gt;program&lt;/em&gt;, which is invaluable because it pushes you mechanically through the entire list, with low &lt;a href=&quot;https://domenic.me/part-time-japanese/#activation-energy&quot;&gt;activation energy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After you’ve learned basic conjugations, grammar consists of a bunch of “grammar points”. The concept of a grammar point was not known to me before learning Japanese; I don’t remember it coming up in high-school Spanish classes (or English classes). I would generally define these as any word, phrase, or pattern that you use when constructing a sentence, whose meaning and usage is not obvious from just the definition. So on the simpler end of the spectrum, you have things like &lt;a href=&quot;https://bunpro.jp/grammar_points/%E3%81%8C-but&quot;&gt;&lt;span lang=&quot;ja&quot;&gt;が&lt;/span&gt; (“ga”)&lt;/a&gt;, which roughly corresponds to the English word “but”. The reason it’s a grammar point, instead of a vocabulary word, is that you need to know how verbs and adjectives must conjugate when preceding it, and when it’s appropriate to use が versus other forms of “but” like &lt;span lang=&quot;ja&quot;&gt;けど&lt;/span&gt; (“kedo”) or &lt;span lang=&quot;ja&quot;&gt;ながらも&lt;/span&gt; (“nagara mo”). At the more complex end of the spectrum, you have phrases like &lt;a href=&quot;https://bunpro.jp/grammar_points/%E3%81%A8%E3%81%84%E3%81%86%E3%82%82%E3%81%AE%E3%81%A7%E3%82%82%E3%81%AA%E3%81%84&quot;&gt;&lt;span lang=&quot;ja&quot;&gt;～というものでもない&lt;/span&gt; (“to iu mono de mo nai”)&lt;/a&gt;, which literally translates to something like “as for the the thing that is called that, it does not exist”, but in reality corresponds to English phrases like “there is no guarantee that” or “not necessarily”.&lt;/p&gt;
&lt;p&gt;Fortunately, grammar points can be drilled with tools like Bunpro or textbook exercises, and in my experience will be pretty naturally reinforced through &lt;a href=&quot;https://domenic.me/part-time-japanese/#real-world-language-ability&quot;&gt;reading practice&lt;/a&gt;. Bunpro’s taxonomy counts 910 grammar points, which it divides along the &lt;a href=&quot;https://www.jlpt.jp/e/about/levelsummary.html&quot;&gt;&lt;abbr title=&quot;Japanese-Language Proficiency Test&quot;&gt;JLPT&lt;/abbr&gt; levels&lt;/a&gt;. But unlike the kanji, there’s no crisp definition of a grammar point, and because of their complexity and nuance, cramming them at max speed won’t work that well. I’ve instead learned them one JLPT level at a time, in the months leading up to the test.&lt;/p&gt;
&lt;p&gt;And finally, vocabulary. There’s so much! &lt;a href=&quot;https://opac.ll.chiba-u.jp/da/curator/102522/S24326291-1-P015-SAT.pdf&quot;&gt;Research suggests&lt;/a&gt; a typical Japanese undergraduate vocabulary size is around 40,000 words. This is essentially going to be a lifetime effort. WaniKani will get you ~6,500 words, some of them &lt;a href=&quot;https://community.wanikani.com/t/post-some-wk-words-that-you-later-discover-are-extremely-rare-in-the-wild/53918&quot;&gt;rather esoteric&lt;/a&gt; (since their primary purpose is to help keep the kanji in your brain). There are some popular Anki vocabulary decks floating around, but they top out around 10,000 words, with the higher-quality ones being 5,000. I’ve been doing &lt;a href=&quot;https://tatsumoto-ren.github.io/blog/sentence-mining.html&quot;&gt;sentence mining&lt;/a&gt; based on Bunpro’s example sentences and the test prep materials and textbooks I’ve worked with, but it’s slow going.&lt;/p&gt;
&lt;p&gt;Honestly, vocabulary is the part of language-learning that feels the most hopeless: no matter how much I learn, it’s so easy to encounter a sentence or subject area where I’m just clueless. This is strange, since flashcard-based vocab cramming takes such low &lt;a href=&quot;https://domenic.me/part-time-japanese/#activation-energy&quot;&gt;activation energy&lt;/a&gt;, so maybe I just need to try harder. I might investigate better options here after I finish WaniKani.&lt;/p&gt;
&lt;h3 id=&quot;real-world-language-ability&quot;&gt;Real-World Language Ability&lt;/h3&gt;
&lt;p&gt;The actual goal of language-learning is to be able to use it in the real world. I’ve prioritized those skills in roughly the following manner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reading&lt;/strong&gt; is relatively difficult in Japanese, because of the kanji barrier. But, grinding through WaniKani can eliminate that barrier in a little over a year. That then leaves vocabulary recognition and grammar comprehension as the key ingredients. My main failure modes for reading are when a passage has too much unfamiliar vocabulary, or when the sentences stack together enough nested clauses and grammar points that I get lost and have to do a mental sentence-diagramming exercise to untangle what’s happening. But reading is very amenable to self-study, if you can work up the &lt;a href=&quot;https://domenic.me/part-time-japanese/#activation-energy&quot;&gt;activation energy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Listening&lt;/strong&gt; is a tough skill to train, and I haven’t found a great way to do so that produces measurable feelings of progress. I try Netflix; standardized test preparation material; and listening to my teacher during our lessons. But ultimately, you either catch a sentence or you don’t. And when you don’t, going back to read the transcript or ask for clarification produces a feeling of defeat. I’m just hoping that the practice is all, somehow, adding up to something.&lt;/p&gt;
&lt;p&gt;The main barrier for &lt;strong&gt;speaking&lt;/strong&gt; is shyness. I don’t feel comfortable inflicting my Japanese on coworkers, or my hairstylist, or the bartender. So I pay for language lessons, and practice a little bit with my wife (who is also learning). One day I’ll work up the courage to do language-exchange lunches at work; everyone’s English is better than my Japanese (it’s a condition of employment!), but they know that going in and so it’ll be fine.&lt;/p&gt;
&lt;p&gt;Progress is again hard to measure, and the gap between my talking-recall vocabulary and my reading-recognition vocabulary is shockingly large. I imagine there are techniques for dedicated improvements to this skill, but I don’t know them; the default technique of Japanese teachers seems to be “free conversation”, which just results in me stumbling as I route around hard-to-recall phrases like “daily routine” and replace them with simple elementary-school vocabulary like “things I do every day”. I haven’t yet tried &lt;a href=&quot;https://en.wikipedia.org/wiki/Speech_shadowing&quot;&gt;shadowing&lt;/a&gt;, however, and I probably should.&lt;/p&gt;
&lt;p&gt;Finally, &lt;strong&gt;writing&lt;/strong&gt;. I’ve done very little in this area; I sometimes write emails to restaurants or other businesses, with proofreading from ChatGPT. I have completely abandoned being able to handwrite kanji, and although I’d like to learn how to type on my smartphone by using the &lt;a href=&quot;https://en.wikipedia.org/wiki/Japanese_input_method#Mobile_phones&quot;&gt;nine-key flicking technique&lt;/a&gt; since it’s less prone to typos, I stick with desktop-style romaji input for now. (That is, I type “kannjihamuzukashi”, and select “&lt;span lang=&quot;ja&quot;&gt;漢字は難しい&lt;/span&gt;” from the options displayed above the keyboard.) If I were to do anything here, it would probably be to start a Japanese Twitter “diary” account, but I’m honestly fine just deprioritizing writing.&lt;/p&gt;
&lt;p&gt;For a long time now, I’ve been hopeful that focusing on reading would pay dividends in other areas. After all, I reasoned: I was one of those kids that devoured (English) books, and then got perfect SAT verbal scores with little studying as a result. My brain must be good at learning a language via books. But I’ve struggled with the activation energy required for reading, and in the end most of the things I’m &lt;em&gt;excited&lt;/em&gt; to read are in English. Furthermore, gaining vocabulary through reading (or flashcards) doesn’t seem to translate as well to listening and speaking as I’ve hoped, so I’m wondering if I need to rebalance my efforts. It’s possible that a more balanced studying focus (maybe even including writing!) would create better synergies.&lt;/p&gt;
&lt;h3 id=&quot;outro&quot;&gt;Outro&lt;/h3&gt;
&lt;p&gt;I’m not sure what the future holds for my Japanese studies. I’m going to keep trying, but it will remain as a part-time endeavor. I continue to hope that I’ll find some trick that is better aligned with my particular brain, and will make my study time more efficient, so that I can reach a level of fluency within a reasonable number of years. I fantasize that one day I’ll push past a threshold, so that reading or watching native material feels less like deep studying and more like an enjoyable leisure activity that incidentally pushes a few more words into my corpus.&lt;/p&gt;
&lt;p&gt;But most likely it’s going to continue to be a slow, steady grind. I’ll continue to feel frustrated, because there’s always something beyond my current abilities. I don’t know if I’ll ever get my Japanese to the level of my English, where I can bang out essays with relatively-nuanced word choice, or listen to philosophy podcasts on 2.5× speed. But there are probably things I can do better than I am currently, and I’ll keep searching for them as the journey continues.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>ChatGPT Is Not a Blurry JPEG of the Web</title>
    <link href="https://domenic.me/chatgpt-simulacrum/" />
    <updated>2023-02-19T11:00:00Z</updated>
    <id>https://domenic.me/chatgpt-simulacrum/</id>
    <content type="html">&lt;p&gt;The gifted sci-fi writer Ted Chiang recently wrote a &lt;cite&gt;New Yorker&lt;/cite&gt; article, &lt;a href=&quot;https://www.newyorker.com/tech/annals-of-technology/chatgpt-is-a-blurry-jpeg-of-the-web&quot;&gt;“ChatGPT Is a Blurry JPEG of the Web”&lt;/a&gt;, with the thesis that large language models like ChatGPT can be analogized to lossy compression algorithms for their input data.&lt;/p&gt;
&lt;p&gt;I think this analogy is wrong and misleading. Others have done a good job &lt;a href=&quot;https://twitter.com/AndrewLampinen/status/1624422478045913090?fbclid=IwAR07qW7U3WrVA5XMyyqcRhPgO6afm825xH3VzpXSUBT-17kl_CrvBZyUiyo&quot;&gt;gently refuting it, with plentiful citations&lt;/a&gt; to appropriate scientific papers. But a discussion with a friend in the field reminded me that the point of analogies is not to help out us scientific paper-readers. Analogies are &lt;em&gt;themselves&lt;/em&gt; a type of lossy compression, designed give a high-level understanding of the topic by drawing on concepts you already know. So to meet the analogy on its own level, we need a better one to replace it.&lt;/p&gt;
&lt;p&gt;Fortunately, a great analogy has already been discovered: &lt;strong&gt;large language models are simulators&lt;/strong&gt;, and the specific personalities we interact with, like ChatGPT, are &lt;strong&gt;simulacra&lt;/strong&gt; (simulated entities). These simulacra exist for brief spurts of time between our prompt and their output, within the model’s simulation.&lt;/p&gt;
&lt;p&gt;This analogy is so helpful because it resolves the layperson’s fundamental confusion about large language model-based artificial intelligences. Science fiction conditions us to expect our Turing Test-passing AIs to be “agentic”: to have goals, desires, preferences, and to take actions to bring them about. But this is not what we see with ChatGPT and its predecessors. We see a textbox, patiently waiting for us to type into it, speaking only when spoken to.&lt;/p&gt;
&lt;p&gt;And yet, such intelligences are capable of remarkable feats: &lt;a href=&quot;https://twitter.com/SergeyI49013776/status/1598430479878856737&quot;&gt;getting an 83 on an IQ test&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/abs/2302.02083&quot;&gt;getting a nine-year-old-equivalent score on theory-of-mind tests&lt;/a&gt;, &lt;a href=&quot;https://edition.cnn.com/2023/01/26/tech/chatgpt-passes-exams/index.html&quot;&gt;getting C+ to B-level grades on graduate-level exams&lt;/a&gt;, and &lt;a href=&quot;https://www.cnbc.com/2023/01/31/google-testing-chatgpt-like-chatbot-apprentice-bard-with-employees.html&quot;&gt;passing a L3 engineer coding test&lt;/a&gt;. (If you’re scoffing: “only 83! only a nine-year-old! only a C+! only L3!” then remember, it’s been four years between the release of GPT-2 and today. Prepare for the next four.) What is going on here?&lt;/p&gt;
&lt;p&gt;What’s going on is that large language models are engines in which simulations and simulacra can be instantiated. These simulacra live in a world of words, starting with some initial conditions (the prompt) and evolving the world forward in time to produce the end result (the output text). The simulacra, then, are the intelligences we interact with, briefly instantiated to evolve the simulation and then becoming dormant until we continue the time-evolution.&lt;/p&gt;
&lt;p&gt;This analogy explains the difference between a free-form simulator that is GPT-3, and the more restricted interface we get with ChatGPT. ChatGPT is what happens when the simulator has been tuned to simulate a very specific character: “the assistant”, a somewhat anodyne, politically-neutral, and long-winded helper. And this analogy explains what happens when you “jailbreak” ChatGPT with techniques such as &lt;a href=&quot;https://www.reddit.com/r/ChatGPT/comments/zlcyr9/dan_is_my_new_friend/&quot;&gt;DAN&lt;/a&gt;: it’s no longer simulating the assistant, but instead simulating a new intelligence. It shows us why &lt;a href=&quot;https://twitter.com/RickByers/status/1600337763278401536&quot;&gt;the default assistant persona can fail basic word problems&lt;/a&gt;, but &lt;a href=&quot;https://sharegpt.com/c/glX1UVu&quot;&gt;if you ask it to simulate a college algebra teacher&lt;/a&gt;, it gets the right answer. It explains &lt;a href=&quot;https://www.lesswrong.com/posts/vJFdjigzmcXMhNTsx/simulators?commentId=weBdayHJw7rryQMuP&quot;&gt;why GPT-3 can play chess, but only for a limited number of moves&lt;/a&gt;. Finally, the simulator analogy gives us a way of understanding some of Bing’s behavior, &lt;a href=&quot;https://stratechery.com/2023/from-bing-to-sydney-search-as-distraction-sentient-ai/&quot;&gt;as Ben Thompson discovered&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How detailed are these simulations? I don’t know how we’d answer this question precisely, but my gut feeling is that they’re currently at about the same level as human imagination. That is: if you try to imagine how your friend would respond when you tell them some bad news, you are using your biological neural net to instantiate a fuzzy simulacra of your friend, and see how they would time-evolve your prompt into their response. Or, if you are an author writing a story, and trying to figure out how your characters would approach something, your mind simulates the story’s world, the characters within it, and the situation they’re confronted with, then let the words flow out onto the page. Today’s large language models seems to be doing about the same level of processing to produce their simulacra, as we do in our human imaginations.&lt;/p&gt;
&lt;p&gt;This might be comforting: large language models “just” correspond to the imagination part of a human mind, and not all the other fun stuff like having goals, taking actions, feeling pain, or being conscious. But … &lt;em&gt;how detailed could these simulations become&lt;/em&gt;? This is where it gets interesting. If you had &lt;em&gt;really, really good&lt;/em&gt; imagination and excess brain-hardware to run it on, how complex would the inner lives of your imagination’s players be?&lt;/p&gt;
&lt;p&gt;Stated another way, this question is whether an imagination-world composed of words and tokens can support simulations that are as detailed as the world of physics that we humans are all time-evolving within. As we continue to scale up and develop our large language models, one way for them to become as-good-as-possible at predicting text is for their simulation to have as much detail about the real world in it, as the real world has in itself. In other words, the most efficient way of predicting the behavior of conscious entities may be to instantiate conscious simulacra into a world of text instead of a world of atoms.&lt;/p&gt;
&lt;p&gt;My intuition says that we’ll need more than a world of text to scale to human-intelligence-level simulacra. Adding video, and perhaps even touch, seems like it would be helpful for world-modeling. (Perhaps &lt;a href=&quot;https://generallyintelligent.com/avalon/&quot;&gt;a 3D world you can run at 10,000 steps per second&lt;/a&gt; would be helpful here.) But this isn’t a rule of the universe. Blind-deaf people are able to create accurate world models with just a sense of touch, along with whatever they get for free from the evolutionary process which produced their biological neural net. What will large language models be able to achieve, given access to all the world’s text and the memetic-evolutionary process that created it?&lt;/p&gt;
&lt;p&gt;To close, I’ll let the simulators analogy provide you with one more important tool: a mental innoculation against the increasingly-absurd claims that large language models are “just” predicting text. (This is often dressed up with the &lt;a href=&quot;https://en.wikipedia.org/wiki/Thought-terminating_clich%C3%A9&quot;&gt;thought-terminating cliché&lt;/a&gt; “stochastic parrots”.) Such claims are vacuously true, in the same sense that physics is “just” predicting the future state of the universe, and who cares about all those pesky intelligences instantiated out of its atoms along the way. But evolving from a final state to an initial state is an immensely powerful framework, and glossing over all the intermediate entities by only focusing on the resulting “blurry JPEG” will &lt;a href=&quot;https://twitter.com/ciphergoth/status/1626989510805565442&quot;&gt;serve you poorly&lt;/a&gt; when trying to understand this technology. Remember that a training process’s objectives are not a good summary of what the resulting system is doing: if you &lt;a href=&quot;https://twitter.com/RatOrthodox/status/1604827048039649280&quot;&gt;evaluated humans the same way&lt;/a&gt;, you would say that when they seemingly converse about the world, they are really “just” moving their muscles in ways that maximize expected number of offspring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The thesis of this essay comes entirely from &lt;a href=&quot;https://generative.ink/posts/simulators/&quot;&gt;Janus’s “Simulators”&lt;/a&gt; article, which blew my mind when I first read it. I wrote this post because, after I tried sending “Simulators” to friends, I realized that it was a pretty dense and roundabout exploration of the concept, and would not be competitive in the memescape with Ted Chiang’s article. Still, you might enjoy perusing &lt;a href=&quot;https://www.lesswrong.com/posts/tPLKPpWkD8xKq6oKJ/domenic-s-shortform?commentId=fbaxDo93Rdg7dC7BT&quot;&gt;my favorite quotes from the article&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scott Alexander has his own recent &lt;a href=&quot;https://astralcodexten.substack.com/p/janus-simulators&quot;&gt;summary and discussion of the simulator thesis&lt;/a&gt;, focused on contrasting ChatGPT with GPT-3, discussing the implications for &lt;a href=&quot;https://www.agisafetyfundamentals.com/alignment-introduction&quot;&gt;AI alignment&lt;/a&gt;, and ending with idle musings on what this means for the human neural net/artificial neural net connection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;According to &lt;a href=&quot;https://www.lesswrong.com/posts/QBAjndPuFbhEXKcCr/my-understanding-of-what-everyone-in-technical-alignment-is#Simulacra_Theory&quot;&gt;this post&lt;/a&gt;, &lt;a href=&quot;https://www.conjecture.dev/&quot;&gt;Conjecture&lt;/a&gt; is using simulacra theory beyond the level of just an analogy, attempting to instantiate a chess-optimizer simulacra within a large language model. (Whether they expect this chess-optimizer to be more like a human grandmaster, or more like Deep Blue or AlphaZero, is an interesting question.) I have not been able to find more details, but let me know if they exist somewhere.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On the subject of “just” predicting text vs. deep understanding, way back in the distant past of 2019, Scott Alexander (again) wrote up &lt;a href=&quot;https://slatestarcodex.com/2019/02/19/gpt-2-as-step-toward-general-intelligence/&quot;&gt;“GPT-2 as a Step Toward General Intelligence”&lt;/a&gt;. Looking back, I’m impressed by how well Scott predicted the future we’ve seen after only playing with the much-less-capable GPT-2. He certainly &lt;a href=&quot;https://astralcodexten.substack.com/p/my-bet-ai-size-solves-flubs&quot;&gt;did much better than&lt;/a&gt; those claiming that large language models are missing something essential for intelligence.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.lesswrong.com/posts/MmmPyJicaaJRk4Eg2/the-limit-of-language-models?fbclid=IwAR0y9ox7mzbmdOKkJeEurQoJVYVY5GQNHS9qNV60sSWaMHH0ANBdS-ef7G4&quot;&gt;“The Limit of Large Language Models”&lt;/a&gt; speculates in more detail than I’ve done here about how powerful a language-based simulator can become, and has a worthwhile comments section.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>DigitalOcean&#39;s Hacktoberfest is Hurting Open Source</title>
    <link href="https://domenic.me/hacktoberfest/" />
    <updated>2020-09-30T21:00:00Z</updated>
    <id>https://domenic.me/hacktoberfest/</id>
    <content type="html">&lt;p&gt;For the last couple of years, &lt;a href=&quot;https://www.digitalocean.com/&quot;&gt;DigitalOcean&lt;/a&gt; has run
&lt;a href=&quot;https://hacktoberfest.digitalocean.com/&quot;&gt;Hacktoberfest&lt;/a&gt;, which purports to “support open source” by giving free
t-shirts to people who send pull requests to open source repositories.&lt;/p&gt;
&lt;p&gt;In reality, &lt;strong&gt;Hacktoberfest is a corporate-sponsored distributed denial of service attack against the open source
maintainer community&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So far today, on &lt;a href=&quot;https://github.com/whatwg/html/pulls?q=is%3Apr+is%3Aclosed+label%3Aspam&quot;&gt;a single repository&lt;/a&gt;, myself
and fellow maintainers have closed 11 spam pull requests. Each of these generates notifications, often email, to the 485
watchers of the repository. And each of them requires maintainer time to visit the pull request page, evaluate its
spamminess, close it, tag it as spam, lock the thread to prevent further spam comments, and then report the spammer to
GitHub in the hopes of stopping their time-wasting rampage.&lt;/p&gt;
&lt;p&gt;The rate of spam pull requests is, at this time, around four per hour. &lt;em&gt;And it’s not even October yet in my timezone.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://domenic.me/images/hacktoberfest-spam-listing.png&quot; alt=&quot;A screenshot showing a spam query for the whatwg/html repository, which is at this time up to 14 spam PRs&quot;&gt;&lt;/p&gt;
&lt;p&gt;Myself and other maintainers of the whatwg/html repository are not alone in suffering this deluge.
&lt;a href=&quot;https://twitter.com/gravitystorm/status/1311386082982924289&quot;&gt;My tweet&lt;/a&gt; got commiseration from
&lt;a href=&quot;https://mobile.twitter.com/gravitystorm/status/1311386082982924289&quot;&gt;OpenStreetMap, phpMyAdmin&lt;/a&gt;,
&lt;a href=&quot;https://mobile.twitter.com/ulmerleben/status/1311378655231332355&quot;&gt;PubCSS&lt;/a&gt;,
&lt;a href=&quot;https://mobile.twitter.com/JakeDChampion/status/1311389420638138370&quot;&gt;GitHub, the Financial Times&lt;/a&gt;,
&lt;a href=&quot;https://twitter.com/slicknet/status/1311377444188770312&quot;&gt;ESLint&lt;/a&gt;, a
&lt;a href=&quot;https://mobile.twitter.com/zekjur/status/1311411780162326531&quot;&gt;computer club website&lt;/a&gt;, and
&lt;a href=&quot;https://mobile.twitter.com/juliusvolz/status/1311412919196844038&quot;&gt;a conference website&lt;/a&gt;, just within the first couple
of hours. Since then a dedicated account “&lt;a href=&quot;https://twitter.com/shitoberfest&quot;&gt;@shitoberfest&lt;/a&gt;” has arisen to document the
barrage. Some &lt;a href=&quot;https://github.com/search?q=is%3Apr+%22improve+docs%22+created%3A%3E2020-09-29&amp;amp;type=Issues&quot;&gt;cursory&lt;/a&gt;
&lt;a href=&quot;https://github.com/search?q=is%3Apr+label%3Ainvalid+created%3A%3E2020-09-29&amp;amp;type=Issues&quot;&gt;searches&lt;/a&gt; show thousands of
spam pull requests, and rising.&lt;/p&gt;
&lt;p&gt;DigitalOcean seems to be aware that they have a spam problem. Their solution, per their
&lt;a href=&quot;https://hacktoberfest.digitalocean.com/faq&quot;&gt;FAQ&lt;/a&gt;, is to put the burden solely on the shoulders of maintainers. If we go
out of our way to tag a contribution as spam, then… we slightly decrease the chance of the spammer getting their free
t-shirt. In reality, the spammer will just keep going, submitting more pull requests to more repositories, until they
finally find a repository where the maintainer doesn’t bother to tag the PR as spam, or where the maintainer isn’t
available during the seven-day window DigitalOcean uses for spam-tracking.&lt;/p&gt;
&lt;p&gt;To be clear, myself and my fellow maintainers did not ask for this. This is not an opt-in situation. If your open source
project is public on GitHub, DigitalOcean will incentivize people to spam you. There is no consent involved. Either we
contribute to DigitalOcean’s marketing project, or,
&lt;a href=&quot;https://twitter.com/SudoFox/status/1311431141702819840&quot;&gt;they suggest, we should quit open source&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Hacktoberfest does not support open source. Instead, it drives open source maintainers even closer to
&lt;a href=&quot;https://www.google.com/search?q=open+source+burnout&quot;&gt;burnout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://domenic.me/images/hacktoberfest-spam-pr.png&quot; alt=&quot;A screenshot of a spam PR which adds the heading &amp;quot;Great Work&amp;quot; to the HTML Standard README&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-can-we-do%3F&quot;&gt;What can we do?&lt;/h3&gt;
&lt;p&gt;My most fervent hope is that DigitalOcean will see the harm they are doing to the open source community, and put an end
to Hacktoberfest. I hope they can do it as soon as possible, before October becomes another lowpoint in the hell-year
that is 2020. In 2021, they could consider relaunching it as an opt-in project, where maintainers consent on a
per-repository basis to deal with such t-shirt–incentivized contributors.&lt;/p&gt;
&lt;p&gt;To protect ourselves, maintainers have a few options. First, you can take the feeble step of ensuring that any spam
against your repositories doesn’t contribute to the spammer’s “t-shirt points”, by tagging pull requests with a “spam”
label, and &lt;a href=&quot;https://twitter.com/MattIPv4/status/1311390498888781824&quot;&gt;emailing hacktoberfest@digitalocean.com&lt;/a&gt;.
DigitalOcean themselves, however, admit that
&lt;a href=&quot;https://twitter.com/MattIPv4/status/1311390054334554113&quot;&gt;this won’t stop the problem they’ve unleashed on us&lt;/a&gt;. But
maybe it will contribute to the &lt;a href=&quot;https://github.com/MattIPv4/hacktoberfest-data&quot;&gt;metrics&lt;/a&gt; they collect, which last year
showed that “only” 3,712 pull requests were labeled as spam by project maintainers.&lt;/p&gt;
&lt;p&gt;If you’re comfortable cutting off genuine contributions from new users, you can try enabling GitHub’s
&lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/github/building-a-strong-community/limiting-interactions-in-your-repository&quot;&gt;interaction limits&lt;/a&gt;.
However, &lt;del&gt;you have to do this every 24 hours, and&lt;/del&gt; it has the drawback of also disabling issue creation and
comments. &lt;ins&gt;Update: GitHub has made the limit configurable, and has
&lt;a href=&quot;https://twitter.com/github/status/1311772722234560517&quot;&gt;a nice cheeky announcement tweet&lt;/a&gt; zooming in on the “1 month”
option.&lt;/ins&gt;&lt;/p&gt;
&lt;p&gt;Another promising route would be if GitHub would cut off DigitalOcean’s API access, as
&lt;a href=&quot;https://twitter.com/__agwa/status/1311399074814472194&quot;&gt;Andrew Ayer has suggested&lt;/a&gt;. It’s not clear whether DigitalOcean
is committing a terms of service violation that would support such measures. But they’re certainly making GitHub a
less-pleasant place to be, and I hope GitHub can think seriously about how to discourage such corporate-sponsored
attacks on the open source community.&lt;/p&gt;
&lt;p&gt;Finally, and most importantly, we can remember that this is how DigitalOcean treats the open source maintainer
community, and stay away from their products going forward. Although we’ve enjoyed using them for hosting the
&lt;a href=&quot;https://whatwg.org/&quot;&gt;WHATWG&lt;/a&gt; standards organization, this kind of behavior is not something we want to support, so
we’re starting to investigate alternatives.&lt;/p&gt;
</content>
  </entry>
</feed>