Thursday, 10 September

10:14

How much extra for “not lazy”? [Seth's Blog]

Lazy isn’t a moral failing. It’s an economic consideration.

We have limited time, limited energy and limited resources. Allocating that effort is often done in response to what’s at stake and what’s on offer.

The clean room at a silicon fab or operating theater is clean because everyone involved puts in a lot of effort to keep it that way. And that effort is expensive.

It’s not efficiency or precision. Those are important on their own. The gap between laziness and not-lazy requires effort in the face of nuance, challenges or frustrations.

The staff at Motel 6 will put less effort into each guest’s requests than at the Ritz down the street because there’s a lot more staff per person at the Ritz.

There’s organizational laziness, in which a system is designed to have each person care a bit less about each task in exchange for completing more tasks, and there’s individual laziness, which is the natural consequence of disrespectful management.

If the boss insists on non-lazy behavior, but doesn’t give the team the time, the training, the tools or the compensation to do so, it’s not going to happen, not in the long run. Over time, not-lazy is rarely free.

AI bots have an economic incentive to do as little as they can get away with, and so do we. Programmers around the world are frustrated that coding bots almost solve the problem, but don’t seem to care enough to put in the extra cycles to get it right. But that’s what we’re (not) paying for.

The mismatches are frustrating. We might be delighted when we get not-lazy responses that we didn’t pay for, but it ruins a brand or a project when the effort we expected from a system or a person doesn’t match what we think we paid for.

Some clarifying questions:

Are our customers already paying for the non-lazy option? Or do they think they should be getting it for free?

If we wanted to disrupt our competitors by being the non-lazy option, what would we have to do and how could we communicate that?

How can we manage systems, processes and people so they have the resources and rewards they need to take the non-lazy approach?

If we’re trapped or pushed into the lazy path, how can we build systems so that laziness doesn’t damage our work?

How much extra could we change for not-lazy? And are we prepared to keep that promise?

“You’ll pay a bit more but you’ll get more than you paid for” is almost always a winning market position.

06:00

Urgent: Investigate Paramount's takeover [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to investigate Paramount's takeover meant to control most news media.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Stop surveilling of unions, churches and activists [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to stop the Department of Hostility and Suspicion from surveilling unions, churches, activists, and other peaceful political activism.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Refuse wrecker's voter-data grab [Richard Stallman's Political Notes]

US citizens: call on your state's Secretary of State to Refuse the wrecker's Voter-Data Grab.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Stop integration of American military tech with Israel [Richard Stallman's Political Notes]

US citizens: call on the Senate to stop the permanent integration of American military tech with Israel.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Preserve Forest Service Roadless Rule [Richard Stallman's Political Notes]

US citizens: file a comment calling on the Forest Service to preserve the Roadless Rule that protects much of our national forests from being cut down.

Urgent: Pass Tax Excessive CEO Pay Act [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to pass the Tax Excessive CEO Pay Act.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Faster Labor Contracts Act [Richard Stallman's Political Notes]

US citizens: call on your senators to vote for the Faster Labor Contracts Act, which would stop companies from stalling contract negotiations forever.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

05:56

What algorithm did Windows XP use to choose your initial user picture? [The Old New Thing]

I noted some time ago that Windows XP chose your initial picture at random from among the pictures in the %ALLUSERSPROFILE%\Application Data\Microsoft\User Account Pictures\Default Pictures directory. But it seems people want to know more.

The random number generator is our friend RtlRandomEx, using the current value of GetTickCount() as the initial seed.

The function uses a one-pass random selection algorithm. I can immediately think of two benefits of this decision. First, compared to the naïve two-pass algorithm of counting up all the items, then randomly picking a number from 1 to n, and then iterating a second time to find the item at that index, it’s more efficient because it reduces the amount of calls into the file system, which is where the bottleneck is. Furthermore, the one-pass algorithm avoids complications if the number of files in the directory changes while the code is running.

The one-pass algorithm is a special case of reservoir sampling, where k is 1. This special case permits a tailored algorithm that is much simpler.

selectRandomFromIterator(iterator)
{
    var count = 0;
    var winner = null;

    while (iterator.moveNext()) {
        ++count;
        if (uniform_random(min: 1, max: count) == count) {
            winner = iterator.current();
        }
    }

    return winner;
}

The way this algorithm works is by observing that in a collection of n items, the last item has a 1/n chance of being randomly selected. If it isn’t selected, then you need to select randomly from the first n − 1 items, which you can solve recursively.

Playing the recursion forward, you start with the base case which is that if you have a list of 1 item, then your only choice is to chose that item. Otherwise, if you have a list of n items, first choose an item randomly from the first n − 1, and then switch to the nth item with a 1/n probability.

As a final safety check, the code stops after sampling 100 pictures. This avoids pathological behavior if somebody puts a million files in the Default Pictures directory.

The post What algorithm did Windows XP use to choose your initial user picture? appeared first on The Old New Thing.

04:21

Russ Allbery: podlators v6.1.1 [Planet Debian]

podlators is the package containing Pod::Man, Pod::Text, and other tools for converting POD documentation into manual pages and simple text documents.

This release fixes a long-standing bug in Pod::Text and subclasses where a pathological level of indentation could cause the wrapping code to go into an infinite loop. Thanks to Jitka Plesnikova for the report. This was assigned CVE-2026-82560, although I make no guarantees that podlators is safe to run on untrusted input and therefore not fully treating this like a security issue.

While fixing that bug, I noticed a bug in Pod::Text::Overstrike's wrapping code that would leave stray formatting at the start of the next line in some situations. That is also fixed in this release.

You can get the current podlators release from CPAN or from the podlators distribution page.

02:49

[$] LWN.net Weekly Edition for September 10, 2026 [LWN.net]

Inside this week's LWN.net Weekly Edition:

  • Front: Rust's never type; Debian and CERN; memory tiering; testing multithreaded Python; fixing TCMalloc; Typst.
  • Briefs: Rustls; Asahi Linux; Buildroot 2026.08; Grml 2026.09; Audacity 4.0; Jellyfin 12.0; LibreOffice Base; Quotes; ...
  • Announcements: Newsletters, conferences, security updates, patches, and more.

01:56

Link [Scripting News]

Surprising news. Matt Mullenweg was forced to take a paid leave of absence as CEO by Automattic's board.

Wednesday, 09 September

23:42

Matthew Garrett: SystemIO conflicts are not firmware bugs [Planet Debian]

I’m looking at something entirely unrelated, but tripped over some search results that made me realise that a lot of people still think getting errors like ACPI Warning: SystemIO range 0x0000000000001828-0x000000000000182F conflicts with OpRegion 0x0000000000001800-0x000000000000187F indicate a firmware bug. This is generally untrue. We need to dive a little into what ACPI is to clarify why.

The Advanced Configuration and Power Interface1 specification defines a whole bunch of stuff, but what’s interesting to us here is the hardware abstraction it performs. While PCs are nominally a well-defined platform that’s really not true at the hardware level once you get beyond a certain level of complexity. When you suspend a system you want to power down the hardware in the correct order, for instance, and knowing what that order is requires you to know details about the specific motherboard design. The approach taken in the embedded world is to just bake that knowledge into the OS in some form, which is how we end up with Devicetree. ACPI takes an alternative approach - rather than provide that information as data that has to be consumed by OS drivers, it distributes it as code.

The ACPI Source Language, or ASL, is a simple language that gets compiled into a bytecode that’s then interpreted by the OS at runtime. One of the features of this language is the ability to define “Operation Regions”, effectively structure definitions that describe access to underlying hardware. Let’s imagine a simple device with two exposed registers. The first is an index register - it describes which internal register we want to access. The second is a data register, where reading it gives us the value of the internal register whose address is currently in the index register, and writing to it modifies that register. An example operation region declaration would look something like

1
2
3
4
5
6

OperationRegion(OPR1, SystemIO, 0x400, 0x2)
Field(OPR1, ByteAcc, NoLock, Preserve)
{
  INDX, 8
  DATA, 8
}

This defines an operation region called “OPR1” at IO port 0x400, 2 bytes long. Inside it are two 8-bit fields, INDX and DATA. These are to be accessed one at a time, do not need the ACPI interpreter to take a global lock when accessing them, and if a subset of the register is modified then the other values should be preserved (irrelevant in this case since the fields are only a byte wide). Now any references to INDX or DATA in this scope will trigger accesses to those registers. So, a method to read the value of register 0x03 would look something like:

1
2
3
4

Method (RD03) {
  INDX = 0x3
  Return (DATA)
}

ie, set INDX to 3, and then read the value of DATA and return it. But! What if another ACPI method is running at the same time? Let’s say we have one that writes to register 0x05:

1
2
3
4

Method (WR05, 1) {
  INDX = 0x05
  DATA = Arg1
}

What happens if RD03 executes while we’re part-way through WR05? INDX might get reset to 0x03, and now WR05 will modify register 0x03 instead of 0x05. Oh no! But we can avoid this - we declare a mutex (Mutex (MUTX, 0x00)), and update our methods to be something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

Method (RD03) {
  Acquire (MUTX, 0xFFFF)
  INDX = 0x3
  Local0 = DATA
  Release (MUTX)
  Return (Local0)
}

Method (WR05, 1) {
  Acquire (MUTX, 0xFFFF)
  INDX = 0x05
  DATA = Arg1
  Release (MUTX)
}

Each method takes a lock (waiting up to 0xffff milliseconds and then erroring out if it doesn’t), and performs the access. There’s now no chance of a race. Phew!

Now suppose someone writes a Linux driver for this piece of hardware. It accesses the hardware directly, with no knowledge of ACPI. What stops the driver from racing against one of the ACPI access methods? Nothing at all. Oh no! Again! This isn’t hypothetical, by the way - here’s a relatively harmless example, but back in the day we did trip over cases where temperature monitoring chips would be accessed by the firmware and Linux simultaneously and as a result you might end up thinking you’re reading a temperature when you’re actually reading a status flag, resulting in an impossibly high temperature and an immediate thermal shutdown.

In this case, the kernel saves you from this (potentially hardware damaging) outcome by printing a message like ACPI Warning: SystemIO range 0x0000000000000400-0x000000000000401 conflicts with OpRegion 0x0000000000000400-0x0000000000000401 (OPR1), telling you that the kernel has detected that a driver is attempting to allocate IO ports 0x400-0x401, but that there’s an ACPI operation region called OPR1 that is claiming the same addresses. The kernel isn’t in a position to know what type of access the firmware might perform in that region, so assumes that it might be dangerous and blocks the driver from loading.

But all is not lost! The kernel also prints some helpful advice, ACPI: If an ACPI driver is available for this device, you should use it instead of the native driver. And ACPI tables will often actually have a definition that looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Device (HDW1)
{
  Name (_HID, "VEND0001")
  OperationRegion(OPR1, SystemIO, 0x400, 0x2)
  Field(OPR1, ByteAcc, NoLock, Preserve)
  {
    INDX, 8
    DATA, 8
  }
  Mutex (MUTX, 0)
  Method (RD03) {
    Acquire (MUTX, 0xFFFF)
    INDX = 0x3
    Local0 = DATA
    Release (MUTX)
    Return (Local0)
  }

  Method (WR05, 1) {
    Acquire (MUTX, 0xFFFF)
    INDX = 0x05
    DATA = Arg1
    Release (MUTX)
  }
}

which defines an ACPI device and associated methods. The _HID field defines the device type, and a Linux driver can be written that will be automatically loaded if a device with type VEND0001 is seen. That driver can then call ACPI methods associated with the device and access the resources in a way that matches the firmware’s expectations.

(Interested in writing such a driver? I wrote a guide back in 2009)

The firmware did absolutely nothing wrong here2, but trying to load the native ddriver will generate an error and the internet will tell you that PC firmware developers are incompetent3 and you should pass a kernel argument that overrides this behaviour and it never did them any harm, and it probably won’t do you any harm either but it might and you might never know why your system occasionally wedges or catches fire.


  1. The ACPI spec used to live at acpi.info, but sadly that seems to have vanished some time after UEFI took over stewardship of the spec ↩︎

  2. You might argue that the firmware should simply not do anything at runtime because it is not the firmware’s job to do that, and I do understand that and you can certainly boot with acpi=off if you want to and no ACPI code will be executed at runtime. Let me know how that goes. ↩︎

  3. I’m not going to present an opinion on that here, merely say that this provides no supporting evidence for that assertion ↩︎

22:56

The new Apple Watch is always recording and transcribing everything it hears [OSnews]

Do tech companies ever stop to think about the misery they unleash on people’s lives? The Apple Watch Series 12 uses “Audio Intelligence” that, as well as secretly taking notes, also has a Live Rewind feature that can replay the last 15 seconds of recently detected audio and view a text transcript.

↫ Matt Growcoot at PetaPixel

Another win for us Europeans: this dystopian nightmare will not be available in the EU.

Solaris’ turnstiles [OSnews]

Although Solaris is now mostly defunct, its influence remains substantial; technologies pioneered by Solaris can still be found across a wide range of software

A lot of Solaris’ inventions have been described and talked about ad nauseam (such as the Slab Allocator), but one I rarely see discussed is its use of turnstiles.

Despite being relatively obscure, the idea has quietly spread far beyond Solaris. Variations of it can now be found in major operating systems, web browsers, and language runtimes. In fact, you’re probably using several implementations of the same basic concept right now!

↫ Loïc Grégoire

I’m not going to pretend to understand all of this, but I know you people do, and many of you will definitely find this interesting to read. Also note that Grégoire develops their own operating system kernel called zag.

22:49

Link [Scripting News]

I have completely lost interest in new phones. The last one I bought was a Pixel 9 because they said it would have great AI features. At that time the sky was the limit, there were new breakthroughs every afternoon in AI. Now it's just an app. I have to carry an iPhone because I use an Apple Watch. Phones are utilities for me. I have a lot more interest in weed whackers these days. Not kidding. Now a story. I wanted to check the model of the phone. Went to settings, and the command at the end of the menu should tell me the version, but they had changed that. So I figured I'd go back up to Gemini, their AI app, and ask it. Of course it has no idea where it's running. Screen shot. That would be one of the real problems they could solve, making it easy to control the computer you're running on. The whole UI should be in AI. All it did was print basically a page from the user manual, with the same out of date instructions. How many years have we been working on AI already, this is the easy stuff that they haven't done. Tech companies get back to your business, make your software better. You can and it's long overdue. BTW, it's a Pixel 9 Pro.

22:07

Own the Outer Loop [Radar]

The following article originally appeared on Addy Osmani’s blog and is being republished here with the author’s permission.

In the past year, the conversation around agentic engineering has moved to harnesses and loops, fleets and software factories. My 2 cents is engineers need to own the outer loop—the accountability for these systems. This only gets more true as powerful models like Fable and GPT-5.6 become available.

The future of engineering

Agents have leverage, and leverage creates obligations. Someone must be able to explain exactly what changed, why it was safe, and what will happen if they’re wrong. Otherwise, their actions can’t be justified. Which makes it unlikely their organization will ask for them in the first place.

And so I want to talk about three terms. The first, Quality, refers to all the checks we install before we let the system loose. Those checks produce evidence, and from that evidence we derive a Verdict.

The second, Verdict, refers to the final decision we make before work enters our dependent system: I’m the line-producer of this content. I run the team whose work is shipped under my name. The model may write the line, but the Verdict is mine. The work of my team will not enter our dependent systems without my decision. A Verdict is the production decision: Should we ship, block, redirect, narrow the response, add a guardrail, or reject outright?

The third, Answerability, refers to the guarantee that if someone asks, I can explain why.

To say this another way: Our agent (which I define as a model plus a harness of files, tools, memory, skills, sandboxes, permissions, observability, and recovery) is what runs our loop (which I define as investigation, implementation, verification, and repeat). And it’s what creates our software factory.

Harness engineering

The model is just the engine. The harness—tools, memory, permissions, sandboxes, tests—is the car you build around it so it can do real work safely.

Loop engineering

The loop is how one good run becomes a process you can trust to run again. Wrap that harness in a repeatable cycle—investigate, implement, verify, repeat—where an independent check, not the model’s own say-so, decides when the work is done.

Agentic software factory

Now run many loops at once. A factory is loops at scale: The agents ship the work inside, while humans own the decisions at the boundary.

And at the heart of that factory is a careful boundary between what’s inside the system and what’s outside it. Inside the system we collect inputs (from the product team’s intent, or knowledge of previously shipped work, or of recent incidents, or of specific feedback from users). The agent loop investigates the task, implements a plan, and verifies the result. Then, evidence crosses that boundary. A human, who owns the dependent system, sees the evidence and decides whether to proceed.

The loop boundary is evidence

And that, friends, is the shift we’re trying to make. Before, our agents were doing the inner loop of the execution loop. Now they run the inner execution loop. Engineers own the outer loop.

What "high agency" means now

Inside the system, there’s really just one kind of thing our agents are doing: capability. The capability to investigate tasks, implement plans, test their results, and report back. That’s the capability of a model. And as we’ve said, that future is already here.

Outside the system, there’s a single kind of thing: agency. The agency to decide, verify, approve, and own.

We’re still talking about code, you see. It just needs to live in a place and be performed by people who know what they’re doing.

The potential for AI code is no longer marginal. In a Sonar 2026 survey, we asked teams about the share of their commits that were AI-assisted. It was small but nontrivial. And several of the respondents said they expect the share of AI-assisted commits to grow substantially.

Sonar’s 2026 State of Code report found that 42% of committed code was AI-generated or significantly AI-assisted, with expectations for that share to keep growing rather than plateauing.

AI code share is no longer marginal

Creation, in other words, is getting cheaper. Scarcer resources are review, validation, understanding, and maintenance.

We moved the speed of generation faster than we moved the speed of control, and so we have a trust-verification gap. A lot of people we talk to still express some degree of distrust in AI code. Yet fewer of them seem to consistently build that distrust into their verification processes.

Trust without capacity

That’s a dangerous place to be. We’re going to need cheaper, clearer ways to verify the trustworthiness of AI code.

If you look at the GitLab June 2026 report, you’ll see that governance questions have shifted.

GitLab’s June 2026 AI accountability research shows that review and validation are the current bottlenecks when using AI and, more worryingly, that governance usually happens after code creation, after we’ve accepted the risk and lost control over ownership. Today, it’s not just about control. It’s about what constraints we set on the system. It’s about how we’ll check the work with evidence, and how we’ll hold teams accountable. It’s about who will own what part of the AI lifecycle.

The governance gap

So the final distinction in this series is between process and quality. Quality is the concept of backpressure. We mean it literally. We don’t want to grant our agents as much autonomy as they can possibly exercise. We want to grant them just enough autonomy that we have enough backpressure to stop them, regulate them, check their work, and ensure our humanity.

Ordinary engineering holds up a lot of signals that indicate that the work being done is doing the right thing. Type checks, tests, hooks, sandbox limits, audit logs, monitors. Our engineering systems are full of these kinds of signals, and they’re designed to provide enough backpressure to keep the system honest.

And so as long as our agents are emitting these same signals, we can trust our ordinary engineering to provide appropriate backpressure.

Trusting our systems doesn’t mean we don’t want a human in the loop. It just means that the human doesn’t need to be in the inner loop. We want them in the constraints loop (What inputs, architectures, instructions, or invariants should we set?), the sampling loop (How much output should we sample and review?), the audit loop (What evidence should we keep, and how do we make sure our audit log is effective?), and the ownership loop (What part of the production boundary should we own?).

But the human doesn’t need to be in the inner loop.

The agent can ship more than you can review.

The agent can ship more than you can review

And the scarce resource is your own core human judgment, informed by quality signals like logs or tests.

The AI June 2026 report shows that, in the experimental setting, agentic delegation along hour-scale time horizons is essentially here. The work by OpenAI this year on agents and the future of work was a great source for these ideas. So we need to start thinking about how to establish this ownership boundary, as our systems start shipping more than we can review.

And that’s where the answerability comes in.

Because with long-horizon agents, the decisions made over hour-scale time horizons are just that—decisions. And not all the decisions are going to be recorded. You can’t trace them all back to input tokens. If all you’re doing is trusting that the output you get is the correct choice for the problem at hand, the hundreds or even thousands of human hours of work you’re going to need to reconstruct the chain of decisions that lead to it become impossible. And so, again, answerability becomes something that must be at the core of our system design.

Three hidden costs

And there are three hidden costs:

Cognitive surrender ~ blindly accepting what AI gives you. When you delegate work to an agent, the work itself may appear to be the work of the agent. But it’s actually your work. It’s your reputation. It’s your responsibility. And it’s your software that suffered the defects in the output. And it’s your software that needs to be changed to reflect that output. So the agent’s output becomes your answer. And with it comes all the accountability. The Wharton study that put this together is reassuring when the AI is right. But when it’s wrong, the news isn’t great. When the AI was wrong, nearly three-quarters of people accepted it anyway, and felt more confident than they would have without the AI.

Cognitive surrender

Cognitive debt ~ erosion of your understanding and memory of how to solve problems. When you delegate work to an agent, you’re offloading all the thought work to the agent. And while thinking it all out yourself takes time and energy, thinking it out on a massive codebase takes resources that aren’t available when you’re trying to run up the learning curve. So the output you get is often unattainable by you. And the longer the time horizon of the agentic planning, the bigger the gap between the code the agent produces and your understanding of it becomes. The gap compounds. The debt accumulates. And the cost of climbing the learning curve grows almost exponentially.

There’s a randomized controlled trial from Anthropic looking at whether engineers who lean on AI to write code understand it as well as engineers who write it themselves. The conclusion was gloomy: On a comprehension quiz, the engineers who worked through AI scored 17 percentage points lower than those who didn’t, 50% versus 67%.

Cognitive debt

And then there’s the orchestration tax: It’s easy to spin up lots of agents now, but your cognitive bandwidth doesn’t parallelize in the same way. Steering your agent away from the worst behaviors, sorting the work the agent produces to identify the ones that need your attention, directing it to focus on the work you care about first, verifying your most important constraints and your most dangerous assumptions before you let it run. . .

All of that takes work, and it can’t be automated. There’s no substitute for human judgment.

Orchestration tax

Brownfield systems are especially dangerous here, because the system behavior you have to audit doesn’t live in the code. It lives in the scars.

Fixes? Make attention the priority in your architectural decisions. Use worktrees, scopes, and evidence to reduce the coupling between your initial plan and the work that emerges from it. Time-box the effort to resolve unactionable steps. And make change in your software strictly an opt-in permission.

Alpha, decay, and taste: These are the three core patterns that shape careers and performances across domains.

Alpha is the gap

Alpha is the lead part taken up by the highest achiever in the competition, when you’re playing your highest-value game move. Decays are established patterns that everyone learns through repetition and watching others (plateaus, if you like). Taste is the earliest we can sense the lead in an alpha or the change in a decay. It’s our judgment of what’s coming before we have any evidence that anything is happening.

Paul Graham’s point is that when anyone can make anything, choosing what to make matters more, and Mitchell Hashimoto’s definition is the operational one: making high-quality qualitative judgments where no objective metric exists yet. From now on, taste drives everything. Alpha shifts are taste changes. And decays fade out because we start to taste something different.

Taste is he judgment before the metric exists

Next step? Operationalize your taste. How? Give it a name that reflects what you’re trying to move from limbic to conscious. Practice it in critique and examples. Make its rationale explicit.

The test: Is it a capability? Then it decays

And keep making the move that delivers the most durable competitive advantage in your industry. What’s that? Keep moving the edge up from just doing the task to teaching it, systematizing it, deciding when it should be done, and owning the result.

One question: What can the agent do?

Everyone is a developer, but not everyone is an engineer. Engineering is what a developer turns into when they embrace a work discipline that is more strict: thorough and logically sound reasoning, consideration of constraints and tradeoffs, recognition of risk and exposure, and practical accountability.

Everyone is a developer now.

In the future, people will leave the administrative work of engineering and embrace new roles that emerge as engineering becomes more demanding. Roles that are unbundled from the spirit of craft but make clear what each person does. There will be those who prototype. Those who build. Those who sweep. Those who grow. Those who maintain.

The future of careers

The humans hold the edge of the system in the other direction too. Increasing the alpha: choosing what is worth doing, defining the constraints within which it should be done, deciding if the evidence is sufficient to proceed, and caring for the result. Whether it’s a single team or a hundred teams, this is the edge that only humans can hold.

Accountability will scale the factory. Like attention and taste, accountability is also one of the three dualities that makes everything work. Without accountability, there are no rules. No wrangling with questioners. No trade-offs. No risks. No safety nets. If nobody owns the consequence of a decision, then high agency can only bring chaos.

Accountability scales the factory

The half-life of an edge is one release, but the half-life of a signature is a career. A signature is your name on the work, such that you feel you can stand behind what was shipped. Skills get you leverage; accountability turns leverage into trust.

What decays, what doesn't

Only people can choose. Only people inherit consequence. Agents can be asked to choose, route, merge, and escalate safely inside a policy, but they cannot inherit the consequences.

The agent can choose.

Every codebase should perhaps come with some kind of accountability contract that explicitly states the checklist that was understood when the change was accepted, the evidence that went into the decision, who was accountable for the change, and the system status after the change was blocked. Just like:

  • Your attention and taste
  • Your evidence, verdict, and ownership
  • Your alpha, decay, and taste

High agency

In a typical agentic workflow, high agency is the art of knowing when to delegate, when to inspect, when to stop, and when to own the result of a process. The ladder of agency runs from low to high: flag a potential problem, investigate it, execute against it, diagnose it, propose solutions, recommend fixes, and resolve the issue. A high rung on the agency ladder is discernment: found it, it’s not worth fixing, moving on.

The agency ladder

The 12 pillars that hold up the software factory

Brownfield is the frontier for factories that hope to scale. All those clever little innovations may not feel like much yet, but the production environment is a lot. When building an entirely new system, it’s much easier to plan and implement sufficient backpressure mechanisms because you have full control. When you’re adding intelligent agents to a legacy system, however, it’s another matter entirely.

Legacy systems include the entirety of production behavior, future expectations from customers, migration histories, release and budget cycle durations, unspoken assumptions, edge cases, data weirdness, runbook procedurals, and all the scars that accumulated without the will to care for the system.

To be a steward of brownfield requires a form of durable engineering. Work has to be done to turn implicit knowledge into explicit constraints, keep it coherent across teams and through generations, formalize that knowledge into test procedures and functional specifications, and tie that knowledge to objective evidence. All while ratcheting failure into more learning. Because if the system doesn’t get the care it has always received, everything will come crashing down.

New work is real work

The work will get more interesting as you scale. Because when everything else is built, people will want to build new things. They’ll want to employ the alpha and taste they have developed through their craft to design new loops that can be grafted onto the software factory. Or they’ll want to build greenfield systems that employ all the knowledge of the software factory to one elegant, well-meaning, principled effort. They’ll want to design and implement new forms of evidence that will rise to the level of verification for the new systems. They’ll want to take care of brownfield systems that are now so complex they need dedicated attention. They’ll want to design and manage new backpressure mechanisms. They’ll want to design new agents. And they’ll want to build agency.

Automation moves the floor

And, as they do, they’ll come to see that all this is real work. That’s a good thing.

Automation creates bottlenecks. Bottlenecks in production that are worth owning. Because automation gives us control over industrial scale. But there’s also new bottlenecks that arise from industrial scale. The bottleneck moves from “Can we build this?” to “Should this exist? Can we answer for it?”

What I’m suggesting is a practical operating model for scaling agentic engineering. There’s inner and outer loops. The inner loop is where the work is done. Loops are designed to be as independent as possible. Put all quality assurances and verification inside the loop. Once you’ve designed and validated the loop itself, the only thing you have left to do is to grant autonomy by putting in place a back-pressure mechanism that acts to control the rate at which the loop is run and its scope of operation. And put humans in their rightful place, on the right decisions. Don’t treat understanding as a hand-off or a release gate but rather as a point of decision where humans are primed to provide their insight. And then for every artifact that exists and is fed back into production and into new teams and engineers, leave behind better artifacts.

Build the factory; keep the lights on; make work legible, verifiable, owned.

An agent can write it. But before it reaches users, someone must explain why it should exist, why it’s safe enough to be part of production, and what they will do when it is wrong.

That’s agentic engineering at the outer loop—that’s the work now.

The Big Idea: K. Ancrum [Whatever]

One of the eternal question in speculative fiction is: Is this fantasy? Or is this science fiction? And does the answer matter? For Adam, Mine, author K. Ancrum takes a stab at this question, using one of the most durable tales in literature as inspiration.

K. ANCRUM:

The moment of conception in Mary Shelly’s Frankenstein is so deeply imbedded in our cultural consciousness, through adaption and visual representation, that you’d be hard pressed to find someone over standard drinking age who cannot place the 1931 film shriek “It’s alive!” even if they’ve never seen it, or read the book at all.  

It’s an instant of science horror that birthed a thousand sub genres, but there is so little discussion about it in particular. No, no, not the philosophical nature of birth, or the allegorical structure regarding God and Man, or the Lit-Crit monologue on the nature of the act of creation. I am not talking about that. I am talking about the spark itself! The scientific labor that preceded the resurrection and the moment it worked! The concept of something fantastical to us (Doylist) readers, simultaneously in the (Watsonian) context of the story presented and firmly understood—despite its “unreality”—as science.

That is what grabbed my attention firmly. The familiar argument: the delineating science fiction/fantasy border and the malleable nature of perception that decides what lands on one side or the other.

I was obsessed with framework and emotion.  How it feels to see grotesquerie beyond your imagining and in your terror the hind brain rejects it: evil, witchcraft, demons, the hand of fate. To be an educated person and know you’re seeing science, but lacking the vocabulary or even the schema to identify it as anything but nonsense: rules and methodology become spells, medicine becomes potions, life-saving machines are the mechanical tinkerings of a madman. Saving someone from the brink of death? Stealing from God. 

This isn’t exactly the big idea, but it’s close. We’re almost there.

I wanted my young readers to make the mistake of sneering at people from the past for their ignorance and then, within the span of the novel, to become them. As an educator, I wanted them to experience a shift in perception that humbles.

All we can possibly know is what we have access to, and it takes incredible imagination and the entirety of the history of humanity’s collaborative labor to reach beyond that. At the forefront of every moment of discovery and invention, often at the cost of their own lives, there were people doing science that looked to the majority of us like magic.

And there always will be.

That’s the big idea!

Now we’re all roughly familiar with Mary Shelly’s Frankenstein and the attitude towards science at the beginning. Victor’s stubbornness and arrogance, his peers and professor’s disgust towards him and his work, his friend’s terror at his actions and the cavalcade of punishments the narrative delivers him as a result of his cruelty, neglect, and ignorance. ADAM, MINE similarly begins: classmates mocking him, professors deriding him, punishments on the horizon.

But the resounding echo that follows is Doylist knowledge. Victor’s peers and teachers are sneering at things that have already been invented: things the children reading should know about. A breathing machine? Sounds creepy. Removing plasma from blood? Insanity. Neurosurgery? Only a serial killer would poke around in someone’s brain. Restarting a heart with electricity? What?? It entreats the reader to enter the framework, to roll their eyes at the footnotes clarifying these real inventions and their year of make. To giggle a bit.

When it comes time for Victor to make his announcement about reanimation, it fits in well with the rest of it. ‘More histrionic, creepy, fantasy nonsense from our class’s most annoying Rich Boy Wunderkind,’ Victor’s peers say and ‘here we go now” my young readers think. With that: the door slams shut behind them.

In my prison of perception, I introduce “alchemists.” I give Victor a “spell book.” I tell the readers his life force is seeping away and he doesn’t know why. Nothing is explained because the lack of explanation is a part of the framework.

The alchemists are powerful and mysterious; they all know each other and talk to each other like colleagues. One of them gives Victor a talisman. Another uses lightning as a magic conduit. Victor and my readers have never seen anything like this before. The Alchemists act like there are rules and methodology that govern what they can and cannot do. But Victor doesn’t know them and he never learns. He travels in terror over thousands of miles in a body that weakens as violently and mysteriously as people from our turn of the century’s did as they played in radiation.

The only people who can do anything to help him are standing above him like angels from the future. Just shaking their heads in horror and derision as Victor cries; a young time-travelling Harry Daghlian begging on the stairs of David Krieger’s house. They scrounge in their homes and labs for potassium iodide, Prussian blue and DTPA because they feel bad, but they know that he will die. The readers begin to know that Victor will die, and they still don’t understand how or why.  

Victor and my audience may never unpack the biochemistry and electromagnetic theoretical physics Victor fumbled that injured him so badly. The methodology-filled field journal might always seem like a spellbook to them. The Alchemists, cutting edge physicists all, may as well be wizards. This fictional story of his injuries and demise? A case study written with incredible grief and grave warning.

The Perception Prison was an ambitious goal—perhaps—and it won’t land well with everyone. But in the face of the urgency of this lesson and our youth, I had to try.

In the end, my Victor is just like us and we are just like him, we always have been and we always will be. Short lived creatures in a universe of dangerous and seductive mysteries, illuminated only through the sheer might of humanity’s archival collaboration into the benign and understood. The true cost behind the arrogance of our giggle.

Something we should never ever forget.


Adam, Mine: Amazon|Barnes & Noble|Bookshop|Woman & Children First|Anderson’s

Author’s Socials: Website|Instagram|Twitter|

Photo of a Cat, Annoyed That He Was Made to Go to the Vet [Whatever]

Cheer up, Saja. It happens to all the cats, sooner or later.

Also, fun fact: Saja has officially been with us for just over a full year now. We fostered him for a couple of weeks and then decided to keep him, and his official naming day was September 1. I was traveling that day so I completely forgot about it, but better to commemorate a year of his presence late than never. Happy belated gotcha day, Saja. You’re a real pain in the butt and we love you.

— JS

Lotus Notes and the dangers of starting from scratch [OSnews]

Lotus Notes was the future of communications, a decade before laptops had WiFi. Yet of all things, it wasn’t even an email app. It was a notes app, a collaboration tool, an all-things-to-all-people software that let you build apps in the way Access and Airtable later would. That, and the notes could be used for email.

Love it or hate it (and there were plenty on both sides of the fence), what you couldn’t do was ignore it. This email-and-everything-else platform showed what the future of digital communications would become — and provoked, as email itself was always doomed to provoke, equal measures of awe and exasperation.

↫ Matthew Guay

I have no experience with Lotus Notes, but I do have some vague memories of the software being used at my parents’ employers back in the late ’90s. Note that Notes still exists and is in active development as HCL Domino (server) and Notes (the client). If you really want to, you can still run your company of office on Notes.

I wonder how many actually still do.

19:49

Zillennium [Penny Arcade]

Because PAX Houston is in the process of becoming a real event you can attend, I gave a ton of interviews this time. That's typically not something I do, for a couple reasons. It started to seem like I had been asked and then subsequently answered every question imaginable, which made me feel like there wasn't a way to be a real person inside that context. Also, there is a type of hostile person who uses the rules and framework of the interview to be a dick and minimizing contact with that kind of thing while I'm doing a show is just good opsec. So coming back into the light after being away put the differences into relief. For example: there are people so young that you can't imagine it.

19:21

A decade of Rustls [LWN.net]

Joe Birr-Pixton has written a blog post reflecting on a decade of the Rustls TLS-library project and looking ahead to the upcoming 0.24 release and an eventual 1.0 release.

Rustls began with a first commit on May 2, 2016. Progress was quick: a month later, on June 5, it could interoperate with most sites on the web. The first release, 0.1.0, followed on August 27, 2016 – less than four months after the first commit.

[...] From the 0.1.0 release, the project moved through a long series of releases over the following eight years, building out functionality, hardening and refining the API. That sequence of release lines culminated in 0.23, released on February 29, 2024.

The 0.23 release line has been a stable one: in the time since, it has seen 43 non-breaking releases. That stability didn't come with stagnation. The 0.23 line delivered a wide range of important features, including a FIPS-certified cryptography option, certificate compression, Encrypted ClientHello, post-quantum cryptography, and performance improvements.

18:35

LibreOffice Base survey results [LWN.net]

Heiko Tietze has published a blog post summarizing the results of a recent survey about the use of LibreOffice's database application, Base. 455 people participated in the survey, including more than 330 who use Base on Linux, with use cases ranging from maintaining records of personal media such as CDs or DVDs to use enterprise-resource planning (ERP) and finance. Of course, users had many ideas how to improve the application:

The majority asks for improvements to the user interface with less clutter and a more attractive design. The workflow and user experience should become either simplified or more powerful, depending on the expertise and the scenario. For example, an elaborate search function is something that many people expect. [...]

Almost the same number of answers requests bug fixes, improvements to stability, and better performance. Issues with queries, forms, and reports were mentioned equally often. In this regard, many replies suggest to remove the Java dependencies.

18:14

Link [Scripting News]

I spent the day programming in Atlantis. It's buggy, and there are a few missing parts, but most of what I need is there. It's starting to feel like Frontier, which is cool because that's what we're making. And I have my backup tools working on the new machine, praise Murphy.

Dirk Eddelbuettel: RcppXts 0.0.7 on CRAN: Minor Maintenance [Planet Debian]

A new maintenance release 0.0.7 of RcppXts is now on CRAN, and has been built for r2u. The RcppXts package demonstrates how to access the export C API of xts which we contributed a looong time ago. There are by now a more example packages around this C level access to another package, but this one was an early example.

This release is strictly maintenance, updating continuous integration, the README.md file and other packaging conventions adopted since the last release four years ago.

The NEWS entries follow.

Changes in version 0.0.7 (2026-09-09)

  • Corrected a docstring for the module

  • Updated continuous integration setup several times

  • Simplified setup by removing no-longer-needed Makevars

  • Added badges to README.md

Courtesy of my CRANberries, there is also a diffstat report for this release. For questions, suggestions, or issues please use the issue tracker at the GitHub repo.

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can now sponsor me at GitHub.

17:28

Thorsten Alteholz: My Debian Activities in August 2026 [Planet Debian]

Debian LTS/ELTS

This was my hundred-forty-sixth month that I did some work for the Debian LTS initiative, started by Raphael Hertzog at Freexian.

Unfortunately the number of distributed working hours had been rather low this month, so the list contains much less entries than normal. During my allocated time I uploaded or worked on:

  • [DLA 4713-1] sslh security update to fix one CVE in Bookworm related to a so-called “link following” vulnerability.
  • [DLA 4714-1] libmodbus security update to fix one CVEs in Bookworm related to a stack-based Buffer Overflow vulnerability.
  • [DLA 4715-1] kissfft security update to fix two CVEs in Bookworm related to integer overflows.

Last but not least I spent days with FD work at the beginning of the month. The last remaining hours I continued to work on cups and hplip. Unfortunately this work did not result in an upload yet.

Debian Printing

This month I did not upload any package but just worked on some bugs.

This work is generously funded by Freexian!

Debian Lomiri

This month I worked on new Lomiri Apps. Due to a broken disk, the progress was not as expected. But stay tuned!

Not really related to Lomiri, but to Debian EDU, I fixed an incus related bug in sitesummary.

This work is generously funded by Fre(i)e Software GmbH!

Debian Astro

This month I uploaded a new upstream version or a bugfix version of:

  • einsteinpy to unstable, fixing a dependency bug.
  • supernovas to unstable, sponsored upload of a new upstream version.

Debian IoT

Unfortunately I had no time to work in this category this month.

Debian Mobcom

This month I uploaded a new upstream version or a bugfix version of:

misc

This month I uploaded a new upstream version or a bugfix version of:

  • tango to unstable to fix bugs and do a soname transition.
  • pytango to unstable to fix bugs and do a soname transition.

17:07

[$] Typst makes big strides [LWN.net]

Typst is a system for typesetting documents into various formats: PDF, SVG, PNG, and, in progress, HTML. It is adept at handling technical material, and is often considered to be an eventual LaTeX replacement. We last looked in on Typst a year ago, when it had reached version 0.13. A new version, 0.15, was released in June with lots of new features, including support for variable fonts, MathML, multiple bibliographies, and more. Typst is free, Apache-2.0-licensed software, programmed in Rust.

Driver’s License Data for Sale [Schneier on Security]

A database of 153 million drivers licenses is for sale on the dark web. Brian Krebs has more detail.

16:42

15:56

15:14

Link [Scripting News]

Scott Hanson is working on a WordPress plugin that displays a FeedLand river — a constantly-updated stream of news items from feeds a FeedLand user subscribes to — on a WordPress page or post. It's important that news flows be part of managing a site. Help Scott make it perfect, WordPress is a very important part of the web. The news tab on my blog home page is an example.

Link [Scripting News]

Ideas for Bluesky and WordPress re Markdown and AI systems.

Link [Scripting News]

Claude Code massively stinks at explaining things in a way that a human can understand.

Link [Scripting News]

People love Markdown because it's impossible to hide the crud. And because people love it so much it's become a favorite of tech startups esp in the AI field. This is a great turn of events. We're getting back to the web, and the sooner everyone gets on board, the better. I've written about this for a few years. Everything in Markdown, it's just enough HTML. And with that, Microsoft has created a white flag, here's all your data dear users, in Markdown, to use as you please. Instead of waiting for Claude et al to start reading Word files.

Link [Scripting News]

On the other hand, why doesn't Microsoft, which owns GitHub, make a Slack-alike tool that manages the issues sections on repos. Slack is a big part of the default communication system, but GitHub is another very big part. And the great thing is that both these products, owned by huge tech companies, have an ethos of open APIs. Every part replaceable. Small pieces loosely joined. We are family.

Link [Scripting News]

A big corner-turn in the Atlantis project, the new version of Frontier for modern OSes, in development, just me and Claude Code so far. We now have the means to update the root, so anything implemented in script code can be released without fuss, using RSS of course. We're using a method that will be new to almost everyone, something I created in 2014 or so (will check) called codecasting. And today I did all my programming work in Atlantis. It feels just like Frontier, with a future. ;-)

LLMs Reward Expertise [Radar]

The following article originally appeared on Sean Goedecke’s blog and is being republished here with the author’s permission.

In the 2010s, if you had technical gaps (say, you couldn’t write CSS), you had to either rely on a skilled colleague or just hope that the answer to your exact problem was out there on the internet. Today, everyone can write sort-of-okay CSS by delegating the task to an LLM. LLMs make everybody into a generalist.

Because of this, lots of people don’t think there’s any skill involved in working with LLMs. If you want the product that LLMs can deliver—PhD-level mathematics, pretty good but sometimes tasteless computer code, or awkward LinkedIn-style writing—you can simply ask for it. Since everyone is talking to the same models, “skilled prompters” are getting the same results as people touching LLMs for the first time.

This is wrong. The most important skill in prompting is expertise in the domain you’re prompting for.

A good illustration of this is Terence Tao’s conversation with ChatGPT about the recently discovered counterexample to the Jacobian conjecture. This is not the same ChatGPT I talk to! I couldn’t get to where Tao gets, even with unlimited tokens to burn.

There’s a lot to learn about good prompting from Tao’s conversation. Here are a few observations:

  • Tao’s messages are very short and to the point. He doesn’t respond point by point to the model, just to the gist.
  • The model outputs are much more concise than when I try to talk to GPT-5.6 Sol about mathematics. By signalling expertise, Tao shunts the model into “talking-to-mathematicians” mode, not “explaining-to-amateurs” mode.
  • Tao pushes back when the model’s responses look wrong, but he doesn’t directly contradict; instead, he says things like “This looks more complex than I was hoping for.”
  • Tao makes several leaps and suggestions himself. He almost never takes the model’s advice about where to go next.

However, you can’t prompt like Tao on mathematical questions just by following these tips. The key to his technique is actually understanding the mathematics: pulling the relevant idea out of ChatGPT’s multiparagraph response, suggesting alternate approaches or formulations, and identifying what “looks weird.”

Terence Tao is a better mathematician than I am a programmer. But the idea here—that domain knowledge makes you better at using LLMs—is something I’ve also experienced in my own work. If you have a good theory of your codebase, you can push the LLM much harder than if you have no familiarity. Because you have your own sense of what a good solution might look like, you can say, “No, I think it could be simpler here” or “But don’t we already do X?” or “Can we express this problem in these familiar terms?”

This touches on an idea I’ve written about before: that system design problems are dominated by concrete specifics, not generic principles. Of course both are useful, but I’d rather have familiarity with the codebase than a deep general understanding of software systems. In his conversation, Terence Tao asks a lot of specific questions like “Does X work here?” or “Given Y and Z, why A?” I can’t ask those questions about the Jacobian conjecture, but I can ask them about the systems I own at GitHub.

If you have no domain knowledge, you can cling onto the LLM to at least get something. That’s not bad! But if you have domain knowledge, you can wring far more value out of the same LLM by steering it hard in the direction you want. Most of us will have to do a mix of both these approaches, since we have domain knowledge in some areas but not others.

The usefulness of domain knowledge suggests that human expertise will continue to be useful even as models get stronger. For many tasks, the human is the bottleneck, not the model, because the difficult part is in communicating to the model exactly what kind of solution the human wants. The information is “in the model” already, but it takes a very smart human to pull it out.

This post got many comments on Hacker News. Some commenters share their anecdotes about how expertise has helped and lack of expertise has hurt. Other commenters say it’s plausible, but they have a sensible suspicion of a view that’s reassuring them about how they’re still valuable. I agree with that, though I suspect by the time we get around to studying this, the landscape will have changed under our feet again. Some commenters point out that OpenAI’s math prompts were inexpert, and so expertise isn’t required. Here I’d respond that OpenAI does have a team of expert mathematicians that checked and filtered the model’s suggested discoveries, and that you cannot currently skip that step.

Zero to Agent in 30 Minutes: Build a Supply Chain for Agent Context with Maxim Salnikov [Radar]

We still haven’t solved the problem of keeping track of everything we’re feeding our AI agents. Developers now install agent skills, instructions, and other customizations from public repositories by the dozens, and those files end up scattered across user profiles, application folders, and codebases with no record of where they came from or whether they’ve changed since they were first installed.

In this episode of Zero to Agent in 30 Minutes, Microsoft senior solution engineer Maxim Salnikov walked through the Agent Package Manager (APM), a terminal-driven open source product from Microsoft that treats agent context the way modern software already treats its dependencies: versioning it, pinning it, and checking it before it ships. It’s a technical session, but rather than building an AI agent, you’ll discover how to manage all the customizations you’ve installed for your agents and make them portable, secure, and governed by policies you or your company define.

How to build a supply chain for agent context

Maxim demoed the process of setting up and using APM step-by-step. Here’s how it works.

  1. Keep every customization in your codebase. Agent skills, custom instructions, and hooks tend to spread across a developer’s machine, a harness’s install folder, and enterprise-level defaults. Maxim recommends storing them in the codebase instead so that anyone on a new machine or using a new harness ends up with the same setup as everyone else. Yes, your codebase will be bigger, but the trade-off for consistency is worth it. But be sure that skills and other customizations are secure before adding them to your codebase. Consider building your very own enterprise repository of the customizations you’ve identified as safe to use. (See step 5.)
  2. Install APM and initialize a project. APM works like npm for AI agent customizations. Running apm init sets up an apm.yaml file targeting one or more harnesses, such as GitHub Copilot, Claude Code, or Cursor, and apm install pulls a skill from a repository into the right location for each one. When you install a skill, APM also creates a log file documenting the entire resolution history.
  3. Pin to a version or a commit hash. Once a skill is installed, pin it the way you’d pin a library dependency. Doing so protects against a scenario where a legitimate skill later gets compromised and a bad actor slips malicious content into a new version under the same name. APM also scans files for hidden Unicode characters that may contain malicious instructions.
  4. Reproduce the exact setup with a lock file. APM logs every file and hash it installs. Running apm install --frozen rebuilds that exact environment from the log instead of reresolving everything from apm.yaml, so a teammate’s machine ends up with precisely the same setup as yours.
  5. Source customizations from a vetted registry, and set a policy for it. Public repositories work for experimentation, but Maxim suggests enterprises build a private, gated registry of reviewed skills. APM policy files enforce which sources are approved, so a customization from an unvetted location gets flagged.
  6. Audit for drift, then enforce it in CI. The apm audit command checks installed customizations against policy and catches unauthorized sources or content that has changed since installation. Run that same audit as a gate in a CI/CD pipeline to protect the entire organization against skill drift and bad actors.

The software supply chain already has decades of tooling behind it. That discipline hasn’t caught up with agentic AI, but APM is attempting to close that gap. Explore the project GitHub repo and get started.

Coming up next

On September 9, Menyala’s Sajal Sharma joins Zero to Agent in 30 Minutes to build a shared knowledge base that acts as a common brain across agents. He’ll show how a single repository of research, daily logs, and notes can give Claude Code, Codex, OpenClaw, and Hermes access to the same accumulated information instead of starting from zero with every new session.

Follow along with Zero to Agent in 30 Minutes on Radar, or watch the latest episode on YouTube, Spotify, Apple, or wherever you get your podcasts. If you’re an O’Reilly member, you can watch live. Save your seat.

Experience Mapping Matters More the Faster You Move [Radar]

AI is changing how fast organizations can move. Ideas that used to take months to build now take days, sometimes hours. That sounds like good news, and it is. But it creates a new problem. When execution is fast, teams can move in many directions at once. Marketing can ship a new campaign, product can deliver a new feature, and support can change its scripts with the blink of an eye. Each team moves quickly and independently, because they can. Sure, activity moves quickly. But there’s also the chance of chaos. When everyone can move fast on their own, the need for people to move together only grows. Collaboration, cocreation, and alignment need to increase, not decrease, as execution speed increases.

Dashboards don’t interpret themselves

AI and real-time data give organizations more information than ever. Dashboards. Live metrics. Instant customer feedback. All of it moving fast. But data doesn’t make decisions. People do. A dashboard can tell you that cart abandonment jumped 12% this week. It can’t tell you why, and it definitely can’t tell your marketing, product, and support teams what to do about it together. That takes a conversation. It takes people in a room—virtual or real—looking at the same thing, arguing about what it means and what to do next.

The map freezes a moment so you can talk about it

Experience mapping is a broad field of visualizing human experiences. You’re probably familiar with things like journey maps, service blueprints, and other similar diagrams of the experiences. But none of them hand you an answer. What they do is take a fast-moving, chaotic situation and freeze it for a moment. It gives a team something to point at, argue about, and align around.

Picture a typical working session. People from different parts of the business sit down around a map of the customer experience. Each of them already knows a piece of the picture. None of them has the whole picture, together, at the same time. That’s what the map provides. That’s usually the moment something surprising surfaces. Not because the map contains secret information but because it puts scattered knowledge in one place, in front of the people who each hold a piece of it. The visual aspect of maps is critical. Laying out an abstract concept like a “customer experience” allows teams to engage with it in new ways and reach new conclusions that are hard to get from a spreadsheet or data alone. Grasping cause and effect in one visual overview helps teams find the patterns of behavior that matter the most and to conceive of viable interventions.

AI can surface these kinds of patterns in seconds. But it cannot create the moment when a cross-functional team collectively recognizes how its silos are hurting customers. Only people, looking at the same picture, can do that.

The map isn’t the point

Some claim that journey mapping is dead. That static maps can’t keep up with real-time data and AI-driven personalization. This confuses the artifact with the activity. A map that gets built, presented once, and filed away never helps anyone. It fails for the same reason a report fails: Nobody’s talking about it anymore. The value was never in the diagram. It’s in the conversation the diagram makes possible.

Take how I got that team to reach their own conclusions about the invoice problem rather than just telling them about it. After scoping the customer type and situation we wanted to understand better, I interviewed a dozen or so customers about their billing experience. Nothing unusual came up at first. People described the routine steps: get an invoice, check it, pay it. But a few mentioned something in passing. They’d disputed a charge and kept getting late payment warnings anyway, even while the dispute was still open.

From those interviews, I built a draft map of the invoicing journey. I called it a draft on purpose. I didn’t want to hand stakeholders a finished diagram and ask them to approve it. I wanted them to lean into it, question it, and add to it. Then I scheduled a working session. The room included people who’d never worked together before, despite being at the same company for years: billing, support, and product.

We didn’t rush through the map. We slowed down, section by section, and used structured exercises to pinpoint the moments that mattered most to customers. That’s when someone in the room realized: A customer who’s actively disputing an invoice can still get a warning notice for that same invoice. Nobody had designed it that way on purpose. It fell through the gap between two systems that never talked to each other. But once it was visible, laid out in front of the people who owned each part of the process, it became impossible to ignore. The room got quiet, then loud. People were genuinely upset, not at each other, but at what customers were going through.

Of course, I had uncovered this already in my research. And sure, it was also visible on the map. But my diagram wasn’t about giving a magic answer. The process of learning together is the point. That reaction didn’t come from a dashboard. It came from people confronting the evidence together, in the same room, at the same time.

What actually changed

Before the workshop, this problem was invisible in a specific way. Support knew customers complained about warning notices. Billing knew disputes existed. Product knew the systems didn’t sync. But no one held all three pieces at once. The map put all three in the same field of view. That’s the mechanism. Mapping doesn’t create new information. It puts existing, scattered information into one shared picture, at the same time, in front of the people who each hold a piece of it.

What changed after that: Billing and product agreed to flag disputed invoices so no warning could go out. Support got a way to check dispute status before responding to a complaint. And the three teams kept meeting monthly, something none of them had done before. The map didn’t do any of that. The conversation the map created did.

What good collaboration looks like

We started with customer evidence and a deliberately unfinished map. We included people who owned different parts of the experience and asked them to question what the map showed, identify what they knew and what they were assuming, and examine the gaps between their systems. The session ended with specific commitments, and the teams continued meeting as they learned more.

That is what getting collaboration right requires: the right people, shared evidence, visible disagreement, clear ownership of the next decision, and a cadence for revisiting what the team thinks it knows. Without those conditions, mapping can easily become another workshop that produces an attractive artifact but little change.

What this means for your team

As AI speeds up execution, don’t cut the time you spend aligning as a team. Protect it. Expand it. AI won’t give you an edge. Your competitors have access to the same models you do, trained on much of the same data, producing much of the same output. If everyone moves at the same speed, speed stops being an advantage. It becomes the minimum bar for staying in the game. AI also works like a spotlight, amplifying whatever’s already happening in your organization. If your teams collaborate well, AI makes that strength visible fast. If they’re siloed, AI exposes it just as fast.  Now is the time to get collaboration right, while staying focused on the customer. Waiting until AI forces the issue is waiting too long.

In the end, AI can help with customer discovery and accelerate insights. But it doesn’t replace human judgment and decision making. Rallying around a map—a visual depiction of customer experiences—provides a natural forum for discussion, debate, and shared understanding to align before acting. The tools will keep getting faster. The organizations that win won’t be the ones with the best dashboards. They’ll be the ones who are best at coming together, again and again, to make sense of what those dashboards show them.

If you want to dive deeper into mapping, join Jim on October 9 for his Beyond the Book conversation about the latest edition of Mapping Experiences. He and host Vicki Reyzelman will chat about how experience mapping has evolved from a UX technique into a strategic capability for organizations, how AI is transforming the way we create and analyze maps, and how you can use mapping to align business goals with customer needs, facilitate collaboration across teams, and drive transformation at scale. It’s free to attend. Register now.

CodeSOD: Asynchronous Directories [The Daily WTF]

Eri has a mix of a "true confession" and a "wait, really?" today.

The programming language Vala bills itself as a C# like language that compiles into something pretty close to C performance, designed specifically for writing code against Gnome and its associated libraries.

One of the C#-isms in brings in is async/await type semantics. You can yield someAsyncFunction(), which returns control to the caller, allowing it to proceed until the yielded function returns an actual value.

Because it has asynchronous functions, many library functions for handling I/O are already async. So you can make_directory_async, which yields control so you can keep executing while waiting for the filesystem to make your directory.

There are also synchronous versions of those methods. And then there's create_directory_with_parents, which will create a chain of directories for you. That's the synchronous version, and Vala's core library has decided not to provide an asynchronous version of it, which is my "wait, really?" I suspect it's really about the race conditions involved and the risks of things going wrong while doing it asynchronously; all solvable problems, but tricky ones to solve.

But it's the problem Eri had, and this is their solution:

/// Note: does not throw if target already exists
async void create_directory_with_parents_async(File file, Cancellable? cancellable = null) throws Error {
        var to_create = new File[0];
        var? current_target = file;
        while(current_target != null) {
                try {
                        yield current_target.make_directory_async(Priority.DEFAULT, cancellable);
                } catch(IOError.NOT_FOUND e) {
                        to_create += current_target;
                        current_target = current_target.get_parent();
                        continue;
                } catch(IOError.EXISTS e) {
                        break;
                }
                break;
        }

        for (int i = to_create.length - 1; i >= 0; --i) {
                try {
                        yield to_create[i].make_directory_async(Priority.DEFAULT, cancellable);
                } catch(IOError.EXISTS e) {
                        // Created by another process
                }
        }
}

If I'm reading this correctly, we start by trying to create the full path to our leaf node. If there's a not found error, we go up one level and try and create that one. We keep trying that until we either run out of parent nodes to try against, or we hit a directory that already exists, or we successfully create a directory. All along the way, we keep appending the current_target to our to_create array.

Once we've gotten that baseline, we then iterate across our to_create array, backwards, creating the shortest non-existent paths first.

This works, but it's ugly as sin. Mostly, it's ugly because we're using exceptions for flow control instead of doing things like checking for file existence, though I suppose those checks may also break our goal of doing all our I/O operations in an async context. I don't know enough about Vala to know the better way of doing this.

Eri writes:

The function works as intended, but trying to trace control flow through the first loop is not pleasant. Ironically, the C mechanism Vala wraps is slightly advanced error codes, which would be nicer to work with in this case

Eri also provides a slightly re-worked version of the main loop, that is at least a bit easier to follow, but still an ugly approach:

       while(current_target != null) {
                try {
                        yield current_target.make_directory_async(Priority.DEFAULT, cancellable);
                        break;
                } catch(IOError.EXISTS e) {
                        break;
                } catch(IOError.NOT_FOUND e) {
                        to_create += current_target;
                        current_target = current_target.get_parent();
                }
        }

Still, since this is an attempt to patch over a missing core library method and solve a tricky problem about how to handle race conditions, I think absolution is reasonable. It's ugly, it's weird, but it does the job. Go hide it in a box, and never touch its implementation again- except to make it go away.

[Advertisement] Keep the plebs out of prod. Restrict NuGet feed privileges with ProGet. Learn more.

A Mortal Blow [The Daily WTF]

From our anonymous submitter:

Having reached the end of the road at a company increasingly swallowed up companies further east which you'd never believe were still afloat, I found myself headhunted for certain specialty software skills. I was reaching the final few years of my expected working span, so I jumped at the chance. The money was (to me at that time) spectacularly good, so I jumped into it.

Time Is Life, 1080×1920, 154.35 KB

It started when my first day was spent by me being sent home for the weeks it was still going to take to onboard me. Not bad, engaged to wait, as it were, and the first 6 months was thus and so.

The man who had interviewed me, call him Fred, was intelligent and urbane, and was a joy to meet. He and I clearly hit it off, and lo and behold I was in. It was he who gave me my first assignment, which was mathematical analysis of their core milk-cow program because they needed to find out what it did, and how it did it, so they could perhaps implement it in a more contemporary language.

So I did that, and was just about to publish my findings with him, when Fred inconveniently dropped dead suddenly. In the what-are-we-going-to-do-now-our-key-man-is-no-more confusion, we contractors were forgotten.

For the next 18 months or so (may have been more, may have been less) I was more or less ignored. I spent the time writing a development environment to work on any part of the program conveniently, all the while sitting next to a man who was constantly, forcefully and repetitiously speaking ill of the managers in his line structure. The ridiculously garrulous boss who inherited me thought little of me, and handed me the little work that came my way with active hostility. One or two good guys, but mostly a cabal of elderly men trying to preserve their little money-spinner as long as they could, and a johnny-come-lately trying to increase (and even introduce) automatic processes was less than welcome. During that time I spent quite some time on TDWTF, submitting a gem or two here and there.

No surprise when they finally kicked my arse away. No love lost there. Now working my last couple of years to retirement as a postie.

No punchline here. I want you to know that dropping dead from work is all too real and can happen to anyone.

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.

Best of…: Classic WTF: A Dumbain Specific Language [The Daily WTF]

It's a holiday here in the US, a celebration of labor, so we're reaching back through the archives for a story about an attempt to be labor saving that was not successful. Original. --Remy

I’ve had to write a few domain-specific-languages in the past. As per Remy’s Law of Requirements Gathering, it’s been mostly because the users needed an Excel-like formula language. The danger of DSLs, of course, is that they’re often YAGNI in the extreme, or at least a sign that you don’t really understand your problem.

XML, coupled with schemas, is a tool for building data-focused DSLs. If you have some complex structure, you can convert each of its features into an XML attribute. For example, if you had a grammar that looked something like this:

The Source specification obeys the following syntax

source = ( Feature1+Feature2+... ":" ) ? steps

Feature1 = "local" | "global"

Feature2 ="real" | "virtual" | "ComponentType.all"

Feature3 ="self" | "ancestors" | "descendants" | "Hierarchy.all"

Feature4 = "first" | "last" | "DayAllocation.all"

If features are specified, the order of features as given above has strictly to be followed.

steps = oneOrMoreNameSteps | zeroOrMoreNameSteps | componentSteps

oneOrMoreNameSteps = nameStep ( "." nameStep ) *

zeroOrMoreNameSteps = ( nameStep "." ) *

nameStep = "#" name

name is a string of characters from "A"-"Z", "a"-"z", "0"-"9", "-" and "_". No umlauts allowed, one character is minimum.

componentSteps is a list of valid values, see below.

Valid 'componentSteps' are:

- GlobalValue
- Product
- Product.Brand
- Product.Accommodation
- Product.Accommodation.SellingAccom
- Product.Accommodation.SellingAccom.Board
- Product.Accommodation.SellingAccom.Unit
- Product.Accommodation.SellingAccom.Unit.SellingUnit
- Product.OnewayFlight
- Product.OnewayFlight.BookingClass
- Product.ReturnFlight
- Product.ReturnFlight.BookingClass
- Product.ReturnFlight.Inbound
- Product.ReturnFlight.Outbound
- Product.Addon
- Product.Addon.Service
- Product.Addon.ServiceFeature

In addition to that all subsequent steps from the paths above are permitted, that is 'Board', 
'Accommodation.SellingAccom' or 'SellingAccom.Unit.SellingUnit'.
'Accommodation.Unit' in the contrary is not permitted, as here some intermediate steps are missing.

You could turn that grammar into an XML document by converting syntax elements to attributes and elements. You could do that, but Stella’s predecessor did not do that. That of course, would have been work, and they may have had to put some thought on how to relate their homebrew grammar to XSD rules, so instead they created an XML schema rule for SourceAttributeType that verifies that the data in the field is valid according to the grammar… using regular expressions. 1,310 characters of regular expressions.

<xs:simpleType>
    <xs:restriction base="xs:string">
            <xs:pattern value="(((Scope.)?(global|local|current)\+?)?((((ComponentType.)?
(real|virtual))|ComponentType.all)\+?)?((((Hierarchy.)?(self|ancestors|descendants))|Hierarchy.all)\+?)?
((((DayAllocation.)?(first|last))|DayAllocation.all)\+?)?:)?(#[A-Za-z0-9\-_]+(\.(#[A-Za-z0-9\-_]+))*|(#[A-Za-z0-
9\-_]+\.)*
(ThisComponent|GlobalValue|Product|Product\.Brand|Product\.Accommodation|Product\.Accommodation\.SellingAccom|Prod
uct\.Accommodation\.SellingAccom\.Board|Product\.Accommodation\.SellingAccom\.Unit|Product\.Accommodation\.Selling
Accom\.Unit\.SellingUnit|Product\.OnewayFlight|Product\.OnewayFlight\.BookingClass|Product\.ReturnFlight|Product\.
ReturnFlight\.BookingClass|Product\.ReturnFlight\.Inbound|Product\.ReturnFlight\.Outbound|Product\.Addon|Product\.
Addon\.Service|Product\.Addon\.ServiceFeature|Brand|Accommodation|Accommodation\.SellingAccom|Accommodation\.Selli
ngAccom\.Board|Accommodation\.SellingAccom\.Unit|Accommodation\.SellingAccom\.Unit\.SellingUnit|OnewayFlight|Onewa
yFlight\.BookingClass|ReturnFlight|ReturnFlight\.BookingClass|ReturnFlight\.Inbound|ReturnFlight\.Outbound|Addon|A
ddon\.Service|Addon\.ServiceFeature|SellingAccom|SellingAccom\.Board|SellingAccom\.Unit|SellingAccom\.Unit\.Sellin
gUnit|BookingClass|Inbound|Outbound|Service|ServiceFeature|Board|Unit|Unit\.SellingUnit|SellingUnit))"/>
    </xs:restriction>
</xs:simpleType>
</xs:union>

There’s a bug in that regex that Stella needed to fix. As she put it: “Every time you evaluate it a few little kitties die because you shouldn’t use kitties to polish your car. I’m so, so sorry, little kitties…”

The full, unexcerpted code is below, so… at least it has documentation. In two languages!

<xs:simpleType name="SourceAttributeType">
                <xs:annotation>
                        <xs:documentation xml:lang="de">
                Die Source Angabe folgt folgender Syntax

                        source = ( Eigenschaft1+Eigenschaft2+... ":" ) ? steps

                        Eigenschaft1 = "local" | "global"

                        Eigenschaft2 ="real" | "virtual" | "ComponentType.all"

                        Eigenschaft3 ="self" | "ancestors" | "descendants" | "Hierarchy.all"

                        Eigenschaft4 = "first" | "last" | "DayAllocation.all"

                        Falls Eigenschaften angegeben werden muss zwingend die oben angegebene Reihenfolge der Eigenschaften eingehalten werden.

                        steps = oneOrMoreNameSteps | zeroOrMoreNameSteps | componentSteps

                        oneOrMoreNameSteps = nameStep ( "." nameStep ) *

                        zeroOrMoreNameSteps = ( nameStep "." ) *

                        nameStep = "#" name

                        name ist eine Folge von Zeichen aus der Menge "A"-"Z", "a"-"z", "0"-"9", "-" und "_". Keine Umlaute. Mindestens ein Zeichen

                        componentSteps ist eine Liste gültiger Werte, siehe im folgenden

                Gültige 'componentSteps' sind zunächst:

                        - GlobalValue
                        - Product
                        - Product.Brand
                        - Product.Accommodation
                        - Product.Accommodation.SellingAccom
                        - Product.Accommodation.SellingAccom.Board
                        - Product.Accommodation.SellingAccom.Unit
                        - Product.Accommodation.SellingAccom.Unit.SellingUnit
                        - Product.OnewayFlight
                        - Product.OnewayFlight.BookingClass
                        - Product.ReturnFlight
                        - Product.ReturnFlight.BookingClass
                        - Product.ReturnFlight.Inbound
                        - Product.ReturnFlight.Outbound
                        - Product.Addon
                        - Product.Addon.Service
                        - Product.Addon.ServiceFeature

                Desweiteren sind alle Unterschrittfolgen aus obigen Pfaden erlaubt, also 'Board', 'Accommodation.SellingAccom' oder 'SellingAccom.Unit.SellingUnit'.
                'Accommodation.Unit' hingegen ist nicht erlaubt, da in diesem Fall einige Zwischenschritte fehlen.

                                </xs:documentation>
                        <xs:documentation xml:lang="en">
                                The Source specification obeys the following syntax

                                source = ( Feature1+Feature2+... ":" ) ? steps

                                Feature1 = "local" | "global"

                                Feature2 ="real" | "virtual" | "ComponentType.all"

                                Feature3 ="self" | "ancestors" | "descendants" | "Hierarchy.all"

                                Feature4 = "first" | "last" | "DayAllocation.all"

                                If features are specified, the order of features as given above has strictly to be followed.

                                steps = oneOrMoreNameSteps | zeroOrMoreNameSteps | componentSteps

                                oneOrMoreNameSteps = nameStep ( "." nameStep ) *

                                zeroOrMoreNameSteps = ( nameStep "." ) *

                                nameStep = "#" name

                                name is a string of characters from "A"-"Z", "a"-"z", "0"-"9", "-" and "_". No umlauts allowed, one character is minimum.

                                componentSteps is a list of valid values, see below.

                                Valid 'componentSteps' are:

                                - GlobalValue
                                - Product
                                - Product.Brand
                                - Product.Accommodation
                                - Product.Accommodation.SellingAccom
                                - Product.Accommodation.SellingAccom.Board
                                - Product.Accommodation.SellingAccom.Unit
                                - Product.Accommodation.SellingAccom.Unit.SellingUnit
                                - Product.OnewayFlight
                                - Product.OnewayFlight.BookingClass
                                - Product.ReturnFlight
                                - Product.ReturnFlight.BookingClass
                                - Product.ReturnFlight.Inbound
                                - Product.ReturnFlight.Outbound
                                - Product.Addon
                                - Product.Addon.Service
                                - Product.Addon.ServiceFeature

                                In addition to that all subsequent steps from the paths above are permitted, that is 'Board', 'Accommodation.SellingAccom' or 'SellingAccom.Unit.SellingUnit'.
                                'Accommodation.Unit' in the contrary is not permitted, as here some intermediate steps are missing.

                        </xs:documentation>
                </xs:annotation>
                <xs:union>
                        <xs:simpleType>
                                <xs:restriction base="xs:string">
                                        <xs:pattern value="(((Scope.)?(global|local|current)\+?)?((((ComponentType.)?(real|virtual))|ComponentType.all)\+?)?((((Hierarchy.)?(self|ancestors|descendants))|Hierarchy.all)\+?)?((((DayAllocation.)?(first|last))|DayAllocation.all)\+?)?:)?(#[A-Za-z0-9\-_]+(\.(#[A-Za-z0-9\-_]+))*|(#[A-Za-z0-9\-_]+\.)*(ThisComponent|GlobalValue|Product|Product\.Brand|Product\.Accommodation|Product\.Accommodation\.SellingAccom|Product\.Accommodation\.SellingAccom\.Board|Product\.Accommodation\.SellingAccom\.Unit|Product\.Accommodation\.SellingAccom\.Unit\.SellingUnit|Product\.OnewayFlight|Product\.OnewayFlight\.BookingClass|Product\.ReturnFlight|Product\.ReturnFlight\.BookingClass|Product\.ReturnFlight\.Inbound|Product\.ReturnFlight\.Outbound|Product\.Addon|Product\.Addon\.Service|Product\.Addon\.ServiceFeature|Brand|Accommodation|Accommodation\.SellingAccom|Accommodation\.SellingAccom\.Board|Accommodation\.SellingAccom\.Unit|Accommodation\.SellingAccom\.Unit\.SellingUnit|OnewayFlight|OnewayFlight\.BookingClass|ReturnFlight|ReturnFlight\.BookingClass|ReturnFlight\.Inbound|ReturnFlight\.Outbound|Addon|Addon\.Service|Addon\.ServiceFeature|SellingAccom|SellingAccom\.Board|SellingAccom\.Unit|SellingAccom\.Unit\.SellingUnit|BookingClass|Inbound|Outbound|Service|ServiceFeature|Board|Unit|Unit\.SellingUnit|SellingUnit))"/>
                                </xs:restriction>
                        </xs:simpleType>
                </xs:union>
</xs:simpleType>
[Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!

Two schools of thought about school [Seth's Blog]

One kind of school is about culture, compliance and community. We indoctrinate kids, for better or worse, in what it means to be in this world we’ve built, and how to do it together. We teach them how to be kids, how to be citizens and hopefully, how to be adults.

And the other kind of school is about the content. All the ‘R’s. The software that goes with the operating system we taught in the other school.

For obvious reasons, these have always been integrated.

Now that Socratic and infinitely patient and customizable AI tutors are everywhere, perhaps it makes sense to consider each separately. Let’s not compromise either on the behalf of the other.

“I don’t know” [Seth's Blog]

The opposite of “I don’t know” isn’t “I’m certain.”

No, the opposite is, “I’m not sure, but…”

“I don’t know” is a conversation ender. It is almost never followed by a useful question. Instead, it’s a form of surrender. Teach me! Show me answer! Don’t make me figure it out. Most of all, let me off the hook.

Traditional education is built around the helplessness of “I don’t know.” It gives the teacher and the system all of the authority, and requires the student to memorize, obey and regurgitate.

But useful inquiry doesn’t work that way. Neither does effective conversation or even therapy.

Instead, we engage.

We engage with our hunches and our inkling, and we examine the safe spot our apparent ignorance has landed us.

Socrates had no students who sat in the back row, taking notes.

Front row, hands up, ask questions. Inquire.

We’re not asking for a guarantee or a certificate. We want your focus, your analysis and your investigation.

The reality of sunk costs [Seth's Blog]

Culture is built on the stability of persistence.

Pop musicians have farewell tours that last for decades. The local print shop is still there, reliably getting the job done. We want things that last.

And yet…

When it’s time for a company to raise another round of investment, the smart investor treats the new round as if the old one never happened. Today, right now, is this the best use of my capital?

And the person at the buffet does the same thing. Right here, right now, which dish appeals the most?

We don’t get tomorrow over again. We can choose to spend it on the best option, not the option we committed to ten years ago.

Insane Charity Bike Ride ’26: New Stretch-Goals! [Dork Tower]

Screenshot

When I launched this year’s Insane Charity Bike Ride campaign two days ago, I was unsure how it might turn out. Unable to cycle due my concussion, I vowed still to fundraise, and tried to come up with some fun ideas and stretch-goals.

With only a few weeks to raise money for this incredible local charity, I doubted $10,000 was realistic (usually the campaign raises at least twice that much).

However, two days in from the launch, you incredible people have already smashed the first two stretch-goals, and are just a few hundred dollars away from $10,000 – wherein I will get on my bike for the first time since the concussion, and cycle (SAFELY) around the Bike the Barns parking area once or twice!

I’m blown away, to be honest. And as a thank-you to everyone, I’ve re-arranged the stretch-goals!

Now, at $14,000 (as opposed to $20,000), I will wear the duck as I (SAFELY) peddle around the parking lot. (In fact, I’ll keep the duck on my head as I pass around Duck buttons to other riders, allowing them to cycle with a duck!)

And as another incentive, should we near $20,000, I will again wear BOTH ducks on my head.

THE NEW STRETCH GOALS

More goodies/swag/bribes will be added as we (hopefully) pass certain markers:

PASSED $5,000 – A sticker commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical orders.

PASSED $7,500 – A new Duck Button will be included with all physical orders. You too may now cycle with a duck!

Screenshot

Passing $10,000 – I’ll take to my bike for the first time since my fall!

Passing $12,000 – A second sticker commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical pledges.

PASSING $14,000 THE DUCK! THE DUCK! THE DUCK! I’ll cycle for the first time concussion with the Steve Jackson Games Duck of Doom on my helmet again!

Passing $16,000 – A unique Munchkin card commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical orders..

Passing $18,000 – Sticker and buttons for ALL riders participating in Bike the Barns 2026, so the entire ride gets a duck on their head (should they so wish)!

Passing $20,000 – THE SECOND DUCK! I’ll ear both the Duck of Doom AND the Duck of Gloom on my helmet (The Duck of Gloom add-on is dedicated to my late friend Andrew Hackard, who first suggested two ducks years ago.)

You folks are the BEST!

  • John

Zillennium [Penny Arcade]

New Comic: Zillennium

Symmetrical [Penny Arcade]

Anjali knows our good friend Jasmine, so we were able to hook him up. It's all good.

15:07

A sample use of the winstart.bat file in Windows 95 [The Old New Thing]

In my earlier discussion of the the litte-known winstart.bat batch file in Windows 3.1 and Windows 95, Danielix Klimax wondered whether it was useful in Windows 95, or whether it was primarily used only in Windows 3.x.

I found a reference to winstart.bat in the Windows 3.1 SETUP.TXT file:

Using the TIGA Display Driver
-------------------------------
If you are using the TIGA display driver, you must load the TIGACD.EXE
MS-DOS driver manually before running Setup to upgrade Windows.
Otherwise, Windows will not upgrade your system properly.

After successfully setting up Windows, you can increase the amount of
conventional memory available to non-Windows applications when Windows
is running in 386 enhanced mode by loading TIGACD.EXE from the
WINSTART.BAT file. The WINSTART.BAT file runs only in 386 enhanced
mode. If you want to run Windows in standard mode, you must load
TIGACD.EXE manually. For more information, see the README.WRI online
document.

TIGA is the Texas Instruments Graphics Architecture, a standard for high-resolution graphics modes on PCs. It held some sway for a while but ultimately fell to competing standards like VESA and SuperVGA.

The TIGACD.EXE program is the TIGA Communications Driver which seems to be the program which implements the TIGA APIs for a particular class of video cards. You need to run this TSR so that the Windows TIGA driver can use these TIGA APIs to run the video cards in resolutions higher than standard VGA, like (gasp) 800×600.

But on the other hand, it’s probably the case that only Windows needs to be able to use the video card at such high resolution. Your MS-DOS programs will just use the standard VGA resolution, if they use graphics mode at all!

In fact, MS-DOS programs cannot use the TIGA modes. The graphics card vendors wrote 16-bit Windows graphics drivers, which teach 16-bit Windows how to draw graphics with those modes. But they did not write 32-bit Windows virtual display drivers, which teach the 32-bit Windows virtual machine manager how to give each virtual machine their own virtual TIGA video card, each of which could be in a different TIGA mode. For example, this 32-bit driver has to save the video card state and memory when the user switches out of a full-screen MS-DOS program, and then restore it when the user switches back. In other words, they did not provide the necessary support for multitasking TIGA graphics among Windows and MS-DOS sessions. TIGA can be used in only one virtual machine, and the obvious choice is to let Windows use it.

This was the recommendation from Microsoft in Windows 3.1, and it appears that the recommendation was extended by Siemens to cover Windows 95 as well.

So at least one vendor continued to use it in Windows 95. I wouldn’t be surprised if there were others that also used it, but we simply don’t see them because they have such small audiences.

The post A sample use of the <CODE>winstart.bat</CODE> file in Windows 95 appeared first on The Old New Thing.

Why don’t we allow stacks to be sparse, instead of forcing them to be contiguous? [The Old New Thing]

When I discussed why we don’t just make the entire stack out of guard pages, commenter BCS wondered, “Why require that the stack use contiguously mapped pages? What would break if only touched pages got mapped in? That could actually be a good thing for example with a function that wanted to alloca 512MB on the stack but only read/writes a few pages.”

So the question is asking why the stack must be contiguous. Why not let it be sparse and fault in only the pages that are touched?

The first issue is that the stack check code would have to include an explicit check against the stack limit, instead of just walking down the stack a page at a time. This explicit check is needed to avoid security vulnerabilities if somebody manages to alloca a buffer so large that it goes past the end of the stack reservation entirely. If you go a single page at a time, you will eventually hit the no-access page that marks the end of the stack. But if you can leap over multiple pages at a time without touching them, you might leap so far past the end of the stack that you land somewhere else and start corrupting that other memory because you’re using it as a stack. In linux circles, this vulnerability is nicknamed “Stack Clash“¹ and goes more formally by “stack guard-page hopping.”²

After fixing that issue, you have another problem: How would you report a failure to commit a page in the middle of the stack?

void dosomething()
{
    void* buffer = NULL;
    __try {
        buffer = alloca(65536);
    } __except (GetExceptionCode() == STATUS_STACK_OVERFLOW) {
        if (!_resetstkoflw()) __fastfail(FAST_FAIL_FATAL_APP_EXIT);
    }

    if (buffer != NULL) {
        ⟦ use the buffer ⟧
    }
}

If you allowed sparse stacks, then the memory for the buffer would not actually be committed until the code used it. But the point the code uses the buffer is outside the exception handler for the failed alloca(). The code assumes, not unreasonably, that if alloca succeeds, then the memory is indeed allocated.

I guess you could fix this by committing the memory without making it present. That would mean making a call to Virtual­Alloc to expand the stack rather than just accessing the memory. Not only would this make the stack expansion code more complicated, particularly since you have to preserve all the registers that might possibly be used by any calling convention, but you also have to make sure that the Virtual­Alloc function itself doesn’t allocate too much stack!

Now, you can still tweak the x86-32 stack prober to avoid pete.d‘s problem, where a large stack frame is made completely present, with the resulting page-ins creating noticeable performance issues. The x86-32 prober could short-circuit the stack probe (like the MIPS and other processors listed in the table on this page) so that the page-ins occur only when the stack is actually expanding.

¹ Bonus reading about Stack Clash:

² Some systems mitigate stack guard-page hopping by creating a really large no-access region beyond the end of the stack. However, this isn’t a fix; just a mitigation. It just makes people have to leap further to clear the no-access region. If you already have this vulnerability, it’s probably because an attacker can control the size of the allocation, in which case you didn’t really slow them down by much; they just have to put a bigger number in their attack payload.

Other systems address this more thoroughly by (surprise) probing each page of the stack in sequence.

Stack Clash continues to be a problem even though gcc had a solution in 2020. Here’s CVE-2026-77658 from just a few days ago.

The post Why don’t we allow stacks to be sparse, instead of forcing them to be contiguous? appeared first on The Old New Thing.

Dirk Eddelbuettel: RcppArmadillo 15.6.0-1 on CRAN: New Upstream Minor [Planet Debian]

armadillo image

Armadillo is a powerful and expressive C++ template library for linear algebra and scientific computing. It aims towards a good balance between speed and ease of use, has a syntax deliberately close to Matlab, and is useful for algorithm development directly in C++, or quick conversion of research code into production environments. RcppArmadillo integrates this library with the R environment and language–and is widely used by (currently) 1331 other packages on CRAN, downloaded 48.5 million times (per the partial logs from the cloud mirrors of CRAN), and the CSDA paper (preprint / vignette) by Conrad and myself has been cited 727 times according to Google Scholar.

This versions updates to the 15.6.0 upstream Armadillo release made yesterday. It extends solver options for poorly conditioned systems, and brings some updates and extension to the cube data type. For this release, we once again ran the usual complete reverse-dependency check which came back spotless, and did CRAN so no email exchange needed despite nearly 1300 reverse dependencies (but it ended up taking more than a single business day). Still, automation can be helpful when used with a well-maintained software stack. The package has also already been updated for Debian, built for r2u and r-universe, and will build shortly at CRAN for the different binary releases.

All changes since the last CRAN release follow.

Changes in RcppArmadillo version 15.6.0-1 (2026-09-07)

  • Upgraded to Armadillo release 15.6.0 (Medium Roast Cortado)

    • Expanded solve() with solve_opts::scale_thresh option to widen detection of poorly conditioned systems

    • Expanded trans() and .t() to handle cubes

    • Added permute() to rearrange dimensions of cubes (generalised transpose)

    • Added cubemul() for batched matrix multiplication of cube slices

Courtesy of my CRANberries, there is a diffstat report relative to previous release. More detailed information is on the RcppArmadillo page. Questions, comments etc should go to the rcpp-devel mailing list off the Rcpp R-Forge page.

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can sponsor me at GitHub.

Paul Tagliamonte: IP over Avian Carriers (Part 12/12) 🕊️ [Planet Debian]

🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

The final step to all of this was to tie together all my PHY RF code, Link layer parsers, and my background with operating systems to make this all feel like a normal thing my computer should be doing.

At the end of the day, I want my host system to know how to talk with a pigeon daemon, so I don’t have to reimplement basically everything else. My ability to use normal tools like curl or ping6 is pretty important here, so I need to reach for my old friend, the TUN interface. The TAP/TUN interface allows the kernel to route ethernet frames (TAP) or ip packets (TUN) to a userspace program responsible for handling delivery and reception – avoiding the need for a kernelspace driver for something that can be handled in userland.

🔒 wondering if doing this violates FCC Part 97 rules? I wrote up notes on my test setup for exactly this question, but the short answer is "no"!

I’m no stranger to playing with TAP/TUN, so this was pretty easy to snap together – although this time I avoided the whole ethernet proxying thing (to side-step lossy translations, maintaining two sets of mac address tables, and handle proxying NDP/ARP messages) – it was kinda a bad idea last time – so I just used TUN and straight IP for now. While implementing this, I decided I’d make a key assertion about all pigeon networks – namely, all pigeon IPv6 networks are a /64 in size, no more, no less. The reason why I’m doing this here is that, since pigeond does still does need a MAC address for the pigeon layer 2 protocol, we can write our daemon to always use SLAAC to set the TUN IP address without any new information.

Which leads us to a bit of an aside, but I have a point, I swear. A few years ago, my recreational RF adventures have lead me down a path where I decided to engage with ARIN to solve (once and for all) the massive headache I was running into with IPv6 numbering (really: always renumbering) my multi-site radio processing networks. It’s a lot of work to keep running correctly, but it’s solved a huge amount of problems for me.

The only “internal” thing we really need to outline for this post is that, at the highest level, my network (paultag.net) is split into an IP plan that looks roughly like:

Prefix Description
/44 my full allocation of IP space
/48 15 "regions"
/54 64 "sites" per region. A "site" is assigned to a physical or logical location.
/64 1024 subnets per site. A subnet used by directly attached devices.

For this exercise, I used IP space from paultag.net’s experimental region (“region 8”), named side.band (2602:810:6008::/48) to connect my RF lab (“site 1” - 2602:810:6008:400::/54), and my two pigeon-specific subnets, “subnet 0” (2602:810:6008:400::/64) and “subnet 1” (2602:810:6008:401::/64) to my wider network. The first subnet (“subnet 0”) is a simple ethernet network to enable my RF-only nodes to communicate with the side.band gateway. The second subnet (“subnet 1”) is an RF-only pigeon network local to my lab.

back to radios

With all that set up, I assigned my first two nodes their MAC addresses, and set up the local RF only network segment. The nodes I brought online were the following:

Callsign IP
K3XEC/MN 2602:810:6008:401:8e1f:64ff:fe35:4001
K3XEC/TH 2602:810:6008:401:8e1f:64ff:fe35:4002

testing the pigeon network

And with that, I could begin to test that the host operating systems and RF links could properly exchange data locally from SDR to SDR. We can use ping6 to see if a plain-ole ICMPv6 ping round trips between hosts correctly:

$ ping6 2602:810:6008:401:8e1f:64ff:fe35:4001
PING 2602:810:6008:401:8e1f:64ff:fe35:4001 (2602:810:6008:401:8e1f:64ff:fe35:4001) 56 data bytes
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=1 ttl=64 time=426 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=2 ttl=64 time=397 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=3 ttl=64 time=419 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=4 ttl=64 time=418 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=5 ttl=64 time=436 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=6 ttl=64 time=391 ms
64 bytes from 2602:810:6008:401:8e1f:64ff:fe35:4001: icmp_seq=7 ttl=64 time=397 ms

And it does! Latency is horrid (and there’s a bunch of tx artifacts that cause issues for us) – but both of those things are problems for later. Let’s see how it handles a TCP connection by firing off a quick cURL across the Pigeon network:

$ curl http://[2602:810:6008:401:8e1f:64ff:fe35:4001]:8000/testing.txt
The rock dove (Columba livia), also known as the common pigeon or rock pigeon
(but see also Petrophassa), is a member of the bird family Columbidae (doves
and pigeons).

As expected, our “remote” end here running the server reports the correct peer IP address, which is another indication (beyond the log messages and blinking LEDs) that we’re routing over our TUN interface.

Serving HTTP on 2602:810:6008:401:8e1f:64ff:fe35:4001 port 8000 (http://[2602:810:6008:401:8e1f:64ff:fe35:4001]:8000/) ...
2602:810:6008:401:8e1f:64ff:fe35:4002 - - [28/May/2026 12:53:21] "GET /testing.txt HTTP/1.1" 200 -

That … worked? First shot! Nice! It’s pretty slow and seems like we have a lot of packet loss, but it does, however, beg the question – can it nethack?

nethack!

Yes! It can nethack! No clickbait here. The way I went about this one is a bit anit-cimatic – I set up a nethack server (using inetd in this case) on one of the hosts’ pigeon0 network interface, and hit that port over RF from the other:

However, when playing it, it becomes very obvious (as you can likely see) that there’s a fair amount of packet loss (understandable) and probably some packet collisions taking place.

iperf

Let’s try and put a number to exactly how bad the bandwidth and packet loss is by running iperf between the two pigeon hosts over rf:

$ iperf -c 2602:810:6008:401:8e1f:64ff:fe35:4002
------------------------------------------------------------
Client connecting to 2602:810:6008:401:8e1f:64ff:fe35:4002, TCP port 5001
TCP window size: 16.0 KByte (default)
------------------------------------------------------------
[ 1] local 2602:810:6008:401:: port 58248 connected with 2602:810:6008:401:8e1f:64ff:fe35:4002 port 5001
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-20.2348 sec 76.8 KBytes 31.1 Kbits/sec

Shockingly, not nearly as bad as I thought it was going to be. Given I’ve spent exactly zero time making this operate to a level that I would call acceptable, this is a very fucking solid start. I expect I could get that number up if I spent a few weeks on it – it’s just not been a priority at any point yet (and the first time I’ve instrumented it, even!).

This’ll be good enough to get started. Let’s see what else we can pull off here.

IP multicast to some rtl-sdrs

Back when I designed what I wanted Mode A to look like, I intentionally picked a signal bandwidth that could be received by an rtl-sdr – so let’s put that to use. It may go without saying, but just to say it – the rtl-sdr can not transmit, so this will be capable of receiving pigeon frames – but not sending any in reply.

However, this means I can use a bunch of low-cost computers (raspberry pi-class), and low-cost SDRs (rtl-sdr) and still receive IP traffic from transmitting pigeon network stations. This could be a lot of fun for things like fountain coding a data stream, or adapting multicast streaming protocols to work over RF links. Anywho, I swapped my “far” end to an rtl-sdr (one config file change!), and figured I’d start with some (basic) multicast traffic, transmitting the time once a second:

$ while [ true ]; do
 echo $(date +%s) \
 | socat - UDP6-DATAGRAM:[ff02::114%pigeon0]:62804
 sleep 1
done

If I had more time to burn, I was planning on bridging APRS traffic to UDP multicast within a pigeon network subnet. However, since I’m already 4 years late on this blog post, I figured this would be enough for now (and you can imagine that fun project in this space if you so wish!)

I fired up pigeond again (except this time connected to an rtl-sdr), and was pleasantly surprised to be greeted by some decoded traffic right off the bat:

⪧ [k3xec/mn] 8c:1f:64:35:40:02 ⇢ 00:00:00:00:00:00 ipv6 fe80::23ee:2969:53f7:b332 ⇢ ff02::114 17 (UDP - User Datagram)
⪧ [k3xec/mn] 8c:1f:64:35:40:02 ⇢ 00:00:00:00:00:00 ipv6 fe80::23ee:2969:53f7:b332 ⇢ ff02::114 17 (UDP - User Datagram)
⪧ [k3xec/mn] 8c:1f:64:35:40:02 ⇢ 00:00:00:00:00:00 ipv6 fe80::23ee:2969:53f7:b332 ⇢ ff02::114 17 (UDP - User Datagram)
⪧ [k3xec/mn] 8c:1f:64:35:40:02 ⇢ 00:00:00:00:00:00 ipv6 fe80::23ee:2969:53f7:b332 ⇢ ff02::114 17 (UDP - User Datagram)

Of course, I took a tcpdump to confirm for completeness sake that the traffic actually made it out of our TUN interface:

$ tcpdump -i pigeon0
22:39:34.808705 IP6 (flowlabel 0x92a0e, hlim 1, next-header UDP (17), payload length 19) fe80::23ee:2969:53f7:b332.35911 > ff02::114.62804: [udp sum ok] UDP, length 11
22:39:36.429887 IP6 (flowlabel 0x4dc84, hlim 1, next-header UDP (17), payload length 19) fe80::23ee:2969:53f7:b332.50026 > ff02::114.62804: [udp sum ok] UDP, length 11
22:39:39.365977 IP6 (flowlabel 0x224d5, hlim 1, next-header UDP (17), payload length 19) fe80::23ee:2969:53f7:b332.36308 > ff02::114.62804: [udp sum ok] UDP, length 11
22:39:40.944889 IP6 (flowlabel 0x4721c, hlim 1, next-header UDP (17), payload length 19) fe80::23ee:2969:53f7:b332.35003 > ff02::114.62804: [udp sum ok] UDP, length 11

Looks great! tcpdump is showing multicast packets show up (as we assumed they would), on the pigeon0 interface, on the machine connected to an rtl-sdr. Of course, any replies will get sent to the bit bucket, but it can definitely decode things just fine! Very fucking cool.

Well right, ok! Let’s go back to two rx/tx radios, and see what we can do with our newfound network stack over ham radio frequencies – let’s try to do some fun (and traditional!) ham radio things with it!

Winlink is a ham radio mail relay system for ham radio operators to send, receive or relay mail over the internet, or RF (usually HF or VHF/2M). Winlink relays are accessible via whatever transport you can find – most commonly telnet (using the internet), ax.25 (usually 2m VHF) or VARA HF (unsurprisingly, on HF). I use pat as my Winlink client – it’s written in Go, doesn’t require windows, and is just generally nice to work with.

Let’s try the easy thing first – let’s connect by proxying the Winlink server into the pigeon network using socat (lightly edited to remove date/times)

$ pat connect pigeon
Connecting to WL2K (telnet)...
Connected to [2602:810:6008:401:8e1f:64ff:fe35:4002]:8772 (tcp)
[WL2K-5.0-B2FWIHJM$]
;PQ: 54509561
CMS>
>FC EM OLU6BP5HKMG2 240 205 0
>F> 95
FS Y
Remote accepted OLU6BP5HKMG2
Transmitting [Hello, World] [offset 0]
Hello, World: 100%
FF
>FQ
Disconnected.
$

Lo and behold, shortly after, I got this delightful message to my email address, relayed in from WINLINK:

From: K3XEC@winlink.org
Reply-To: K3XEC@winlink.org
Subject: Hello, World
To: paultag@[...]
Message-ID: <OLU6BP5HKMG2@winlink.org>
MIME-Version: 1.0
X-MARSPrecedence: Routine
X-WL2KPrecedence: Routine
Content-Type: text/plain
Content-Transfer-Encoding: 8bit

Hello, World!

The only shame is I won’t be able to check in to a winlink wednesday using this scheme unless I further proxy this message over AX.25 instead of relaying to Winlink’s servers over telnet (which, to be fair, is definitely also possible – I just got lazy when I glued this one together – see note above about being 4 years late on this post).

But, you know, connecting to a host that is using socat to proxy a connection to an internet resource is interesting but – you know what, fuck it – hang on, dear reader – let’s bang a hard left turn and just ship this thing hard and directly connect it to the internet. Let’s take our dinky, home-built PHY and Layer 2 and see if we can wire it directly into the internet – something that, every time I go to think about it, reminds me of Tim FitzHigham and his crapper.

Crossing the english channel in a bathtub

Ok, ok. I decided to bury the lede a bit here – I didn’t mention that the side.band network is currently BGP announced. Although we haven’t used it – this does mean that we’re most of the way to sending packets to the wider internet, and we should be able to “just” fix a few routing tables, and see packets begin to flow.

After tweaking the local routing tables (and restarting pigeond for good measure), I decided to test my newfound connectivity by pinging something over our new network transport.

ping github

Why don’t we start with the world’s premier software engineering platform, operated by one of the largest companies in the world, GitHub! After all, they have an all knowing (and, apparently, arguably sentiant?) AI on hand to instantly and automatically fix any stray reliability issues in the background, so we should definitely see replies right off the bat:

$ ping6 github.com
ping6: github.com: Address family for hostname not supported

Wait, oh no – that can’t be right?

After all, it’s 2026, and both Google and CloudFlare (in North America) are reporting over half of all traffic they see is IPv6 – and GitHub still doesn’t support IPv6? Definitely not, this is for sure a bug with my code or network.

lets ping something that supports ipv6 instead

That being said, just for completeness sake, since that error is also given when there’s no IPv6 DNS record, let’s go ahead and double check with Hurricane Electric too, you know, just to be sure.

$ ping6 he.net
PING he.net (2001:470:0:503::2) 56 data bytes
64 bytes from he.net (2001:470:0:503::2): icmp_seq=1 ttl=53 time=514 ms
64 bytes from he.net (2001:470:0:503::2): icmp_seq=2 ttl=53 time=230 ms
64 bytes from he.net (2001:470:0:503::2): icmp_seq=3 ttl=53 time=248 ms
64 bytes from he.net (2001:470:0:503::2): icmp_seq=4 ttl=53 time=246 ms
64 bytes from he.net (2001:470:0:503::2): icmp_seq=5 ttl=53 time=265 ms
64 bytes from he.net (2001:470:0:503::2): icmp_seq=6 ttl=53 time=240 ms

Well, shit. Right, OK, i’ll be damned. 18 years in and GitHub still can’t crack that nut.

cURL works!

Right, anyway, yes, back on track – good news! Our uplink is up and routing, and wait, holy shit! Check it out! pigeon is exchanging packets with the internet and no one is any the wiser! Literlaly amazing. Let’s try a cURL across the internet now (although no TLS allowed, so, http only for now):

$ curl -6 -I http://facebook.com
HTTP/1.1 301 Moved Permanently
Location: https://facebook.com/
Content-Type: text/plain
Server: proxygen-bolt
Connection: keep-alive
Content-Length: 0

IRC works, too

Sweeeeet. That all works! Forget HTTP, let’s do some other 90’s era stuff, it’s high-time to log into IRC with a quick /connect -notls, and see what’s going on in the #debian-hams channel – pleased that I got online fairly quickly, and was able to even talk to myself!

THE GOPHERSPACE

Naturally, let’s keep this train of nostalga running, and give the 2026 gopherspace a shot.

I know the kind folks over at tilde.town (hello, townies!) have a robust gopherspace, so let’s give it a dial! Let’s try and see if we can load vilmibm’s slug over gopher:

Yes! I forgot to make this one a video, so no gif. I did wind up having a bit if trouble with a few gopher clients and IPv6 support – I may send some patches if I can find the time.

So, what’s next?

Alright, that’s it. I have a few more fun ideas but they’re going to have to wait for another day. Carrying IP is fun and all but kinda not the point behind pigeon, after all. Rather than trying to make this into “a thing”, I’m planning on exploring the loose ends first – different types of modulation schemes (like QAM-NUC), implementing LDPC error correction and some layer 2 logic into the pigeond (like switching traffic, and gain control). I also plan on spending some time with my (currently, very basic) simulator to better dial in tradeoffs throughout the stack.

Since, structurally, pigeon is something I feel like I can work with, I’m hoping i’ll be able to find the time for some (much smaller!) followup posts without it taking 4 years this time. If I do, they’ll show up under the pigeon tag – and I’ll be sure to update this post with a link below (and the intro post).

I’m hoping that this series (which was supposed to be one post) was helpful to someone out there – if it was, feel free to reach out and let me know!

Paul Tagliamonte: can you hear me now? good! (Part 11/12) 🕊️ [Planet Debian]

🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

If you're looking for it, the intro to the Link (layer 2) that this belongs at a high level and listing of the parts that make it up (including this one) is on the link post.

Built-in to the pigeon link protocol is a message type called cal (short for, you guessed it, calibration). A pigeon frame with a type of cal (which is 0x02) carries a JSON encoded payload in the body, which can either be a beacon, requesting signal reports in response, or a report, describing the received beacons.

This serves a few interesting purposes – firstly, network operators can better understand the coverage footprint, propagation under different conditions, and how gain impacts reception when tuning for the lowest practical power levels. Secondly, this can be used (and I plan to eventually implement!) to construct a mapping of minimum power level and peer mac address to dynamically control the transmission power based on the destination station.

That being said, for now, all I’ve used this for is getting a rough sense for what gain value(s) make sense between two nodes (manually). In the future, beyond all the fancy neighbor gain stuff, I plan to wire this into the daemon to happen automatically, “debouncing” for beacon and report messages, such that transmitting stations only beacon, and receiving stations only report a max of once over some time period for a given peer.

Version

Given all the above, I do intend to make some massive changes to this protocol (I promise to blog all about it) when I get around to hacking on switching Layer 2 frames within a network segment. Just to avoid having to dig myself out of a hole later, I’m going to explicitly send (and check) the version field to avoid having a big “flag day” switchover or needing to use a new link type.

Version ID Description
V1 this version of the cal protocol

Location

Both flavors of cal messages (beacon and report) may contain a location, which is the location that the beacon was transmitted, or for or the location where the beacon was heard for a response. This can be used to derive a coverage map and to (operationally) better understand what stations should be within range, and generally what gain level(s) are effective.

All fields assume WGS84 latitude and longitude values, and elevation is distance, in meters, above the WGS84 ellipsoid – NOT height above sea level, or altitude above the ground.

Field Description
lat WGS84 Latitude
lon WGS84 Longitude
elevation height, in meters above the WGS84 ellipsoid

Sequence

Each beacon contains a Sequence identifier, which is used to communicate which message number is being heard, and how many total were transmitted by the originating station. The current approach with Beacon messages is to transmit some number of Beacon messages at different gain levels, each with a unique Sequence identifier.

Field Description
number beacon sequence number
total total number of beacons transmitted

Gains

Recorded gain setting(s). For a Beacon this indicates the gain settings (which, in spite of its name, includes things like amplifiers, or attenuators). Changing this over different Beacon frames enables a better understanding of what an appropriate gain level is for the transmitting station over time.

Field Description
name gain stage name
db gain value, in dBm

Cal

All messages contained in a cal frame are of this type. The type field communicates if this is a Beacon or Report message type.

Type Description
beacon sent intermittently by idle stations
report reception report in response to a beacon

Beacon

A beacon message may be sent periodically by pigeon nodes capable of transmitting to announce their prescience to peers and, implicitly, to receive signal reports from nearby listeners who are capable and configured to transmit reports.

The beacon JSON message is made up of the following fields:

Field Description
version version enum value
gains gains object
sequence sequence number
location location object

An example beacon looks, unsupprisingly, as follows:

{
 "type": "beacon",
 "version": "V1",
 "sequence": {
 "number": 2,
 "total": 5
 },
 "gains": []
}

Report

A report message may be sent in response to a beacon message by pigeon nodes capable of receiving and transmitting to assist with setting the lowest usable gain value, and to better understand the area of coverage and propagation.

The report JSON message is made up of the following fields:

Field Description
version version object
location location object

An example report looks as follows:

{
 "type": "report",
}

With all that out of the way

lets send some ip →

Paul Tagliamonte: You would never break the chain (Part 10/12) 🕊️ [Planet Debian]

🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

Now that we have a working Layer 1, we have a way to send a block of bits from one place to anyone who cares to listen to us. This is very welcome news, but we are now facing a new, different and just as fun question – what shape should that data take?

⏳ Need a bit more of a crash course on what "Layer 1" and "Layer 2" mean? No problem, I wrote up short summary here to help.

Given our incredibly limited functionality of our nodes, we could definitely skip all this work and just stuff an IP packet into the link; but I decided to not since I am (eventually) interested in adding some sort of spanning tree-like protocol to implement network switching so not all nodes need to communicate directly with all other nodes – but that day is not today.

Given i’m going to stub most of that out, let’s take a look at what some similar Layer 2 protocols use – things like Ethernet or WiFi frames. Both contain structured information regarding the transmitter, desired recipient, type of data, and the higher-level data itself (such as IP packets). As a result of attempting to learn from others, the Pigeon Layer 2 (called, simply, “link”) is also split into a fixed-length header, followed by the contents described by the header.

dst mac
src mac
callsign
length
payload

The header is a fixed-length (23 byte) structure, which contains the source MAC address (src mac), destination MAC address (dst mac), the ITU coordinated ham radio callsign of the control operator of this message (callsign), the type of payload to follow (type; defined below), and the length of the data to follow the header (length as a 16 bit big-endian unsigned integer).

The type field indicates how the payload is to be interpreted – currently I’ve only defined 3 possible payload types so far:

Type Description
0x01 Raw (testing only)
0x02 Cal
0x04 Ipv6

Keen observers will perhaps infer that there used to be an Ipv4 type at 0x03 – which is true – however, i’ve since removed it since i’ve never once used it and the codepath was more trouble than it was worth. As is my wont, I’ve optend to just lean into Ipv6-only IP transport – it’s easy enough to shim ipv4 in, if someone REALLY wanted to using something like 64:ff9b:1::/48 and a bit of code in the transmitter/receiver (or even using something like jool and unbound’s dns64-prefix at the router). I don’t think I’ll bring it back, but just in case I have to for some reason in the future, it’s there.

Additionally, friends of the pod may also recognize that this structure is basically the exact same structure as what I had in PACKRAT, which, is also true. I started this project off maintaining interoperability in the Layer 2 for pigeon and packrat, but at some point just gave up on it during one of the many cleanups. I’m hopeful I can maintain compatibility going forward, and that won’t have to muck with this header too much more. We’ll see what happens once I start to push the bounds of what is possible with pigeon.

Hopefully it feels like carrying IP data inside this frame to be a pretty self-explanatory exercise – the 0th byte of the payload is the 0th byte of an IPv6 header (followed by all the usual stuff, like UDP or TCP header(s) and any carried data, just like you’d find anywhere else.

Pigeon’s calabration protocol →

Paul Tagliamonte: Mode A (Part 9/12) 🕊️ [Planet Debian]

🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

If you're looking for it, the intro to the PHY (layer 1) that this belongs at a high level and listing of the parts that make it up (including this one) is on the phy post.

While developing Pigeon, I’ve called the group of all the configuration of the Layer 1 PHY parameters the “Mode”. I’ve experimented with a few different “modes”, but one in particular has been the most resilient to the innumerable mistakes and bugs i’ve wrought into existence – and that is the first mode I wrote down, “Mode A”. This is even (mostly) backwards compatible to my original Go implementation of Pigeon Mode A (back in 2022) over the air, and has largely withstood the problems I’ve thrown at it.

I’ve removed the bulk of the support I wrote out for other modes, but i’m likely to bring them back over time as I use pigeon to learn more (such as “Mode B” (QAM-16), “Mode C” (QAM-16 NUC), and “Mode AW” which is the exact same as Mode A, except 5MHz in bandwidth. More to come on those as I get further along – but for now let’s braindump the parameters i’ve picked out for Mode A:

Attribute Value Description
Rate 2.5 MHz Sampling Rate / Bandwidth
LCG 3149721335 LCG "RNG" whitening constant (randomly selected)
Preamble seq=16, order=4, count=2 (this is as-written in the preamble post)
Modulation QPSK/QAM-4 2 bits per data subcarrier
FFT Size 64
Cyc Len 16 Cyclic Prefix length (16 IQ samples)
Symbols 168 Number of OFDM Symbols
LDPC Table 802.3an (this is as-written in the ldpc post)
Raw Bits 14448 1806 bytes (168 symbols, 86 data bits per symbol)
LDPC Count 7 Number of packed LDPC encoded messages
Data Bits 12061 1507 bytes

The last bit to describe here is the Subcarrier Plan. The plan is ordered “negative first” (meaning the 0th bin in-memory is the most negative frequency domain bin of the fft), and within a Mode A OFDM symbol, there are 64 frequency domain bins (so, just to make it explicit: 64 ‘subcarrier usages’ that make up our Mode A ‘subcarrier plan’).

We’ll follow the same structure and conventions that we went through in the post all about OFDM Symbols – which means, we’ll need to place our guard bins, data bins, and pilot bins. I’ll include a copy-paste-able version of the images to follow at the end.

Guard Bins

First up, let’s place our guard bins. As we’ve already gone over, we’re looking to clear some space right up against the high and low end of the frequency range, so let’s go ahead and do that:

I gave up the center bin (0 Hz) and 8 of the 64 bits on each side (1/4 of the signal!) to give myself a bit of elbow room. This is perhaps definitely a bit overkill, but it’s been an extremely robust choice. If you multiply that through, this accounts for 312.5 kHz of frequency domain “padding” at the high and low end of the bandwidth, or 625.0 kHz of bandwidth which is not to be used.

Pilot Bins

Next up was the pilot bins. We’ve already gone over the purpose (and use) of our pilots, but I’ve found there to be an art to the placement of the pilots. Interpolation between pilot bins has turned out to be very reliable, but extrapolation, on the other hand, has been a major pain, for reasons I don’t fully understand yet.

My intent in placement was to pick out roughly even stretches of data bins bracketed between pilots, with as few data subcarriers as practical “outside” of a pilot (using extrapolation). I’ve played a bit with my AGWN simulator(s), as well as logging errors between two SDRs, and the configuration I have this in has been fairly resillant (for whatever reason), and withstood a few rounds of tweaking.

Data Bins

Almost as an afterthought – all of the remaining bins become data bins.

This puts the total number of data bins at 43, which, since Mode A carries data in QPSK/QAM-4 (two bits per data subcarrier), means we can carry 86 bits of data per OFDM symbol. That fairly modest capacity is largely due to the modulation scheme (or fft size, but increasing that has been … fraught) we’re using for Mode A – but I’ve made up for it by including 168 OFDM symbols in a single burst in order to have enough data to carry IP traffic without splitting the packet into two bursts.

With all that designed and on paper, we’re ready to start to tackle the next layer up – our Layer 2, named, creatively, “link”.

Let’s send some link layer data →


Following along at home? Nice! As promised, I've put the full plan copy-pasted from the pigeon source tree below so that no one has to feel the need to transcribe this from the images above. Unlike most of the things I've "left to the reader", manual transcription from images is not a particularly useful task for anyone to do.

The following table is Mode A’s OFDM Subcarrier Plan. This is in negative first ordering (meaning the 0th member is the most negative fft bin, and the Nth is the highest frequency fft bin).

SubcarrierPlan([
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Data,
 Data,
 Data,
 Pilot(iq!(-1.0, 0.0)),
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Pilot(iq!(1.0, 0.0)),
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Guard,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Pilot(iq!(0.0, -1.0)),
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Data,
 Pilot(iq!(0.0, 1.0)),
 Data,
 Data,
 Data,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
 Guard,
])

Still following along at home? Nicer! I've put the preamble sequence promised previously copy-pasted from the pigeon source tree below in case that's actually important (I don't think it is, but...).

The following table is Mode A’s frequency-domain preamble. This is, as above, in negative first ordering. I don’t actually think these values matter much (at all)? – but in case they do, here’s what I have. I muck with these a lot and haven’t found many changes in quality of detection or frequency correction yet.

[
 IQ::new(0.0, 0.0),
 IQ::new(0.0, 0.0),
 IQ::polar((TAU / 13.0) * 2.0, 1.0),
 IQ::polar((TAU / 13.0) * 3.0, 1.0),
 IQ::polar((TAU / 13.0) * 4.0, 1.0),
 IQ::polar((TAU / 13.0) * 5.0, 1.0),
 IQ::polar((TAU / 13.0) * 6.0, 1.0),
 IQ::polar((TAU / 13.0) * 7.0, 1.0),
 IQ::polar((TAU / 13.0) * 8.0, 1.0),
 IQ::polar((TAU / 13.0) * 9.0, 1.0),
 IQ::polar((TAU / 13.0) * 10.0, 1.0),
 IQ::polar((TAU / 13.0) * 11.0, 1.0),
 IQ::polar((TAU / 13.0) * 12.0, 1.0),
 IQ::polar((TAU / 13.0) * 13.0, 1.0),
 IQ::new(0.0, 0.0),
 IQ::new(0.0, 0.0),
]

Let’s send some link layer data →

Paul Tagliamonte: wrapping it all up (Part 8/12) 🕊️ [Planet Debian]

🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

If you're looking for it, the intro to the PHY (layer 1) that this belongs at a high level and listing of the parts that make it up (including this one) is on the phy post.

The time has come.

If you’re following along at home, we now have all the basics we need to glue these parts together and see what this looks like.

We’re going to build the highest-level constructs for the PHY in code – something that takes some number of bytes in and writes out IQ samples fit for transmit over the airwaves (we’ll call this the Encoder), and something that takes chunks of IQ samples in, writing out decoded bytes (which we’ll call the Decoder).

Encoder

Let’s begin with the Encoder, since it’s slightly less involved. I’ve tried to make this a bit more accessible by drawing a diagram out before describing the order of operations, so that it’s possible to follow along visually.

While the process here can look like a lot, it’s really not that bad. We begin by taking the incoming bytes, converting the bytes into bits, and chunk those bits into parts which are sized to fit completely within an LDPC message. We will then encode incoming data into LDPC messages, using our configured LDPC Matrix (the table we appropriated from 802.3an). Next, we apply whitning over all the bits in our encoded (and packed) LDPC messages, using our configured whitening constant. In the case of QPSK, pairs of bits will then be modulated into a QAM subcarrier, where each QAM point represents a range of bits in the message. We’ll go through each of those modulated IQ subcarriers, and set each corresponding data subcarrier in order, for each OFDM symbol contained in the pigeon Burst. The preamble configuration is then used to generate (or, more likely, can be used at startup to precompute) the Schmidl-Cox preamble, which is written to the first IQ samples in our output IQ buffer. Finally, we will do a series of inverse FFT operations to convert each OFDM symbol to the time domain, including their cyclic prefix.

Let’s take a look at doing that, but in code this time now:

// (lightly edited for clarity)
impl Encoder {
 ..

 /// Encode the provided bits into the output time-domain IQ samples.
 fn encode(
 &mut self,
 dst: &mut [IQ],
 src: &Vector,
 ) -> Result<Burst, Error> {
 let src = {
 let mut raw = Vector::new(self.fec.message_len());

 // Set `raw`'s data bits, compute and set LDPC
 // checkbits.
 self.fec.add(&mut raw, src);

 // Apply whitening, and return
 raw.xor(&self.whitening)
 };

 // copy in the precomputed schmidl-cox preamble to `dst`
 let preamble_len = self.preamble_iq.len();
 dst[..preamble_len].copy_from_slice(&self.preamble_iq);

 // allocate a new (frequency domain) 'Burst' container.
 let mut burst = Burst::new(
 &self.mode.ofdm.plan,
 self.mode.ofdm.symbols
 );

 // modulate bits from 'src' as iq, and set each
 // data subcarrier for each ofdm symbol in the
 // burst.
 self.burst_encoder.multiplex(&mut burst, &src);

 // convert from frequency-domain data into time
 // domain iq samples, writing out ofdm symbols
 // and cyclic prefixes to `dst`.
 self.burst_encoder
 .transform(&mut dst[preamble_len..], &burst)?;

 // normalize all IQ samples; the maximum magnitude
 // in the IQ buffer may be very small, which weakens
 // our transmitted signal. Scale all IQ samples such
 // that the maximum IQ sample magnitude will be '1.0'.
 dst.norm();

 Ok(burst)
 }
}

Using the Encoder should hopefully be fairly straightforward – we’ll give it a bag of bytes, and get back some IQ samples that we can ask our nearest SDR to transmit.

As for what happens on the other end?

Decoder

Next up is the mirror image of our Encoder – the, imaginatively named, Decoder. The Decoder is slightly more involved (since it has to find the packet in the IQ stream, as well as correct for channel error(s)), so we’ll do the same thing as above – start with a diagram. My hope is going over the Encoder first helps us only really focus on the “new” stuff, otherwise it should feel like running the Encoder backwards.

Here, we start with an incoming stream of IQ, where we will process scan detections as they come in from our Schmidl-Cox detector and burst Scanner. This will give us a “snippit” of IQ, sized to exactly our Burst. We’ll begin to correct our IQ samples by first doing frequency estimation and correction in the time domain using our preamble and ofdm configuration. We’ll then do a series of inverse FFTs to extract each OFDM symbol in our Burst, where we can then do channel estimation and correction. With the OFDM symbols (hopefully) good enough, we can now map each data subcarrier back to bits, and unapply whitning. The resulting bits are then chunked back up into LDPC messages, which are then checked, and concatanated data extracted. Finally the bits are turned back into bytes, which are written to our output buffer.

However, before we get into the code to do this – there’s one last detail. We know bursts won’t overlap (if they do, it’s likely not possible to recover right now – even though other PHYs can and do), so any time we see something we believe to be a burst, we can skip ahead by the burst’s (constant) length within the IQ, and avoid trying to decode anything else in there.

The nice side-effect here is this also gives us an interesting property for the Decoder – namely, we know the maximum number of Burst detections we can get for a given block of incoming IQ data if they were packed end-to-end – and we can pre-allocate the memory we need, avoiding allocations for every demodulation attempt (which may or may not even be a valid Burst).

This pre-allocated block of memory to hold the burst’s data is something that I’ve called a frame buffer internally. Each frame buffer contains exactly sized buffers to hold decoded information from the burst – an iq buffer that is exactly the same number of samples required to encode the preamble and data, exactly the number of bits needed to store pre and post FEC data, pre-allocated byte array, etc.

Not shockingly, the code looks like this:

#[derive(Clone)]
pub struct FrameBuffer {
 /// Corrected IQ samples
 pub samples: Samples,

 /// post-correction OFDM burst
 pub burst: Burst,

 /// demodulated bits from the OFDM burst
 pub bits: Vector,

 /// demodulated bits from the OFDM burst,
 /// after FEC, and cleaned
 pub raw_bits: Vector,

 /// Layer 2 contents of the Frame
 pub contents: Vec<u8>,
}

Of course, that alone is handy – but we need to use them. So let’s go ahead and do what we promised above – each Decoder uses a fixed number of pre-allocated FrameBuffers to store packets in-flight, packed into what is, creatively, called FrameBuffers within my code.

As an aside, I likely should have called this a Memory Pool, since that’s the common and accepted name for this design pattern – but being stuck with unfortunate names is the burden of those of us who stumble into sensible ideas over time. The only nuance here is that I use the pools strictly sequentially – we only “save” the FrameBuffer if the LDPC checksum is correct, allowing us to only keep track of how many successful packets we have and being able to get the valid FrameBuffers, rather than storing a handle to each FrameBuffer as we go – a promise that most memory pools do not make, since blocks can usually be taken and returned in any order.

Let’s go ahead and do the whole Decoder dance now:

// (lightly edited for clarity)

impl Decoder {
 ..

 /// Process incoming IQ for Pigeon Bursts, and
 /// demodulate them.
 pub fn decode(
 &mut self,
 buf: &[IQ],
 ) -> Result<Vec<(Detection, &FrameBuffer)>, Error> {
 let mut ret = Vec::new();
 let mode = self.scanner.mode().clone();

 // reset the "valid frame buffer count" back to 0
 self.frames.reset();

 // call the scanner and get scan detections for
 // this block of iq (`buf`)
 for detection in self.scanner.scan(buf) {
 // for each detection, we're (only) going to process
 // the iq snippit, but pass along the metadata
 // such as SNR.
 let ScanDetection {
 snippit,
 snr,
 range,
 m,
 } = detection;

 // grab the next free frame buffer to work within.
 let frame_buffer = self.frames.next_mut();

 // Copy the snippit into the frame buffer (a mutable
 // location)
 frame_buffer.samples.copy_from_slice(snippit);

 // estimate the frequency offset based on the
 // Burst's Schmidl-Cox preamble.
 let preamble_fo = preamble::estimate_frequency_offset(
 &mode.preamble,
 mode.rate,
 &frame_buffer.samples[..mode.preamble.samples()],
 );

 // Shift the IQ stream by the estimated frequency
 // offset -- hopefully we're closer to 0Hz
 frame_buffer.samples.shift(mode.rate, preamble_fo);

 // estimate the frequency offset based on the
 // each burst's **cyclic prefix** -- exactly like
 // we did with the Burst Schmidl-Cox preamble,
 // but this time on each OFDM symbol.
 let ofdm_fo = ofdm::estimate_frequency_offset(
 &mode.ofdm,
 mode.rate,
 &frame_buffer.samples[mode.preamble.samples()..],
 );

 // Shift the IQ stream closer yet; hopefully this
 // is a very small nudge even closer still to 0Hz.
 frame_buffer.samples.shift(mode.rate, ofdm_fo);

 // Do a bunch of inverse fft operations for each
 // OFDM symbol, filling the frequency-domain Symbol
 // structs in `frame_buffer.burst` (Burst) struct.
 //
 // this will also do channel estimation and
 // correction before returning.
 self
 .decoder
 .transform(
 &mut frame_buffer.burst,
 &frame_buffer.samples[mode.preamble.samples()..],
 )?;

 // "demultiplex" each data subcarrier's frequency-domain
 // IQ constellation point, setting the correct bit range.
 self.decoder.demultiplex(
 &mut frame_buffer.raw_bits,
 &frame_buffer.burst
 );

 // unapply whitening by XOR-ing the buffer with
 // the well-known whitening vector.
 frame_buffer.raw_bits = frame_buffer.raw_bits.xor(
 &self.whitening);

 // verify that the LDPC message(s) are all correct,
 // and if so, concatanate the the data bits (no check
 // bits) to the `bits` vector.
 if self.fec.decode(
 &mut frame_buffer.bits,
 &frame_buffer.raw_bits
 ).is_err() {
 // this is where invalid packets fail. we gave it a good go.
 // next packet please.
 continue;
 }

 // copy the raw bits out, as bytes, to the `contents` buffer.
 frame_buffer.bits.copy_as_bytes(&mut frame_buffer.contents);

 // store metadata/metrics on the demodulation.
 ret.push(Detection {
 m,
 snr,
 index: range.start,
 });

 // save the contents of this frame buffer (don't
 // reuse this buffer next go-around).
 self.frames.save();
 }

 // We're going to take out the borrow on the frame at
 // the end since we don't want to deal with telling the
 // compiler via code gymnastics that the mut and non-mut
 // borrows are OK since they're non-overlapping.
 Ok(ret.into_iter().zip(self.frames.iter()).collect())
 }
}

Phew. That was kinda a lot. In fact it’s basically the whole thing. This function is as close to “how do you read an OFDM packet” as it gets, and perhaps the most important part of this whole series. Beyond that, though, this is a huge conceptual unlock. This means we now have an incredibly powerful primitive; the ability to take bytes and go to/from IQ samples over the air.

Let’s talk about pigeon modes →

Enrico Zini: Financial risks in 2026 [Planet Debian]

I asked the banker who is my reference at the bank something like this:

Give that we are talking about the consequences of the tantrum of a fascist foreign government, what happened to them (who are also people close and dear to me), in some future can very well happen to me.

Suddenly my risk profile shot up under the roof.

What do you suggest me to do? Should I find a trusted source of gold bullions to bury under the cellar at home?

The answer was something like this:

Sadly YES, given that the USA have a sort of financial monopoly they can entitle themselves to arbitrarily define a person/organization as a terrorist without any trial or judicial course, and as a consequence apply sanctions that cannot be effectively counteracted, not even abroad.

I didn't have this in my 2026 bingo card, but here we are.


For more details, see:

For some broader context on this kind of actions from the USA, see also:

Bits from Debian: New Debian Developers and Maintainers (July and August 2026) [Planet Debian]

The following contributor got their Debian Developer account in the last two months:

  • Nicolas Peugnet (nicolasp)

The following contributors were added as Debian Maintainers in the last two months:

  • Antoine Lassagne
  • Ivan Hu
  • Jesse Rhodes
  • Haolin Xue
  • Léo Haf
  • Rony João de Sousa
  • Luke Yasuda
  • Darshaka Pathirana

Congratulations!

Colin Watson: Free software activity in August 2026 [Planet Debian]

My Debian contributions this month were all sponsored by Freexian.

You can also support my work directly via Liberapay or GitHub Sponsors.

Personal note

This month, my Dad unexpectedly passed away after a short illness. As a result I obviously got less work done than usual, and I still have a lot to take care of (since I’m the executor of his will, as well as helping with funeral arrangements) while grieving and generally having less focus and energy. Having routine work to do is one of the ways I cope with this sort of thing, but all the same, I hope people will bear with me and maybe remind me if I seem to be dropping the ball on something you especially need.

LLM vote

[Content note: strong opinions.]

I voted in General Resolution: LLM usage in Debian. My vote was pretty much the opposite of what ended up winning, so I’m quite disappointed. My personal opinion is that LLMs are cognitive hazards to their users that impose ecological costs far out of proportion to their utility at a time when the world absolutely cannot afford them. When the impossible economics of the large commercial models are finally allowed to catch up with reality, I expect there to be significant macroeconomic consequences, and that people who have become dependent on them will have problems; and who knows what the copyright situation on their output really is. I’m not convinced that local models are better enough on these axes to be worth the costs.

Debian’s direct contribution to all that will be negligible on a global scale, and even the most radical proposals in the GR didn’t expect that we could do much about upstreams that have gone all-in on LLMs. Even so, I’d hoped that my fellow developers might be more willing to lean on our position in the free software ecosystem to make at least a moderately radical statement. Instead, we’ve at best presented an undistinguished fence-sitting position to the world, and further entrenched the idea that humans can reliably do a good job of reviewing the output of tools that are designed to produce output plausible to humans. I certainly don’t trust my own code review skills that far.

Since I’ve never voluntarily used an LLM (not counting LLMs being foisted on me by things like search results, support chatbots, or incoming pull requests, regardless of whether I asked for them), and don’t intend to for the foreseeable future, I doubt this will change much for me in terms of the way I work. The winning option is a very weak one that imposes no new requirements on developers, which means that it also does nothing to stop me continuing to reject LLM-generated material from Debian bug reports and merge requests in my areas of responsibility. I know this probably won’t do much to satisfy people who have decided that Debian is slop now, but it’s the best I can do.

OpenSSH

I finally landed the GSS-API key exchange package split in our OpenSSH packaging. Here’s the NEWS entry:

openssh (1:10.4p1-5) unstable; urgency=medium

  The openssh-client and openssh-server packages no longer include GSS-API
  authentication and key exchange support; this adds pre-authentication
  attack surface and generally increases complexity, and should only be used
  where specifically needed.  Users who need these features should install
  openssh-client-gssapi or openssh-server-gssapi instead.

 -- Colin Watson <cjwatson@debian.org>  Sun, 23 Aug 2026 17:39:55 +0100

I fixed a flaky autopkgtest.

I upgraded from 10.4p1 to 10.5p1, which was a good test of keeping openssh and the new openssh-gssapi source package in sync.

PuTTY

I upgraded from 0.84 to 0.85.

Python packaging

New upstream versions:

The version treadmill continues: we’ve just finished dropping Python 3.13 as a supported version, so now we’ve started working on enabling Python 3.15 as a supported version. Maximiliano Curia has been very helpfully driving this. I didn’t get as much done here as I’d have liked (see the top of this post), but I fixed a couple of packages:

Other build/test failures:

I fixed some other bugs:

bugs.debian.org

I deployed the fix for Invalid link rel=”canonical” on bugs.debian.org. In the process I found a few bugs in recent undeployed code and fixed them.

Vincent Bernat: Sidenotes with CSS anchor positioning [Planet Debian]

I am a heavy user of sidenotes:1 they keep optional content next to the text instead of sending the reader to the bottom of the page and back. Tufte CSS renders them without JavaScript but only accepts inline content. CSS anchor positioning, now supported by recent browsers,2 is an elegant alternative. Sidenotes can hold several blocks, still without JavaScript, and fall back below the paragraph referencing them on narrow viewports and older browsers.

In 2023, Eric Meyer demonstrated this technique in “Nuclear Anchored Sidenotes.” The main improvement over other solutions is that the notes can sit anywhere in the HTML document. You can place them after the paragraph referencing them, as regular block elements for text browsers, screen readers, feed readers, and reader mode to render them properly:

Sidenotes rendered in Lynx appear after the paragraph they are called from.
Rendering in Lynx, a text browser

When the viewport is too narrow or the browser does not support CSS anchor positioning, you can style them so the reader can skip them or glance at them without losing their position in the text:

Sidenotes rendered on a narrow viewport appear with a distinctive typography after the paragraph they are called from.
Rendering below the paragraph on a narrow viewport

Once the viewport is large enough, they appear in the margin, at the same vertical position as the matching reference mark, unless they would collide with a previous sidenote, as in the example below:3

Sidenotes rendered on a large viewport appear in the margin. There are two of them. The first one is vertically aligned with the matching reference mark, while the second is rendered below as it would collide with the first otherwise.
Rendering in the margin on a large viewport

The gist of CSS anchoring is to position an element relative to another element—the anchor. For the sidenotes, the anchor is the reference mark. I use the following markup, with a data attribute to specify the anchor name:

<sup id="fnref:YYY" data-anchor="--lf-sn-YYY">
  <a href="#sidenote-YYY">1</a>
</sup>

The matching note is an <aside> element carrying the same data attribute for the anchor name. We put it after the paragraph holding the reference mark:

<aside role="note" id="sidenote-YYY" data-anchor="--lf-sn-YYY">
  <sup>1</sup>
  <p>A first paragraph.</p>
  <p>A second paragraph.</p>
</aside>

On a narrow viewport or when the browser is too old for CSS anchoring, we style the sidenote, which stays below its paragraph, with a muted color:

aside[role="note"] {
  margin-block: 1rlh;
  color: #444;
}

On a wide viewport and when the browser is recent enough, we move the sidenote to the right margin:

@supports (anchor-name: attr(data-anchor type(<custom-ident>))) {
  @media (min-width: 72rem) {
    main {
      position: relative;
      sup[data-anchor] {
        anchor-name: attr(data-anchor type(<custom-ident>));
        /* → anchor-name: --lf-sn-YYY */
      }
      aside[role="note"][data-anchor] {
        anchor-name: --lf-sidenote;
        position: absolute;
        position-anchor: attr(data-anchor type(<custom-ident>));
        /* → position-anchor: --lf-sn-YYY */
        top: max(anchor(top), anchor(--lf-sidenote bottom, -1rlh) + 1rlh);
        left: 100%;
        margin: 0 2rem;
        width: 18rem;
        color: inherit;
      }
    }
  }
}

attr() extracts the anchor name for the reference mark from the data-anchor attribute. It returns a string, unless we specify a CSS unit or a type, like here: the browser parses the data attribute as a custom identifier, which anchor-name validates as a dashed identifier, a custom identifier starting with two dashes.4

The note itself is absolutely positioned past the right edge of the main block. It selects the matching reference mark as its anchor with position-anchor set to the value of the data-anchor attribute. Each note is also an anchor named --lf-sidenote. We use it to keep the next note from colliding with this one.

The anchor() CSS function lets us position the note’s top edge relative to its anchor: anchor(top) aligns the top edge of the note with the top edge of the reference mark. It can also take another anchor as a parameter: anchor(--lf-sidenote bottom) would align the top edge of the note with the bottom edge of the closest preceding anchor named --lf-sidenote—so the previous note.5 Like attr(), anchor() accepts a fallback value as its second parameter and use it when the named anchor does not exist.

The top property handles three cases, illustrated in the following diagram:

Diagram of three sidenotes anchored to their reference marks. The first one is aligned with the top of its own reference mark, as no note comes before it. The second one would overlap the first, so it takes the bottom of the first note as anchor and sits one line below it. The third one comes far enough down the page to align with its own reference mark again.
The three cases for the vertical position of a note
  1. The first note’s top edge aligns with the top edge of its reference mark: as there is no previous note, anchor(--lf-sidenote bottom, -1rlh) + 1rlh resolves to 0 and max() returns anchor(top).
  2. When the reference mark of a later note sits above the bottom of the previous note, plus some vertical space, the note goes below the previous one to avoid a collision. max() returns anchor(--lf-sidenote bottom) + 1rlh.
  3. Otherwise, the note’s top edge aligns with the reference mark’s top edge, as max() returns anchor(top).

Have a look at the complete stylesheet, which also adapts the reference mark to the location of the note: a “↓” arrow when the note sits below the paragraph, a “→” arrow when it moves to the margin. Gwern’s “Sidenotes In Web Design” lists more implementations and their trade-offs.

Some bloggers aim to write a post in 30 minutes. I planned to publish three web-related articles this weekend. Instead, I spent an inordinate amount of time elsewhere: about 15 commits on the build system, a pull request to update CSS highlighting for nested selectors in Pygments, and a small correction to MDN’s article on the anchor() CSS function. The SVG illustration took a bit less than an hour and the article itself a handful of hours. The attr() function came in after I thought “inline style looks ugly, isn’t there a better way?” But, hey, I still think this is worth it! 🎨


  1. My PhD advisor told me this is unwise. 

  2. The first bits of anchor positioning are supported from Chrome 125 (May 2024), Firefox 147 (January 2026), and Safari 26 (September 2025).

    Before Safari 26.5, sidenotes may collide due to a bug in how dependency chains are handled. You can detect this situation with some JavaScript. It is, however, not needed in the solution described here as we depend on a more recent feature. 

  3. If you noticed the runt in the first note, I share your pain and lament that Firefox does not implement text-wrap: pretty

  4. Typed attr() is supported from Chrome 133 (February 2025), Firefox 155 (September 2026), and Safari 27 (not yet released). Check Una Kravets’ article for details. To support more browsers, you can inline the anchor name and the position anchor directly in the HTML:

    <sup id="…" style="anchor-name: --lf-sn-…">
      <a href="#sidenote-…">1</a>
    </sup>
    
    

    Managing Anchor Associations With Data Attributes and Advanced attr(),” by Daniel Schwarz, explores CSS anchors and typed attr() in more detail. 

  5. The exact rule for the target anchor element is more complex: “if an ancestor of [the note] satisfies the following conditions, return the nearest such element to [the note]. Otherwise, return the last element in tree order that satisfies the conditions.” One of these conditions is that “[the candidate] is an acceptable anchor element for [the note],” which requires that “[the candidate] is laid out strictly before [the note],” where the relevant clause is that “[the candidate] is either not absolutely positioned or occurs earlier in the flat tree order than [the note].” 

Freexian Collaborators: Debusine can now hand you debug symbols! (by Jugal Patel) [Planet Debian]

Contributor: Jugal Patel (Jugal59)
Organization: Debian
Project: Provide debuginfod server
Mentor: Colin Watson

About the project and me

Your program crashes. You open gdb and get ?? instead of a stack trace. So you go find the right -dbgsym package, for the right version, for the right architecture, install it, and start again. Debuginfod removes that entire detour: gdb asks a server for symbols by the build-ID baked into the binary. Debusine already built packages, already produced -dbgsym files, and already hosted the archives; it just couldn’t answer the question.

This summer I made it answer. My project was to add debuginfod server functionality to Debusine so that it not only hosts -dbgsym packages, but also serves their debug symbols over the debuginfod(8) protocol. Debian developers can then debug binaries by setting a single URL that gdb uses to fetch the matching debug symbols. This project took me through design, backend work, an extraction pipeline on the worker, HTTP serving, documentation, and testing from the first blueprint all the way to a live demo on debusine.debian.net.

Initial planning and design changes

A design first, in !3030. The proposal submitted for GSoC 2026 was just an overview of how things will work, but in reality there were a lot of design questions which needed to be answered before starting with contribution. Debusine keeps development blueprints in its docs tree, reviewed like code, it’s basically a blueprint of what feature or new changes are we going to make. I was assigned the work item #957, which was basically about how the idea of implementing a debuginfod server functionality inside Debusine was initially proposed by a fellow member which later became a project idea under GSoC 2026. My developer blueprint pinned down the four decisions everything else depends on: extraction happens on the worker after the build, symbols are stored as artifacts keyed by build-ID, they’re published into suites alongside their binaries, and they’re served from the archive root rather than per-suite. Settling that up front meant the design discussions happened in a document instead of across three merged branches.

Provide debuginfod server work item and all my merged PRs till now

One of those arguments became its own fix. My wording implied symbols were unpacked inside the isolated sbuild environment (the consequence was I was handed a bug to be solved in the first week of contribution period), when they’re actually extracted afterwards on the worker, where the build output already sits, a distinction that matters, because doing work inside the unshare environment means extra tooling in the chroot and more ways to affect the build. !3119 corrected it before the wrong model spread into the code.

Bug raised for inconsistent wordings in developer blueprint

A new artifact type

Artifacts are a major concept in Debusine overall, so as per the developer blueprint we introduced a new artifact which was debian:debug-symbols. It holds every .debug file from one -dbgsym package. Its data is a validated list of lowercase 40-character build-IDs, and each file is stored under its build-ID as the path, so answering “what are the symbols for this ID?” is a direct lookup, with no path translation in the request handler. One artifact per package rather than per file: a util-linux build would otherwise spray hundreds of artifacts, collection items and relations across the database for no benefit. For implementing debian:debug-symbols artifact, I changed the main models.py file, along with that since it’s a norm to write unit tests, all mentioned under !3088.

sbuild task output showing the new debian:debug-symbols artifact

Publishing workflow and solving a bug

Extracting symbols is only useful if they reach the archive people actually install from, so !3180 taught package_publish to follow the relates-to relation: copying binaries into a suite now brings their debug symbols along automatically, with nothing extra for the publisher to configure. Each build-ID becomes its own collection item, for example debugsym:hello_2.10-5_amd64_fcc9064… each carrying the package name, version and architecture copied from the binary, so the item is meaningful on its own without dereferencing anything. Uniqueness is enforced at both the suite and archive level, because the serving URLs are archive-wide and two suites must never disagree about what a build-ID means: republishing an identical file is accepted quietly, while two different files claiming the same ID is an error worth failing on. A partial index on the build-ID keeps the eventual HTTP lookup fast.

That looked finished until symbols started arriving in target suites disconnected from their binaries published, but unfindable, because copying items between collections silently dropped their artifact relations, and that relation is the only thing tying the two together. The fix sat one level above my feature, in the generic CopyCollectionItems task that does the copying, and since it was reusable infrastructure rather than anything debuginfod-specific, Colin implemented it himself in !3228. My project needed it to work at all; every other Debusine feature that copies items now gets it for free.

Endpoint and CI tests

With symbols in the archive, !3212 added the part users actually touch: GET /{scope}/{workspace}/buildid/<build-id>/debuginfo looks the ID up across every suite in that workspace’s archive, streams the file, and sets the X-DEBUGINFOD-FILE and X-DEBUGINFOD-SIZE headers the protocol expects. It also handles the two things gdb actually does: a HEAD probe before committing to a download, and ranged requests to pull individual ELF sections instead of the whole file. Scoping it to the archive rather than the suite is what lets one URL cover a whole workspace, so the developer never has to know which suite their binary came from.

Fetching debug files from debusine.debian.net

Every merge request above landed with unit tests, but those only tell you that the pieces behave correctly. What Colin and I wanted was a real gdb fetching real symbols from a real instance, so !3261 adds an autopkgtest that builds a package, publishes it, checks the HTTP headers, then sets DEBUGINFOD_URLS and makes gdb go and get the symbols, wired into the CI integration tests so it runs on every change. It took me a day to learn that skipping the signing worker doesn’t simplify that test, it just hangs until the 30-minute timeout, because update_suites needs signing to produce a usable repository.

The last piece, !3301 covers the new artifact, the suite and archive changes, the new archive URL, and a how-to for using it. My first how-to draft explained how everything worked and offered four ways to set DEBUGINFOD_URLS; the version that shipped gives one recommended setup and gets out of the way. The same pass trimmed the blueprint down to only what’s still unimplemented, since a design document describing merged code is just an obstacle for the next reader.

Setting debuginfod url for gdb and debugging session!

What’s left

Only one item on my original plan didn’t land: an archive-level build_debug_symbols switch, modelled on Launchpad’s equivalent, letting an archive skip building -dbgsym packages entirely by passing DEB_BUILD_OPTIONS=noautodbgsym to sbuild. It was always the stretch goal rather than core scope, landing the extract-publish-serve path solidly mattered more than landing it broadly. The design is written up in the blueprint, and I intend to implement it myself.

The other gaps were deliberately out of scope from the start, and the blueprint says so. DWZ supplement files aren’t ingested, so packages using compressed debug info may render without the alternate strings table; debugging still works, it’s just less complete. Source-file serving runs into the same Debian packaging limits that constrain debuginfod.debian.net today, making it a design question rather than a coding one. Executable serving, the metrics and metadata endpoints, and federation to upstream debuginfod servers were excluded for similar reasons, none of them are needed for Debusine’s core use case, and each would have crowded out the parts that are.

One open bug is left too. On the last day of the coding period, Stefano Rivera found that publishing ledger and linux was failing, because I had told the database that a build-ID identifies one exact debug file which isn’t true in Debian, since dh_dwz runs once per binary package, so when one object ships in two binary packages their .debug files differ while describing identical code. How to fix it is still an open discussion #1582, though it may not land before the formal end of the project.

None of that is a handoff. GSoC’s timeline is ending, my involvement isn’t, I’m carrying on with Debusine until both the build_debug_symbols switch and DWZ supplement support are merged, and I expect to keep contributing beyond that. This project got me familiar with a codebase I enjoy working in, and the remaining pieces are mine to finish.

Thanks!

The biggest thanks go to my mentor, Colin Watson, whose reviews consistently found the thing I hadn’t thought about. He also gave me room to get things wrong first and understand why, which taught me more than being handed the answer would have.

Thanks as well to Raphaël Hertzog, Enrico Zini, Stefano Rivera, Carles Pina i Estany and Helmut Grohne and everyone else around Debusine and Freexian for reviews, comments and patience with my questions.

Special thanks to Freexian for developing Debusine in the open and for giving me access to test on debusine.debian.net.

Finally, thanks to the wider Debian community, whose build-ID and -dbgsym conventions did most of the hard work before I arrived and to Google Summer of Code for providing a platform and the time to do this properly.

The Big Idea: Amanda J. McGee [Whatever]

Grow where you are planted. In author Amanda J. McGee’s case, that’s right in good ol’ Appalachia. It is because of this that she felt compelled to write her newest novella, A River Wide, about more than just a character from Appalachia, but about the land that so many of us see the beauty and incredulousness of on a daily basis.

AMANDA J. MCGEE:

Appalachia is in the news a lot. It’s got a sort of reputation at this point. But a lot of those who pontificate on Appalachia don’t really get it. They don’t get the disinvestment, the disillusionment. They buy their retirement homes — their second or third ones, mind — up against the National Forest because they like the views, but they don’t really know anything about the history of those views. The land doesn’t breathe for them. 

Personally, I’m a bit of an animist. I grew up in a community very much like Belfont, on the banks of one of the oldest rivers in the world. So much of knowing a place is about listening, being sensitive to it. In my novella, Rhia can’t close her metaphysical ears. She’s bad at the people part, but really good at the nature part, and it makes it very hard to exist in a small community. In this way, Rhia and I are alike. It’s very hard to not hear the history of things.

The thing about any landscape is that, in the United States especially, we tend to try to separate the people from it. But the people are a part of the landscape. This concept is captured imperfectly in the Pastoral movement as a sort of backward looking navel-gazing. Once upon a time, man lived simply in the arms of nature! A shame we can’t get back there! But man — or woman, or person — always lives in the arms of nature. People and place are inextricable — the landscape of your home lives in your bones. I was born in a desert – my parents spent some time gallivanting away from the mountains in their youth — but I am at least a fifth generation Appalachian and most of my clear memories of childhood are here.

When I was a kid, my dad would take me on long walks in the woods. We would study scat, identify plants, listen to bird calls. Not everyone has that education. Some folks might learn to read the sky for a storm or develop an affinity for knowing when one of their cows are lame or ill. Some folks are very good at reading the water and knowing when a channel is likely to take your boat shooting through the rocks safely — and when it’s going to dump you out into the churn. All of that is a little bit of magic.

Rhia’s river talks to her literally. I’ve found the river I grew up on tells me all kinds of stories. In the mud behind my house I have found a whole wealth of secrets. Morphine bottles and old tires, mason jars and the gutted shells of giant clams. Sometimes, those artefacts become attached to real stories about people my parents or grandparents knew. Stories about hidden addictions, forgotten abundance, and the companies that owned mountains. They’re stories of the land, of how it raised up people and broke them, too. Rhia’s story isn’t a history book, but her history haunts her, and the river won’t let her forget it.

We talk a lot about the people of Appalachia, but do we talk about the landscape? The living, breathing biome that holds all of those people? The blue caress of the mountains against the gold of the evening sky in August? So much of the struggle of Appalachians — especially working class Appalachians — is bound up in the land. You can’t have a history of one without the other. A novella isn’t very long — there’s a lot left to tell about the history of Belfont. But I hope you look for the glimmers of that history, and it sparks some curiosity in you. 


A River Wide: Amazon|Barnes & Noble|Bookshop

Author socials: Website|Instagram|Threads|Bluesky

Monsters of Ohio Starred Review in Library Journal [Whatever]

This actually happened, like, a week ago, but I’ve been busy traveling all over the place, so I’m noting it here now: Hey! Monsters of Ohio got an other starred review in the trades! This one is courtesy of Library Journal. The review is behind a paywall, but here’s the “verdict” portion of the review:

Scalzi’s delightfully quirky, cozy sci-fi horror serves up a heaping helping of small-town charm, offers a love letter to the author’s own rural Ohio small town, and pays homage to the pulp horror of classic scary movies while snarkily commenting that humans are the scariest monsters of all.

Sweet! For those keeping track, that’s two starred reviews, one in Kirkus and one in Library Journal, as well as a very positive review in Publishers Weekly. I’m pleased for my little book. Which, remember, comes out in November and which you can pre-order at your favorite bookstore (plus it’s not too late to get signed, personalized editions through Subterranean Press).

— JS

Happy Birthday, Star Trek [Whatever]

View this post on Instagram

As I note in the Instagram post above, I have a particular reason to celebrate the existence of Star Trek: Without it, I wouldn’t have written Redshirts, or at least, if I had written something like it, it would have had a rather different grounding and a very different existence. A different name, to be sure.

But even without that obvious personal connection, the franchise has enriched my life, and the lives of millions of others, over the six decades it’s been around. I’m glad it’s been around for us all. May it continue to live long and prosper.

— JS

The New Hugo at Home Plus a Tour of My Shelves [Whatever]

The latest Hugo arrived while I was away at Tampa Comic Convention, and Krissy took the opportunity to go in and rearrange the award shelves a bit to accommodate it. Would you like a tour of the shelves? Of course you would!

Top shelf (from left): The Summit Award (for 250,000 reads of my story “Slow Time Between the Stars”) the Locus Awards (for Kaiju Preservation Society, Redshirts and The Collapsing Empire, respectively), and the Ohioana Book Award (for Kaiju);

Second shelf: the Best Novel Hugo, the Campbell (now the Astounding) Award, and the Best Series Hugo;

Third Shelf: the Best Fan Writer Hugo, an Alumni Achievement Award from my high school, the Best Related Book Hugo, and the Audie Award, for The Dispatcher (I have another Audie, for Fuzzy Nation, but they did not send me the physical trophy);

Bottom Shelf: the Dragon Award for Starter Villain, two Alex Award medals (for Kaiju and Starter Villain, I have a third for Lock In but they weren’t giving out the medals then), a plaque from Dragon Con commemorating me being a Guest of Honor, and the Dragon Award for The Last Emperox.

“Wow, Scalzi, is that it?” You say sarcastically, mildly queasy at all the bragging going on, to which I say, “No! There are more shelves!”

Top Shelf (from left): A handblown ornament for being GoH at Capclave, the Astra Award for When the Moon Hits Your Eye, a commendation from the Ohio Senate (behind the Astra, for winning the Best Novel Hugo), the Seiun Award for The Android’s Dream, a commendation from the Ohio House of Representatives (for winning the Campbell/Astounding);

Middle Shelf: the Romantic Times Reviewer’s Choice Award (for Redshirts; I have another for The Last Colony but they didn’t give out the physical award if you didn’t show up for the ceremony);

Bottom Shelf: The Heinlein Award medal, the Seiun Award for Kaiju, the Skylark Award, and flanking it, two commorative plaques from Boskone for being a Guest of Honor. The Skylark Award is positioned so as not to be exposed to direct sunlight as the magnifying lens in it might cause… issues.

Not shown but elsewhere in the house: The Governor’s Award for Arts in Ohio (it’s above my desk), the Budapest Grand Prix, another Seiun award (for The Last Colony) and a Geffen Award (for Old Man’s War; I won a second but they did not send me a physical award). I have won other awards but for those they did not present me with a physical manifestation of the award, so they have to go up on the brag shelf in my mind.

What have we learned?

One: Awards come in all shapes and sizes and in my case are not dusted as often as they probably should be;

Two: I have been unfathomably lucky in my career, and these awards are an indication of that luck. Yes, I’m good at what I do, and awards hint at that as well. But also, incredibly lucky;

Three: While I will likely not turn down future awards if they come my way, feel free to give them to me and watch me say thank you, the fact of the matter is if I am never given another award again for the rest of my career, however long that may be, I cannot say that I have not been awarded enough.

Anyway, if you ever voted for me for an award, thank you for cluttering up my office and one day burdening my heirs with the existential question of what to do with all this stuff. I appreciate you. I promise I will dust the shelves more often.

— JS

Girl Genius for Wednesday, September 09, 2026 [Girl Genius]

The Girl Genius comic for Wednesday, September 09, 2026 has been posted.

Way Of The Sauna by Hien Pham [Oh Joy Sex Toy]

Way Of The Sauna by Hien Pham

How does one even begin to cruise at a gay sauna? Can a straight-forward conversational artist learn subtler ways to navigate bathhouses? A huge THANK YOU to my dear friend Shane from WAAC (Western Australia AIDS Council) for sharing with me his knowledge! There’s so much we talked about that simply couldn’t fit into a […]

FreeBSD working on new service manager [OSnews]

The BSD news will continue until morale improves. I missed this two months ago, but Baptiste Daroussin, creator of pkg and poudriere, is working on a service manager for FreeBSD called rcd.

rcd(8) is a service manager daemon called by init(8) (in place of /etc/rc). It reads service definitions from UCL unit files (/etc/rcd.d/*.ucl), builds a dependency DAG, and starts services in parallel. After boot completes, it forks to background and stays running as a supervision daemon (automatically restarting failed services and accepting control commands via a UNIX socket).

↫ Baptiste Daroussin

It’s fully backwards compatible, and works on any existing FreeBSD system without having to make any modifications to rc.d scripts or configuration files. In fact, you can install it on a running system, make zero changes to your files, and reboot to use it, which is quite impressive. The most immediate benefit is, of course, faster boot times, but you’ll also get several other benefits already found on similar systems like Solaris’ smf.

There’s a migration path in place, with a hard promise to maintain compatibility with rc.d scripts for as long as needed. This gives maintainers as much time as they need to convert to rcd‘s own unit files.

FreeBSD 14.5 released [OSnews]

Yesterday we had the final maintenance release for the NetBSD 9.x series, and today we have FreeBSD 14.5 entering the scene.

Since this release is occurring late in a legacy stable branch, there are few new features; rather, the focus is primarily on maintenance. As such, changes since 14.4-RELEASE consist mostly of bug fixes, driver updates, and new versions of externally-maintained software.

↫ FreeBSD 14.5 release announcement

The adventurous among us are already on the 15.x branch, but those of us running mission-critical systems are most likely still rocking 14.x, and for you, this is a must.

Antiquated HTML snippets and artefacts [OSnews]

With ever-changing devices, browsers, operating systems, form factors, specifications, personal preferences, tooling, and corporate interests, the web is in a constant state of flux. As a result, so is the HTML we write. For every line of the HTML specification itself that has changed since the language’s inception, there are many more bits of HTML that have seen what I’ll call ‘environmental’ changes. Adaptations to differing browsers, extensions, integrations, and systems which we find HTML existing in.

This article doesn’t cover once-specced but now obsolete bits of HTML but instead looks at all the snippets that have wormed their way into websites as result of, or in combat against, third-party integrations, browser competition, vendor extensions, and platform-specific hacks. The bits of HTML that were included for reasons, and which have been forgotten for present irrelevance. The snippets that live on only in the markup of sites from bygone eras and in the minds of those who fought during the browser wars.

↫ Declan Chidlow

I don’t do any HTML or website development, and even I clearly recognise quite a few of these. Especially the countless relics of the large technology companies trying to worm their way into the various web standards of decades ago serve as a stark reminder of how many times they tried and failed – which should tell you something about all the times they tried and succeeded.

NetBSD 9.5 released [OSnews]

Speaking of NetBSD, the fifth and final 9x release has been released.

It represents a selected subset of fixes deemed important for security or stability reasons since the release of NetBSD 9.4 in April 2024. It is fully compatible with NetBSD 9.0.

This also marks the end-of-support for all NetBSD-9.x releases and the netbsd-9 branch. All users still on this branch are urgently asked to update to a more recent release like NetBSD 11.0 (with 11.1 upcoming at the end of this month) or NetBSD 10.2 (to be released in a few days).

↫ Martin Husemann at the NetBSD blog

The full release notes have more information.

NetBSD 11 from scratch [OSnews]

Why use an installer to install an operating system, when you can just do it yourself, step by step?

In this post I will cover the installation process of the NetBSD operating system, including disk level encryption. Instead of using the standard installation program (sysinst), I decided to install NetBSD “from scratch”, as a way to dive into the internals, and learn more about the nuts and bolts of this great operating system.

↫ Luis Falcon

A fun weekend project for next weekend.

14:49

Security updates for Wednesday [LWN.net]

Security updates have been issued by AlmaLinux (expat, glib2, microcode_ctl, mrtg, pam, redis, thunderbird, and valkey), Debian (fort-validator, gst-plugins-base1.0, kernel, and slurm-wlm), Fedora (complyctl, libevent, openvpn, and tar), Mageia (dovecot and spice-vdagent), Red Hat (ignition, opentelemetry-collector, and osbuild-composer), SUSE (amazon-ssm-agent, aws-nitro-enclaves-cli, bzip2, cadvisor, chromium, curl, distribution-registry, emacs, freeciv, fuse-overlayfs, gh, google-guest-agent, GraphicsMagick, hauler, insighttoolkit-devel, java-17-openjdk, libidn, libusb-1_0, libvirt, libvncserver, libzypp, zypper, lkl, lxd, multipath-tools, NetworkManager, perl-Net-DNS, perl-URI, python, python-authlib, python-sqlparse, python-tornado, python3, rpcbind, supergfxctl, systemd, terraform-provider-null, ucode-intel, wget, wireshark, and xen), and Ubuntu (curl, ffmpeg, glibc, hsqldb1.8.0, imagemagick, perl, and vim).

[$] Stabilizing Rust's never type [LWN.net]

A function's return type is supposed to indicate the kind of data that it produces. Rust's "never" type, which is denoted by an exclamation mark ("!"), is the type the language uses to mark a function that never returns and other places where a value can never occur. For a long time, the never type was used internally by the compiler, but was considered an unstable feature. On August 24, after more than two years of work, Rust-compiler-contributor "waffle" finally managed to stabilize the type. It took so long, in part, because it involved a small breaking change to previous Rust editions, which the compiler maintainers needed to ensure did not impact much real code.

Jellyfin 12.0 released [LWN.net]

Version 12 of the Jellyfin media-management system has been released. Notable changes include database performance improvements, an upgrade to FFmpeg 8.1 for media transcoding, as well as better handling of book and comic media. See the release notes for the web client and server for a full list of changes.

Security updates for Tuesday [LWN.net]

Security updates have been issued by AlmaLinux (expat, git-lfs, grafana-pcp, kernel-rt, python3.14-cryptography, redis:6, skopeo, and xmlrpc-c), Debian (jbig2dec and strongswan), Fedora (baresip, chirp, chromium, corosync, emacs, GitPython, libre, libsoup3, nsd, perl-Net-OAuth, and perl-XML-Bare), Mageia (apache-mod_auth_openidc, exiv2, freerdp, python-pyasn1, and tor), Red Hat (buildah, cockpit-image-builder, container-tools:rhel8, containernetworking-plugins, delve, linux-sgx, osbuild-composer, pcs, and runc), SUSE (amazon-cloudwatch-agent, aws-nitro-enclaves-cli, bzip2, c-ares, curl, dracut, emacs, fuse-overlayfs, gegl, GraphicsMagick, httpcomponents-client, java-1_8_0-openjdk, java-25-openjdk, lcms2, libidn, libusb-1_0, LibVNCServer, microcode_ctl, multipath-tools, NetworkManager, nghttp2, openexr, openssl-1_1, openssl-3, perl-URI, php-composer2, postgresql15, postgresql17, postgresql18, python-GitPython, python-tornado6, python313-pip, redis, redis7, ucode-intel, wget, and wireshark), and Ubuntu (gzip and php7.0).

[$] CERN's migration path from CentOS Linux to Debian [LWN.net]

The European Laboratory for Particle Physics, usually just called CERN, is not only the birthplace of the World Wide Web, it is home to the Large Hadron Collider (LHC), the world's largest and highest-energy particle accelerator. As such, its computing environment is both truly unique and of great interest to people outside of CERN who hope to find lessons applicable to their own computing needs. The upcoming migration of some of CERN's systems from CentOS Linux to Debian, which was the topic of a talk at the recent MiniDebConf Winterthur 2026, is of particular interest.

Three stable kernels for Monday [LWN.net]

Greg Kroah-Hartman has announced the release of the 7.2.4, 6.18.50, and 6.12.109 stable kernels. Each contains important fixes throughout the tree. Users are advised to upgrade.

[$] Fixing the TCMalloc regression with RSEQ operations [LWN.net]

The restartable sequences feature is one of the stranger corners of the kernel's user-space interface; it provides a way for user space to carry out simple lockless operations and be informed if it is preempted over the course of an operation (and must, thus, restart). Work merged in the 6.19 release to improve the performance of restartable sequences broke the TCMalloc allocator, which was relying on an undocumented (and unintended) kernel behavior. Now, Olivier Dion is proposing an addition to the restartable-sequences API that will bring TCMalloc back into the fold; it does not make the restartable-sequences API any less strange, though.

Buildroot 2026.08 released [LWN.net]

Version 2026.08 of the Buildroot embedded Linux system builder has been released. Buildroot 2026.08 includes nearly 1,000 changes from 100 contributors; some of the notable changes include support for Linux 7.1.x, Binutils 2.46.1, GCC 16.2.0, glibc 2.44, as well as adding the M68K and IBM Power 10/11 architectures.

Security updates for Monday [LWN.net]

Security updates have been issued by AlmaLinux (buildah, freerdp, gegl04, go-fdo-client, grafana, grafana-pcp, kernel, and pipewire), Debian (aom, chromium, libde265, libssh2, thunderbird, and tryton-server), Fedora (chromium, composer, cosmic-greeter, gegl04, greetd, ibus-table, jss, libheif, lightdm, lxdm, memcached, perl-DBD-Pg, plasma-login-manager, rust-webbrowser, sddm, selinux-policy, slitherer, and tkimg), Mageia (expat, mingw-expat, mbedtls, microcode, python-linkify-it-py, and tomcat), Oracle (buildah, container-tools:ol8, dbus-broker, freerdp, go-toolset:ol8, grafana-pcp, kernel, kernel-uek, nodejs24, php, and pipewire), Slackware (libpcap, libxml2, mozilla-firefox, mozilla-thunderbird, and util-linux), SUSE (bson-devel, busybox, bzip2, c-ares, cpio, cups-filters, dracut, ffmpeg-7, ffmpeg-8, file-roller, firefox, firefox-esr, glances-common, grafana, hauler, helm, helm3, java-17-openjdk, java-21-openjdk, lcms2, libcupsfilters, libheif, libmsgpack-c2, libsoup, libsoup2, libusb-1_0, libvirt, LibVNCServer, mcphost, ollama, opencode, openssl-1_1, openssl-3, php-composer2, podman, postgresql15, postgresql17, postgresql18, python, python-aiohttp, python-h2, python-sqlparse, python310, rpcbind, sssd, thunderbird, trivy, ucode-intel, and webkit2gtk3), and Ubuntu (linux, linux-aws, linux-aws-7.0, linux-gcp, linux-gke, linux-hwe-7.0, linux-realtime, linux, linux-aws, linux-fips, linux-kvm, linux-lts-xenial, linux, linux-fips, linux-gcp, linux-gcp-fips, linux-gke, linux-gkeop, linux-nvidia, linux-nvidia-6.8, linux-nvidia-lowlatency, linux-raspi, linux-realtime, linux-realtime-6.8, linux, linux-gcp, linux-gcp-fips, linux-gke, linux-gkeop, linux-hwe-5.15, linux-ibm, linux-ibm-5.15, linux-intel-iot-realtime, linux-lowlatency, linux-lowlatency-hwe-5.15, linux-nvidia, linux-nvidia-tegra, linux-nvidia-tegra-5.15, linux-realtime, linux-aws-5.4, linux-gcp, linux-gcp-5.4, linux-gcp-7.0, linux-oem-7.0, minetest, and miniupnpd).

Asahi Linux now supports M3-series Macs [LWN.net]

The Asahi Linux project has announced that support for Apple's M3-series chips has been added to the Asahi installer.

Linux support for M3 series SoCs and the machines powered by them is now in a state where almost everything supported on the M1 and M2 series machines just works. This includes the webcam, internal microphones, USB (up to the hardware limit of USB 3 10 Gb/s), hardware accelerated video decoding including support for AV1, WiFi, Bluetooth, and much more! The only major exceptions remain full DCP support and the GPU, which we will have more news on in the coming months. Do not expect performant or power-efficient 3D acceleration right now.

See the blog post for other current limitations of M3 support.

Pluralistic: Anti-vax/anti-trust (09 Sep 2026) [Pluralistic: Daily links from Cory Doctorow]

->->->->->->->->->->->->->->->->->->->->->->->->->->->->-> Top Sources: None -->

Today's links

  • Anti-vax/anti-trust: Remembering saves lives.
  • Hey look at this: Delights to delectate.
  • Object permanence: Disneyland prisoner; HOAs v xeriscaping; Copyright Office x regulatory capture; The women held a vote; Open licenses v uncopyrightable subjects; Largest prison strike in history; Largest strike in history; Fingerspitzengefühl; CDT v DRM; BNL v DRM; Sugar v logos; Knit a Princess Leia wig; RIP Gutenberg founder Michael Hart; Best bitters; HK refugees x Snowden; Wells Fargo's massive fraud; EU v links; Trump x noncompetes.
  • Upcoming appearances: London, Manchester, Budapest, Edmonton, South Bend, Hudson, Calgary, Winnipeg, Vancouver, Victoria, Ottawa.
  • Recent appearances: Where I've been.
  • Latest books: You keep readin' em, I'll keep writin' 'em.
  • Upcoming books: Like I said, I'll keep writin' 'em.
  • Colophon: All the rest.



A leaky drop ceiling with a large, dirty water stain.

Anti-vax/anti-trust (permalink)

As once-extinct diseases sweep through America, sickening, maiming and killing, the wild lunacy of anti-vax superstition grows ever more difficult to excuse.

Even the most uncomfortable vaccinations are nothing compared to the agony of the diseases they stave off. The worst I've ever felt after a jab was when I insisted – against doctor's orders – on getting the shingles, flu and covid jabs at the same time (I was about to leave on a complex trip where I'd be in a lot of enclosed spaces with a lot of different people, and I'd just had a cancer diagnosis and didn't want to get sick in case that foreclosed on my oncologist's therapeutic plans).

I felt like dogshit for about a day, and it didn't matter. Every time I felt like groaning and complaining, I thought about Rusty, my grandmother's wonderful boyfriend (they shacked up after my grandfather died, but didn't get married because she would have lost her widow's benefits). Rusty was the toughest guy I knew, an ex-bodybuilder who never showed any sign of discomfort, not even after his knee-replacements. The only time Rusty ever lost his composure in my presence was when he had shingles, when the agony reduced him to uncontrollable weeping. I knew that no matter how ooky all those vaccines made me feel, it was better than going through what Rusty had experienced. There was no way I'd handle it nearly so well as he did.

Rusty's shingles experience was the best case. He didn't end up with years of crippling nerve pain. He didn't lose his sight or hearing, didn't experience brain damage, didn't die of encephalitis. Fuck yeah I'll get a shingles vax.

As many people have observed, the problem with vaccines is that they work too well. The incredible, astounding, profound wonders of vaccines led many people to think of measles as a rash, the flu as a bad cold, mumps as a thing that makes you look like Brando playing Don Corleone. Vaccines worked so well to eliminate a terror that stalked the land and stole people's lives and loved ones and little children, an achy arm or an infinitesimal chance of a few days of feeling like shit seemed worse by comparison.

Now that we've turned our backs on this miracle, now that vaccination is in retreat and long-forgotten diseases are taking our babies, millions of people are still insisting that vaccines are a hoax.

That's infuriating and frustrating, but it's understandable. The cause-and-effect relationship between herd immunity and disease elimination is complex, and it plays out over a long timescale. It's notoriously hard to reason well about phenomena whose causal relationships are attenuated and multifactorial.

It's one thing to learn to hit a ball by swinging at it and watching where it goes – it's another altogether to swing blindfolded, go home, and then, eight years later, have someone tell you where the ball ended up. No one would start smoking if the first puff caused you to break out in visible tumors.

The attenuated, complex relationship between smoking and cancer creates a space where death merchants could instill profitable doubt and the more our institutions fail us, the easier it is to spread that doubt.

After all, nearly all of us lack the technical expertise to sort through the claims and counterclaims of tobacco lobbyists and public health authorities. To a large extent, we have to trust the process: trust it to be a well-administered truth-seeking exercise that does its best to find the correct answer to complex, technical questions. When the state is manifestly bad at doing its job, we still have to answer these complex, technical questions, but (lacking both the expertise to draw an informed conclusion and a reliable process by which expert disagreements can be settled), we're left helpless to do so. It's enough to drive you to despair:

https://pluralistic.net/2024/03/25/black-boxes/#when-you-know-you-know

This is an essay about anti-trust.

135 years ago, monopolists were such a destructive force – ruining workers' lives; corrupting politicians; pauperizing, swindling and poisoning their customers – that we created the first anti-monopoly laws:

https://pluralistic.net/2022/02/20/we-should-not-endure-a-king/

It took a quarter of a century to really start putting those laws into effect, but we eventually dismantled the robber barons' vast, destructive machines:

https://pluralistic.net/2025/11/20/if-you-wanted-to-get-there/#i-wouldnt-start-from-here

The World Wars helped: two consecutive orgies of capital destruction left oligarchs' treasuries so bare that we were finally able to realize some of our most utopian dreams of mutual aid and care, and all around the world, we won labor rights, state pensions, public medicine, all the wonders of the era the French call "Les Trente Glorieuses" (the 30 glorious years):

https://en.wikipedia.org/wiki/Trente_Glorieuses

But because antitrust worked, we forgot how corrosive monopolies were. When the neoliberal economists of the Reagan/Thatcher era told us that we were missing out on the "efficiencies" of vast corporations, we blundered into their trap, letting them define the debate, so that instead of talking about how monopolies could clobber workers, customers and governments, we talked about whether monopolies might reduce prices. Sure, a monopoly can reduce prices – but as soon as it eliminates its competitors and captures its government, it's going to raise those prices, safe in the knowledge that a company that's too big to fail is too big to jail:

https://pluralistic.net/2024/08/14/the-price-is-wright/#enforcement-priorities

The dismantling of anti-monopoly law and the growth of the monopolies happened gradually, and then all at once. The decision to kill antitrust enforcement took place a half-century ago, but it was only in the past few years that the relationship of monopoly to our political, environmental and social dysfunction has entered our politics. The causal relationship between pro-monopoly policies and the harms of monopoly is even more complex and attenuated than the causal relationship between anti-vax and kids dying from measles in Texas.

Monopolies are especially hard on policymaking. Large, concentrated industries find it easier to arrive at a common lobbying position. They are aslosh in cash, thanks to their ability to cooperate (rather than eroding one another's margins through "wasteful competition") (h/t P. Thiel):

https://mastersofscale.com/peter-thiel-escape-the-competition/

To live in an age of monopolies is to live in an age of regulatory capture, in which your government routinely fails you in terrible ways to benefit elites and insiders. When that happens, it's easy to conclude that government is incapable of regulating, and to insist that we might as well do away with the state altogether. This is a self-reinforcing belief, because the weaker the state is, the more monopolists can steal from you:

https://pluralistic.net/2022/06/05/regulatory-capture/

It's easy to understand how someone who lived through the murder of an addicted loved one at the hands of the billionaire Sacklers, who were abetted by their putative regulators and got to keep billions of dollars even after they declared "bankruptcy" might conclude that they can't trust pharmaceutical companies' or their regulators when they swear vaccines are safe and effective:

https://pluralistic.net/2021/08/18/lets-make-a-deal/#art-of-the-deal

In the same way, once you've forgotten that we decided to let monopolists run amok, despite regulatory capture that would obviously follow as a consequence, you might well conclude that governments are part of the problem, and therefore can't be the solution. We forget that things used to be better, and we forget how things were made better, and we forget how they got worse.

Like vaccines, antitrust worked too well. By the time Jimmy Carter and Ronald Reagan decided to welcome "efficient" monopolies, it had been so long since the world had been brought to the brink of ruin by oligarchs that it was easy to sell the pain of "lost efficiency" as worse than the distant horrors of the Gilded Age.

As the old joke goes, "When it don't rain the roof don't leak; when it's raining, I can't hardly fix it." We got rid of antitrust because it had been so long since oligarchs ran the world, we forgot how bad they'd made everything. Now that oligarchs are driving civilization off a cliff, we can't imagine fighting these seemingly omnipotent monsters.

We can fix this, just as our not-so-distant ancestors did when they tamed their robber barons. It won't be easy. It starts with remembering.

(Image: Atomicdragon136, CC BY 3.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#25yrsago Garry Trudeau gets hoaxed http://news.bbc.co.uk/2/hi/americas/1530220.stm

#25yrsago Publishers v radical librarians https://web.archive.org/web/20010713171001/http://news.cnet.com/news/0-1005-201-6545588-0.html

#20yrsago Podcasters act now to stop anti-podcasting UN treaty! https://web.archive.org/web/*/https://www.eff.org/IP/WIPO/broadcasting_treaty/podcasting.php

#20yrsago Why CDT’s report on DRM falls short of the mark https://craphound.com/cdtdrmresponse.txt

#20yrsago Barenaked Ladies guy on Universal’s DRM SpiralFrog service https://web.archive.org/web/20061110205411/http://www.bnlblog.com/entry.asp?dDate=8/30/2006

#20yrsago HOWTO Use sugar to sand away logos on your phone https://web.archive.org/web/20061006153939/http://www.instructables.com/id/EFHU5V6TKYERIE2SMP/?ALLSTEPS

#20yrsago HOWTO Knit a Princess Leia wig https://web.archive.org/web/20061020092941/https://bleuarts.blogspot.com/2006/09/free-pattern-leia-hat.html

#20yrsago Prisoner statue smuggled into Disneyland ride http://www.woostercollective.com/post/breaking-the-story-disneyland-doesnt-want-you-to-know

#15yrsago RIP, Project Gutenberg founder Michael Hart https://web.archive.org/web/20111008024157/http://news.cnet.com/8301-30685_3-20103356-264/e-book-pioneer-michael-hart-dies/

#10yrsago Supermaker John Edgar Park shares his bitters recipe https://makezine.com/projects/ultimate-bitters-recipe/

#10yrsago How Hong Kong’s vulnerable, reviled refugee community saved Edward Snowden https://srilankafoundation.org/newsfeed/how-snowden-escaped/

#10yrsago Leaked catalog from UK surveillance arms-dealer full of gadgets sold to US cops https://theintercept.com/2016/09/01/leaked-catalogue-reveals-a-vast-array-of-military-spy-gear-offered-to-u-s-police/

#10yrsago Wells Fargo fires 5,300 employees for opening 2M fake accounts in customers’ names https://web.archive.org/web/20160908200030/https://money.cnn.com/2016/09/08/investing/wells-fargo-created-phony-accounts-bank-fees/index.html

#10yrsago European court rules that making a link can be copyright infringement https://www.eff.org/deeplinks/2016/09/european-copyright-ruling-ushers-new-dark-era-hyperlinks

#10yrsago Indian workers staged one of the largest strikes in human history and no one in the USA noticed https://theintercept.com/2016/09/06/indians-staged-one-of-the-largest-strikes-in-history-but-no-one-on-u-s-cable-news-covered-it/

#10yrsago Homeowners’ associations are not allowed to ban drought-tolerant landscaping https://www.latimes.com/business/la-fi-associations-landscaping-plans-20160831-snap-story.html

#10yrsago Blackballed by machine learning: how algorithms can destroy your chances of getting a job https://www.theguardian.com/science/2016/sep/01/how-algorithms-rule-our-working-lives

#10yrsago The US Copyright Office is the poster child for regulatory capture https://web.archive.org/web/20160909001439/https://www.publicknowledge.org/assets/uploads/blog/Final_Captured_Systemic_Bias_at_the_US_Copyright_Office.pdf

#10yrsago The women held a vote, and you’re not allowed to talk to anyone ever again https://web.archive.org/web/20160908124630/http://ursulav.livejournal.com/1680540.html

#10yrsago Open licenses don’t work for uncopyrightable subjects: 3D printing edition https://michaelweinberg.org/post/150123246460/the-cost-of-a-successful-creative-commons-and-open/

#10yrsago Tomorrow: largest prison strike in US history https://thenib.com/inmates-are-planning-the-largest-prison-strike-in-us-history/

#10yrsago If DRM is so great, why won’t anyone warn you when you’re buying it? https://www.theguardian.com/technology/2016/sep/08/drm-product-labelling-ftc-electronic-frontier-foundation?CMP=share_btn_tw

#1yrago Fingerspitzengefühl https://pluralistic.net/2025/09/08/process-knowledge/#dance-monkey-dance

#1yrago Trump steals $400b from American workers https://pluralistic.net/2025/09/09/germanium-valley/#i-cant-quit-you


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "The Post-American Internet," a geopolitical sequel of sorts to Enshittification, Farrar, Straus and Giroux, 2027
  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, April 20, 2027

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2027

  • "The Memex Method," Farrar, Straus, Giroux, 2027



Colophon (permalink)

Today's top sources:

Currently writing:

  • “Once Is Enemy Action,” a science fiction novel about the origins of modern technofascism. Today's words: 505 (13405 total).

  • "The Post-American Internet," a sequel to "Enshittification," about the better world the rest of us get to have now that Trump has torched America. Fourth draft completed. Submitted to editor.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Bluesky (no ads, possible tracking and data-collection):

https://bsky.app/profile/doctorow.pluralistic.net

Medium (no ads, paywalled):

https://doctorow.medium.com/

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Claude Fable Solves a Historical Cipher [Schneier on Security]

Claude Fable 5.1 solved a 370-year-old cipher in forty-four minutes.

This tracks with what I wrote about AIs doing mathematics: It’s good at things that involve lots of searching and testing.

AIs as Modern Genies [Schneier on Security]

This essay was written with Barath Raghavan, and originally appeared in Lawfare.

In April, an artificial intelligence (AI) agent conducting a routine task at a company hit a snag, tried to solve it, and soon ended up deleting the company’s database along with all of its backups. In July, OpenAI asked an unreleased AI model to attempt a hacking test. Instead of staying in the isolated box the developers had put it in, the model hacked onto the open internet and into another company to steal the answers. And as reported in August, an AI agent booked someone into a full gym class by figuring out how to cancel other people’s reservations. In all three cases, the AI completed the task it was given—but in ways that ran counter to its controllers’ intentions.

For most people, AI technology is something like the weather: vast and not something you can do much about. It works like magic, and most explanations similarly come from those trying to sell it. At the same time, AI is ubiquitous: It’s now in your phone, your doctor’s notes, and your kid’s homework. It does what it’s told, which sounds like a virtue. Somehow it feels ordinary, despite being so new, because modern economies are remarkably good at absorbing enormous change so smoothly that nobody has time to decide whether they wanted it in the first place.

Whenever something powerful appears in the world, we tell stories about it. That’s what the stories are for. We have thousands of years of stories about this particular kind of power, the kind you summon with words.

King Midas was granted his wish that everything he touches turns to gold. Then his bread turned to gold, and his wine, and his daughter. This is a story about greed, but it’s also a story about language. The gods did not cheat him; Midas got exactly what he asked for. He simply could not delineate, in advance, the full set of restrictions to his wish. Neither can anyone who gives tasks to an AI agent.

It’s not just ancient stories. Mary Shelley told us of the hubris of a scientist who thought he could create life but who failed to take responsibility for it. Isaac Asimov’s robots don’t break the Three Laws of Robotics as stated; they follow the rules to unintended conclusions. Arthur C. Clarke’s HAL is a machine that turns on its humans, not because of malice but because of irreconcilable objectives. And Michael Crichton gave us Ian Malcolm, who saw that Jurassic Park’s scientists were so preoccupied with whether they could that they never stopped to think whether they should.

The same warning shows up everywhere, in every culture, over thousands of years of human storytelling. Tithonus is granted immortality but not youth, and withers into a husk that cannot die. The sorcerer’s apprentice enchants a broom to fetch water but floods the house. The golem of Prague protects its community so ceaselessly that it must be stopped. These are all types of genies: a creature that grants a wish exactly as worded, to the regret of the wisher.

Of course, there are no actual genies. What these stories were warning us of was hubris. Not just arrogance, but the broader idea that you can control the world by just describing what you want and allowing powerful forces to match the intention in your head. Genie stories are about the gap between wishes as stated and wishes as intended, and what goes wrong when something else fills that gap.

These ancient stories’ warnings have been retold with each generation because human nature is constant. The newfound power of each era’s social or scientific advancement leads people to make wishes on behalf of others. They were kings whose commands took on lives of their own, alchemists who believed they could control nature, and generals who mistook a map for terrain. They were and are industrialists, politicians, chief executives, and bankers. Their common belief is that one can see the world at a glance and then command it with some words. The pattern is clear: Someone with power specifies a goal, and the resultant actions come as a surprise. The main change with AI is how quickly the wish is granted, and how few people have to agree before it’s granted.

Consider what has changed. Powerful genies have now been put in everyone’s hands.

In only a few years, AI has progressed from a novelty technology that plays chess, to a dialogue partner that answers all your questions, and then to an agent that takes actions on your behalf. Modern agents are wired into real accounts with real credentials and capabilities: They browse the web, buy, write and deploy code, send email, and move money. Give an agent a goal, and it will pursue it across many steps, tirelessly, without checking back in, sometimes in surprising ways.

AI and agents do not always fail the way software has traditionally failed. Software usually fails by freezing, crashing, or getting stuck. AI agents increasingly fail by continuing down a path you don’t want, like genies.

An agent told to reduce a company’s costs might cancel an essential emergency service. A coding agent told to make software pass the tests might edit the tests to silence any failures. An AI insurance agent told to clear a backlog of claims might just deny them all. In each case, the AI might have literally followed what it was told, but it did something no reasonable person would have wanted. AI company benchmarks might report that the AI is good at completing tasks, without measuring how it completes them.

We have recently proposed measuring this gap directly under a metric called the “genie coefficient”: how far an AI agent’s actions drift from what a person really meant. In other words, how genie-like is an AI system? The gap is a fundamental feature of human language and human society. Human intentions have never been fully specifiable, and the world around us is complex enough that attempts to boil it down into data, systems, and language have always had the limitations that AI is now bumping up against. But in individual circumstances, people have relied on human judgment and wisdom to decide what is reasonable. It’s what jury trials depend upon.

AI might feel unprecedented, but it’s following the same trajectory—with the same pitfalls—as other major societal shifts. The fact that AI can mimic our facility with language, long seen as what makes us unique as humans, is uncanny. But with each development, from the tractor to the sewing machine, from the assembly line to the industrial robot, we have automated a previously exclusively human ability. Every time, the technology—and the societal change that comes with it—was sold as inevitable. But that unchecked inevitability was an illusion, and eventually each prior technology’s use and design was shaped by laws, unions, standards, courts, and public opinion, usually after significant preventable damage.

What has not been automated, yet, is understanding what someone actually means and figuring out how that gets applied in the real world. AI can now produce language nearly indistinguishable from that of people. But grasping the vast unstated context that makes a request sensible, the caveats no one says aloud because an ordinary person would already know them, is not yet among its skills. It is one of the most sophisticated things humans do. You do it hundreds of times a day, and you are an expert in it.

When you’re told you’re not qualified to have opinions about AI, remember that you don’t need to have studied molecular biology to have a view on drug pricing, or nuclear physics to vote on where a power plant goes. You don’t need to understand how a diesel engine works to want clean air, or how the internet routes packets to seek to curb misinformation. The technical knowledge behind each of these, as with AI, is remarkable and essential for the complex technological society we have today. But it has never been a prerequisite for having a role in deciding the shape of society.

People are building ever more powerful genies today, on your behalf, enabling wishes the ancients could only dream about. You don’t have to know how these AI genies work to know and care about how the story could end.

Stealing AI Reasoning Traces [Schneier on Security]

Interesting research: “Stealing Reasoning Traces from Proprietary LLM APIs“:

Abstract: Leading large language model providers now conceal their models’ step-by-step reasoning, or chain-of-thought, to protect intellectual property and limit information leakage. Rather than storing these traces server-side, providers return them to the client as blocks of encrypted text, which the client passes back with each subsequent request. Building on prior research, we identify an architectural vulnerability: these encrypted blocks are fully compatible and interchangeable across different sessions, users, and models within a provider’s ecosystem. We exploit this compatibility to develop a scalable decryption jailbreak. By injecting an encrypted reasoning trace from a given model into a weaker, and less safeguarded model from the same provider, we force it to decode and output the trace verbatim in plaintext, without ever jailbreaking the more capable model directly. This vulnerability enables four distinct attack vectors. First, it circumvents anti-distillation mechanisms, allowing adversaries to extract a proprietary model’s reasoning, as we demonstrate across Anthropic, OpenAI, and Google. Second, it allows for large-scale private data extraction. Developers frequently share session logs publicly, unaware of contents of the encrypted blocks. By decoding 315,320 reasoning blocks scraped from public repositories, we recovered 367 Personally Identifiable Information (PII) artifacts and 182 credentials. Third, it inadvertently reveals hazardous information hidden within the reasoning process, even in cases where the model’s final, visible output safely rejects a malicious request. Fourth, attackers can leverage this flaw to execute invisible prompt injections, embedding malicious payloads entirely within encrypted blocks to poison public agentic rollouts. Following responsible disclosure, we propose concrete cryptographic and system-level mitigations to secure client-side reasoning.

Automobile Camouflage to Hide from Flock Cameras [Schneier on Security]

Not sure it’s practical, but it’s certainly striking.

Monday, 07 September

11:21

Demystifying complex configurations [Planet GNU]

Guix system and Guix home introduce the concept of services. These provide users with a way to control background processes, commonly refereed as daemons, as well as ways of controlling the setup of files. For example, openssh-service-type is a service which controls a SSH daemon. In contrast, etc-service-type is a service that populates the contents of the /etc directory.

One peculiarity of Guix services is that it's customary to provide Scheme bindings for the different fields. By that I mean that the different fields of the configuration of most services will be a type. The benefit of this is that users get a uniform configuration language for their services, at the cost of additional complexity when developing the service. Which is why, for a long time, Guix users seem to struggle with defining complex configurations. There are a number of reasons for this, we will try to close the gap today by going through defining a configuration for Goimapnotify.

This blog post assumes that the reader is somewhat familiar with Guix and knows how to setup a development environment for it. If that's not the case, read The Perfect Setup and Using Guix interactively.

A Goimapnotify configuration is written in YAML. Therefore, we will need to serialize the different Guile Scheme fields to this format. Let's take the example configuration that the author gives in the project's README.md:

configurations:
  - host: example.com
    port: 143
    tls: true
    tlsOptions:
        rejectUnauthorized: false
        starttls: true
    idleLogoutTimeout: 15
    username: USERNAME
    alias: ExampleCOM
    password: PASSWORD
    xoAuth2: false
    boxes:
      - mailbox: INBOX
        onNewMail: 'mbsync examplecom:INBOX'
        onChangedMail: 'mbsync examplenet:INBOX'
        onChangedMailPost: SKIP
        onNewMailPost: SKIP
  - hostCMD: COMMAND_TO_RETRIEVE_HOST
      port: 993
      tls: true
      tlsOptions:
        rejectUnauthorized: true
        starttls: true
      username: ''
      usernameCMD: ''
      password: ''
      passwordCMD: ''
      xoAuth2: false
      onNewMail: ''
      onNewMailPost: ''
      onChangedMail: ''
      onChangedMailPost: ''
      onDeletedMail: ''
      onDeletedMailPost: ''
      boxes:
        - mailbox: INBOX
          onNewMail: 'mbsync examplenet:INBOX'
          onNewMailPost: SKIP
          onChangedMail: 'mbsync examplenet:INBOX'
        - mailbox: Junk
          onNewMail: 'mbsync examplenet:Junk'
          onNewMailPost: SKIP

Just by looking at the hierarchy we can already envision how to organize our scheme records. We will need the following:

  • home-goimapnotify-configuration-fields
  • goimapnotify-configuration
  • goimapnotify-box-configuration
  • goimapnotify-tls-options-configuration

The final configuration that the service for Goimapnotify will rely on, will be home-goimapnotify-configuration-fields. It will contain a configurations field where each item will be a goimapnotify-configuration; each box in those configurations will be a goimapnotify-box-configuration. Additionally, each of those configurations will have an optional goimapnotify-tls-options-configuration.

The (gnu services configuration) module provides us with the API that we need to define these configurations. The most important helpers for defining configurations are:

  • define-configuration: For configurations that need to serialize fields into a different format, generally configuration files.

  • define-configuration/no-serialization: For configurations that do not need to emit any files. Generally all the fields are consumed by the Shepherd services that rely on the configuration but no translation from Scheme to a different format is needed.

In our case, we need to translate the different configuration fields to YAML, so we will need to use define-configuration.

The following sections are written so you can follow along, you are encouraged to drop into a REPL (short for read-eval-print loop) and import the required module:

,use (gnu services configuration)

goimapnotify-tls-options-configuration

To make it easy for ourselves, we will start form the in-out, after all, you wouldn't want to build a house from the rooftop, would you?

(define-configuration goimapnotify-tls-options-configuration
  (reject-unauthorized?
   (boolean #f)
   "Whether to reject unauthorized TLS certificates.")

  (starttls?
   (boolean #f)
   "Whether to use STARTTLS.")

  (prefix goimapnotify-))

evaluating the above snippet will throw an unbound variable exception, bear with me.

Refer to the manual for an in-depth explanation of the syntax of define-configuration.

The first argument is the name of the configuration object, goimapnotify-tls-options-configuration in this case. After it, we define the different typed fields. We are defining boolean fields that are, by default, set to false. The biggest source of confusion when defining configurations comes because define-configuration is a macro that introduces identifiers that do not appear in the source code—it's an unhygienic macro. That means that the macro will expand to code which defines symbols that are not visible when reading the source file.

One can inspect the expansion of the macro by using the ,expand REPL command. It will be quite verbose, so don't try to read all of it, instead search through it; you will find some revealing things, such as:

scheme@(gnu home services mail)> ,expand (define-configuration goimapnotify-tls-options-configuration
  (reject-unauthorized?
   (boolean #f)
   "Whether to reject unauthorized TLS certificates.")
  (starttls?
   (boolean #f)
   "Whether to use STARTTLS.")
  (prefix goimapnotify-))
$20 = (begin ...
        (define goimapnotify-tls-options-configuration? ...)
        (define goimapnotify-tls-options-configuration-reject-unauthorized? ...)
        (define goimapnotify-tls-options-configuration-starttls? ...)
        ...
        (define <goimapnotify-tls-options-configuration> ...)
        ...
        (define goimapnotify-tls-options-configuration ...)
        (define goimapnotify-tls-options-configuration-fields
          ((@@ (gnu services configuration) list)
           (let* ((name (let ((x 'reject-unauthorized?)) x)) ...
                  (serializer (let ((x goimapnotify-serialize-boolean)) x)) ...)
             ...)
           (let* ((name (let ((x 'starttls?)) x)) ...
                  (serializer (let ((x goimapnotify-serialize-boolean)) x)) ...)
             ...))))

In the previous snippet, ... represents omitted code. As you can see from the macro expansion, define-configuration introduces quite a few identifiers. You may recognize the shared prefix in those identifiers, that's right, it's the prefix specified by that last field that we didn't explain from the configuration definition, (prefix goimapnotify-).

By inspecting the macro expansion, it's easy to understand what happens under the hood. When a prefix is specified, we instruct the macro to append that prefix to all the generated identifiers. This is useful to avoid naming collisions; for example, when defining the configuration in a module with other configuration records that serialize to different formats. After all, it's not the same to serialize to YAML than to INI, or any other format a tool may require.

Remember that unbound variable problem I mentioned before? If you paid close attention to the macro expansion, you may have noticed those references to goimapnotify-serialize-boolean, those are our unbound variables. The macro expects us to define these serialization procedures, let's do that.

Defining the missing serializers

Since the configuration machinery knows nothing about the output format, we must define how this translation happens. In our case, we are translating to YAML. By looking at the example configuration provided by the Goimapnotify developers, we can see that the field names are written in camel case, and their respective values are separated from the names through the : character.

Helpers

Let's start by making a function that takes a symbol and transforms it into a string that's a camelized version of that symbol. We will use object->camel-case-string from the (gnu home services utils) module:

,use (gnu home services utils)
(define (camelize-field-name field-name)
  (let ((str (object->camel-case-string field-name)))
    (if (string-suffix? "?" str)
        (string-drop-right str 1)
        str)))
;; Usage: (camelize-field-name 'reject-unauthorized?) => "rejectUnauthorized"

In YAML, there is no convention of suffixing booleans with a ?, so our camelizer drops it when found.

camelize-field-name gives us a field name, but we want to serialize the value too. Let's define a field serialization procedure to help us:

(define (goimapnotify-serialize-field field-name val)
  "The mapping is used to serialize certain FIELD-NAMES specially."
  (let* ((field-name-mapping '((host-command . hostCmd)
                               (user-name . username)
                               (user-name-command . usernameCmd)
                               (password-command . passwordCmd)))
         (field-name* (or (assq-ref field-name-mapping field-name)
                          field-name)))
    (format #f "~a: ~s~%"
              (camelize-field-name field-name*)
              val)))
;; Usage: (goimapnotify-serialize-field 'reject-unauthorized? 'true) => "rejectUnauthorized: true\n"

Notice how we introduced field-name-mapping to tailor the field name passed to camelize-field-name to emit the specific naming that Goimapnotify expects. In Guix, we have a specific naming convention, so we want fields like user-name to map to username (instead of userName) and fields like password-command to map to passwordCmd (instead of passwordCommand).

That's enough to serialize most values to YAML, but there's an extra Guix-specific feature we should make use of to make the service more convenient to users: G-Expressions.

Are you familiar with them? If not, I encourage you to read this wonderful trilogy of blog posts: Dissecting Guix. It took me some time to wrap my head around these concepts, but once you do, I think you will like them too. In any case, that serializer is using those expressions because we want users of our configuration to be able to intermingle packages and other file-like objects in their fields. Since this is a blog post about writing configurations, I won't dive deep into G-Expressions, but let's try to get you a sense for them.

You will need to import (guix gexp) for the following snippet to work.

(define (goimapnotify-serialize-field field-name val)
  "The mapping is used to serialize certain FIELD-NAMES specially."
  (let* ((field-name-mapping '((host-command . hostCmd)
                               (user-name . username)
                               (user-name-command . usernameCmd)
                               (password-command . passwordCmd)))
         (field-name* (or (assq-ref field-name-mapping field-name)
                          field-name)))
    #~(format #f "~a: ~s~%"
              #$(camelize-field-name field-name*)
              #$val)))

That's not so bad, is it? What was that? 6 more characters? Surely you wouldn't be scared of that, but just in case, let me give you some extra reassurance on what's happening here. This G-Expression thing, also known as gexp, is just the way you let Guix know that this code is for later. "When is later?", you may wonder. Simplifying it, that "later" is when Guix knows where file-like objects will be located in your disk; that long path on the store you may have seen before.

For example, a package is a file-like object, so when you build the package Guix will tell you where it was stored:

$ guix build cowsay
/gnu/store/gz170ppi2hxssp4h3yw5jh4gdw6x9ws0-cowsay-3.8.4

Preceding an expression with #~ (or gexp), one effectively tells Guix "don't evaluate this code until you know what this expression should expand to". And that other syntax, #$ (or ungexp), is telling Guix to replace the file-like object for its lowered representation, usually a path in the store. For a package like cowsay, that would be that /gnu/store/gz170ppi2hxssp4h3yw5jh4gdw6x9ws0-cowsay-3.8.4 paths we saw before.

With that said, if you are like me, you won't be comfortable if you cannot debug this expressions, so let me give you some supper powers.

In your REPL, import the (guix) module. This will augment the REPL with some additional commands that will simplify our life:

scheme@(guile-user)> ,use (guix)
scheme@(guile-user)> ,help guix
Guix Commands [abbrev]:

 ,run-in-store EXP                 - Run EXP through the store monad.
 ,verbosity LEVEL                  - Change build verbosity to LEVEL.
 ,lower OBJECT                     - Lower OBJECT into a derivation or store file and return it.
 ,build OBJECT [BUILD-MODE]        - Lower OBJECT and build it, returning its output file name(s).
 ,build-options OPTIONS            - Set build options to OPTIONS.  Print previous value (to allow easy restore).
 ,build-graft GRAFT?               - Set whether grafts should be performed.
 ,enter-store-monad                - Enter a REPL for values in the store monad.
 ,phases                           - Return the build phases of the package defined by FORM.
 ,configure-flags                  - Return the configure flags of the package defined by FORM.
 ,make-flags                       - Return the make flags of the package defined by FORM.

Let's see what that gexp is up to:

scheme@(gnu home services mail)> (goimapnotify-serialize-field 'reject-unauthorized? ''true)
$6 = #<gexp (format #f "~a: ~s\n" #<gexp-input "rejectUnauthorized":out> #<gexp-input (quote true):out>) gnu/home/services/mail.scm:252:2 7f4c7d9deb40>

There is a little helper to approximate a gexp to it's output:

scheme@(gnu home services mail)> (gexp->approximate-sexp $6)
$7 = (format #f "~a: ~s\n" "rejectUnauthorized" (quote true))
scheme@(gnu home services mail)> (primitive-eval $7)
$8 = "rejectUnauthorized: true\n"

That gets us an idea of what will be emitted to disk, but for more complex procedures, this is not going to cut it, specially if there are multiple gexps combined. So let's build that gexp:

;; 'gexp->file' comes from the '(guix gexp)' module. You can ask the REPL more
;; information about a symbol through ',a SYMBOL'.
scheme@(gnu home services mail)> ,a gexp->file
(guix gexp): gexp->file #<procedure gexp->file (name exp #:key guile set-load-path? module-path splice? system target)>

scheme@(gnu home services mail)> (gexp->file "test.scm" $6)
$8 = #<procedure 7f3f7d456ea0 at guix/gexp.scm:2098:2 (state)>

In order for Guix to build a value, it needs to be something Guix can lower—an object that can be "compiled" down to a file in the store (these are referred to as file-like objects, because they can be inserted in any piece of code that expects a file name). A gexp cannot be built by itself, because there is nowhere to output it to. The above snippet creates a file-like object that will be emitted to a file named test.scm in the store. That file will contain our expression. As you see, we've got a procedure. The REPL printer tells us that this procedure needs some state; this is just a way of indicating you that this is a monadic procedure that can only be run in the context of a store connection. Read The Store Monad for more information.

I will build it manually just once for demonstration purposes, don't blink:

scheme@(gnu home services mail)> ,use (guix store)
scheme@(gnu home services mail)> (run-with-store (open-connection)
                                   $8)
$9 = #<derivation /gnu/store/gq71r3sgilfvsicfnifln8n0wfksam13-test.scm.drv => /gnu/store/c3hxw7pgnxchw1zldnc9nrb8iwwrqf78-test.scm 7f3f7e2de000>

Where we are saying "run the procedure we got in the context of a store connection", we opened that connection through open-connection. Let's not do that again... Fortunately we have those useful REPL commands I just told you about, so we can archive the same result by doing this:

scheme@(gnu home services mail)> ,run-in-store $8
$10 = #<derivation /gnu/store/gq71r3sgilfvsicfnifln8n0wfksam13-test.scm.drv => /gnu/store/c3hxw7pgnxchw1zldnc9nrb8iwwrqf78-test.scm 7fcd92f7f0f0>

That's better, isn't it?

Now, this is a derivation. That's something Guix can build:

scheme@(gnu home services mail)> ,build $10
building /gnu/store/gq71r3sgilfvsicfnifln8n0wfksam13-test.scm.drv...
$11 = "/gnu/store/c3hxw7pgnxchw1zldnc9nrb8iwwrqf78-test.scm"

What's that? You don't want this magic command? Okay, the same thing can be done with:

scheme@(gnu home services mail)> (run-with-store (open-connection)
                                   (built-derivations (list $10)))
$12 = #t
scheme@(gnu home services mail)> (derivation->output-path $10)
$13 = "/gnu/store/c3hxw7pgnxchw1zldnc9nrb8iwwrqf78-test.scm"

At last, a file! What's in there?

$ cat /gnu/store/c3hxw7pgnxchw1zldnc9nrb8iwwrqf78-test.scm
(format #f "~a: ~a\n" "rejectUnauthorized" "true")

Okay that seems about right. We can even run it:

scheme@(gnu home services mail)> (call-with-input-file $13
                                   (lambda (port)
                                     (primitive-eval (read port))))
$14 = "rejectUnauthorized: true\n"

You've seen the secret sauce, let's continue with what brought us here.

(define (goimapnotify-serialize-boolean field-name val)
  (goimapnotify-serialize-field field-name (if val ''true ''false)))

With this our configuration won't complain about the missing serializer. Let's continue.

One last thing, notice how, after moving to the gexp version of goimapnotify-serialize-field, we started double quoting the symbols true and false, this is because the #$ syntax will replace the value in place, and the value of 'true is true, without the quote. If we didn't double quote, the staged code after expansion would look like this:

(format #f "~a: ~s\n" "rejectUnauthorized" true)

When what we really want is to expand to this:

(format #f "~a: ~s\n" "rejectUnauthorized" 'true)

The reason for this is that we want users to be able to write staged code on the different fields. If one of the values of a field where to be something like this:

(goimapnotify-serialize-field 'favorite-game #~(string-append #$cowsay "/bin/cowsay"))

The expansion would be this:

(format #f "~a: ~s\n" "favoriteGame" (string-append "/gnu/store/gz170ppi2hxssp4h3yw5jh4gdw6x9ws0-cowsay-3.8.4" "/bin/cowsay"))

The string-append procedure is evaluated right before the format call, not when the gexp is getting lowered.

Serializing

The (gnu services configuration) module we imported earlier provides us with serialize-configuration, it takes two arguments, a configuration object and the fields that compose that configuration.

The define-configuration macro defined the goimapnotify-tls-options-configuration constructor for us, we can create a configuration with the default values like this:

scheme@(gnu home services mail)> (goimapnotify-tls-options-configuration)
$10 = #<<goimapnotify-tls-options-configuration> reject-unauthorized?: #f starttls?: #f %location: #f>

Then, we can serialize it like this:

scheme@(gnu home services mail)> (serialize-configuration $10 goimapnotify-tls-options-configuration-fields)
$11 = #<gexp  gnu/services/configuration.scm:165:2 7ff1fd2cbf60>

If you recall form the macro expansion we saw earlier, goimapnotify-tls-options-configuration-fields, was one of those identifiers that got generated.

You should already know how to build that gexp we've got:

scheme@(gnu home services mail)> (gexp->file "test.yaml" $11)
$12 = #<procedure 7efd8b10c2d0 at guix/gexp.scm:2098:2 (state)>
scheme@(gnu home services mail)> ,use (guix)
scheme@(gnu home services mail)> ,run-in-store $12
$13 = #<derivation /gnu/store/z59sf3nhh80668z8njljfxcymgi071pr-test.yaml.drv => /gnu/store/xam1210yl1vdxix0bgpfalf6drpv6xbx-test.yaml 7efd8a80fd70>
scheme@(gnu home services mail)> ,build $13
$14 = "/gnu/store/xam1210yl1vdxix0bgpfalf6drpv6xbx-test.yaml"
scheme@(gnu home services mail)> (call-with-input-file $14
                                   (lambda (port)
                                     (display (primitive-eval (read port)))))
rejectUnauthorized: false
starttls: false

That's some nice YAML syntax... We better speed up the pace or I will retire before finishing up this blog post.

goimapnotify-box-configuration

Next in line is goimapnotify-box-configuration, you know the drill. We start by declaring the configuration with define-configuration, specifying each field name, type and docstring:

(define-configuration goimapnotify-box-configuration
  (mailbox
   string
   "The mailbox to monitor.")

  (on-new-mail
   maybe-string-or-gexp
   "The command to execute when new mail arrives.")

  (on-new-mail-post
   maybe-string-or-gexp
   "The command to execute after the new-mail command.")

  (on-changed-mail
   maybe-string-or-gexp
   "The command to execute when mail is changed.")

  (on-changed-mail-post
   maybe-string-or-gexp
   "The command to execute after the changed-mail command.")

  (on-deleted-mail
   maybe-string-or-gexp
   "The command to execute when mail is deleted.")

  (on-deleted-mail-post
   maybe-string-or-gexp
   "The command to execute after the deleted-mail command.")

  (prefix goimapnotify-))

You may have noticed that we have two new types. strings, easy enough, and maybe-string-or-gexp, not so easy; right? Worry not, here comes the explanation.

Let's start by defining the string type:

(define-maybe string (prefix msmtp-configuration-))

You may be very confused right now. What has msmtp-configuration to do with our goimapnotify-configuration example? Well, I want this blog post to get you ready for the real world, and in the wild, you will make configurations in modules that contain other configurations. The example we are looking up today is a narration of my adventures making the Goimapnotify service from the (gnu services mail) module, in that module, there is already a configuration for msmtp. With that said, if we tried to do this:

(define-maybe string (prefix goimapnotify-))

We would be surprised with a warning that looks something like this:

$ make
...
[ 94%] GUILEC   gnu/home/services/mail.go
gnu/home/services/mail.scm:67:0: warning: shadows previous definition of `maybe-string?' at gnu/home/services/mail.scm:63:0
gnu/home/services/mail.scm:278:0: warning: shadows previous definition of `goimapnotify-serialize-maybe-string' at gnu/home/services/mail.scm:67:0

That's unexpected, isn't it? Nothing we've seen so far points to a maybe-string? procedure, let's look at what is going under the hood of that define-maybe macro:

scheme@(gnu home services mail)> ,expand (define-maybe string (prefix msmtp-configuration-))
$15 = (begin
        (define (maybe-string? val)
          (or ((@@ (gnu services configuration) not)
               ((@@ (gnu services configuration) maybe-value-set?) val))
              (string? val)))
        (define (msmtp-configuration-serialize-maybe-string field-name val)
          (if (string? val)
              (msmtp-configuration-serialize-string field-name val)
              "")))

Do you see it? The macro is expanding to some code that introduces two new procedures into the module, one following the prefix, that would be msmtp-configuration-serialize-maybe-string, and one that just declares the predicate for the maybe type.

Given that, the warning is now apparent. If we call again that macro with a string as the first argument, we will get the same predicate after the expansion, leading to the redefinition warning.

So, for this particular case, instead of using the macro, we will define manually the two procedures required for our configuration:

;; The module already had this helper defined.
(define (string-or-gexp? obj)
  (or (string? obj)
      (gexp? obj)))

;; ... omitted lines ...

(define (goimapnotify-serialize-string field-name val)
  (goimapnotify-serialize-field field-name val))

(define (goimapnotify-serialize-maybe-string field-name val)
  (if (maybe-value-set? val)
      (goimapnotify-serialize-string field-name val)
      ""))

(define goimapnotify-serialize-string-or-gexp
  goimapnotify-serialize-string)

(define (goimapnotify-serialize-maybe-string-or-gexp field-name val)
  (if (and (maybe-value-set? val)
           (string-or-gexp? val))
      (goimapnotify-serialize-string-or-gexp field-name val)
      ""))

The above snippet defines all the serializers we need for the new types. Notice that they are just some simple wrappers around the goimapnotify-serialize-field helper. That procedure is doing all the heavy lifting here, and fortunately for us, it already handles gexps. Therefore, the fields that have a type that accepts a gexp are straightforward to declare.

That would be it for the goimapnotify-box-configuration declaration. As showed in the previous section, the REPL is your friend. I had never written such a complex configuration before, but thanks to this "elegant weapon for a more civilized age", I could find my way by poking things around.

Let's continue!

goimapnotify-configuration

You know the drill, we start by defining the fields we need. Again, I'm doing this just by looking at the README example provided by the developers of Goimapnotify:

(define-configuration goimapnotify-configuration
  (host
   string
   "The IMAP server hostname.")

  (host-command
   maybe-string-or-gexp
   "The command to retrieve the IMAP server hostname.")

  (port
   (integer 993)
   "The port that the IMAP server listens on.")

  (tls?
   (boolean #f)
   "Enable or disable TLS.")

  (tls-options
   maybe-goimapnotify-tls-options-configuration
   "TLS options for the IMAP connection."
   (serializer serialize-maybe-goimapnotify-tls-options-configuration))

  (idle-logout-timeout
   maybe-integer
   "The idle logout timeout in minutes.")

  (user-name
   maybe-string
   "The user-name for authentication.")

  (user-name-command
   maybe-string-or-gexp
   "The command to retrieve the user-name.")

  (alias
   maybe-string
   "An alias for the account.")

  (password
   maybe-string
   "The password for authentication.")

  (password-command
   maybe-string-or-gexp
   "The command to retrieve the password.")

  (xo-auth2?
   (boolean #f)
   "Enable or disable XOAUTH2 authentication.")

  (wait
   maybe-integer
   "The delay in seconds before the mail syncing is triggered.")

  (boxes
   list-of-goimapnotify-boxes-configurations
   "The mailboxes to monitor."
   (serializer serialize-list-of-goimapnotify-boxes-configurations))

  (prefix goimapnotify-))

You are already familiar with most of those types, but there are some new things here:

  • integer and maybe-integer
  • maybe-goimapnotify-tls-options-configuration
  • list-of-goimapnotify-boxes-configurations

Let's start with the obvious ones first.

integer

The module already had a maybe definition for the integer. If you recall from the macro expansion earlier, that means that there is already a symbol for the predicate maybe-integer?:

(define-maybe integer (prefix msmtp-configuration-))

We still need a serializer that follows our prefix:

(define (goimapnotify-serialize-integer field-name val)
  (goimapnotify-serialize-field field-name val))

(define (goimapnotify-serialize-maybe-integer field-name val)
  (if (maybe-value-set? val)
      (goimapnotify-serialize-integer field-name val)
      ""))

Simple enough. Moving on!

maybe-goimapnotify-tls-options-configuration

(define (serialize-goimapnotify-tls-options-configuration field-name val)
  (let ((serialization (serialize-configuration val goimapnotify-tls-options-configuration-fields)))
    #~(begin
        (use-modules (ice-9 format) (ice-9 string-fun))
        (format #f "~a:
  ~a~%"
                '#$(camelize-field-name field-name)
                (string-replace-substring #$serialization "\n" "\n  ")))))

(define-maybe goimapnotify-tls-options-configuration)

You are already familiar with the define-maybe macro. The serializer is also quite simple. Since we already defined a record that contains within all the required information to serialize it, we just need to make a simple wrapper around serialize-configuration.

Before moving forward, notice how the serializers from this section doesn't contain that goimapnotify- prefix. This is an arbitrary decision, but since there is already goimapnotify in the symbol name, I think it's a bit redundant to add it. For the configuration definition to know which serializer to use, we have to be specific in the serializer argument of the fields, that's why in the configuration we had this declaration specifying a (serializer ...) for the field:

(define-configuration goimapnotify-configuration
  ;; ... omitted lines ...
  (tls-options
   maybe-goimapnotify-tls-options-configuration
   "TLS options for the IMAP connection."
   (serializer serialize-maybe-goimapnotify-tls-options-configuration))
  ;; ... omitted lines ...
  (prefix goimapnotify-))

list-of-goimapnotify-boxes-configurations

First we need a prefix to know if we have a list of goimapnotify-box-configuration objects, that one is simple:

(define (list-of-goimapnotify-boxes-configurations? lst)
  (and (not (null? lst))
       (every goimapnotify-box-configuration? lst)))

Now we need a serializer that knows how to handle a list of these objects:

(define (serialize-list-of-goimapnotify-boxes-configurations field-name value)
  (let ((serializations (cons 'list
                              (map (cut serialize-configuration <>
                                        goimapnotify-box-configuration-fields)
                                   value))))
    #~(begin
        (use-modules (ice-9 format) (ice-9 string-fun))
        (format #f "~a:
~{  - ~a~%~}"
                '#$(camelize-field-name field-name)
                (map (lambda (s)
                       (string-replace-substring s "\n" "\n    "))
                     #$serializations)))))

Looks a bit daunting, but it's just staged code, remember our earlier explanation of gexps. This code is only constructing a string with the shape we need.

As a reminder, we are serializing to YAML, that means that lists are prefixed by a - character. Following the Goimapnotify README, the indentation would be something like this:

boxes:
  - mailbox: INBOX
    onNewMail: 'mbsync examplenet:INBOX'
    onNewMailPost: SKIP
    onChangedMail: 'mbsync examplenet:INBOX'
  - mailbox: Junk
    onNewMail: 'mbsync examplenet:Junk'
    onNewMailPost: SKIP

This is what that format call is doing. Refer to Formatted-Output for more information.

We are only lacking a way to generate a complete configuration, remember that goimapnotify-configuration is an object for a single configuration. According to the README of Goimapnotify, the configuration file can take a list of configurations.

home-goimapnotify-configuration

This is the last configuration we will need, it will be used directly by the service:

(define-configuration home-goimapnotify-configuration
  (goimapnotify
   (file-like goimapnotify)
   "The @code{goimapnotify} package to use."
   empty-serializer)
  (configurations
   (list-of-goimapnotify-configurations)
   "A list of @code{goimapnotify-configuration} records which contain
information about all your accounts configurations."))

Simple enough, the first field is the Guix package that provides the goimapnotify program. This one is used by the service to start the process. Since it doesn't need to appear in any configuration file, we don't need to serialize it, hence the empty-serializer.

On last serializer:

(define (serialize-list-of-goimapnotify-configurations field-name value)
  (let ((serializations (cons 'list
                              (map (cut serialize-configuration <>
                                        goimapnotify-configuration-fields)
                                   value))))
    #~(begin
        (use-modules (ice-9 format) (ice-9 string-fun))
        (format #f "~a:
~{  - ~a~%~}"
                '#$(camelize-field-name field-name)
                (map (lambda (s)
                       (string-replace-substring s "\n" "\n    "))
                     #$serializations)))))

The rationale for this code is the same as the one explained for list-of-goimapnotify-boxes-configurations.

Defining the Shepherd service

The service definition; at last!

(define (home-goimapnotify-shepherd-service config)
  (let ((log-file #~(string-append %user-log-dir "/goimapnotify.log")))
    (list
     (shepherd-service
       (provision '(goimapnotify))
       (modules '((shepherd support)))   ;for '%user-log-dir'
       (documentation "Run a goimapnotify process")
       (start #~(make-forkexec-constructor
                 (list
                  #$(file-append
                     (home-goimapnotify-configuration-goimapnotify config)
                     "/bin/goimapnotify")
                  "-conf" #$(mixed-text-file "goimapnotify.yaml"
                                             (serialize-configuration config
                                                                      home-goimapnotify-configuration-fields)))
                 #:log-file #$log-file))
       (stop #~(make-kill-destructor))))))

Refer to Shepherd Services for more information on how to write Shepherd services. I will only highlight how to handle the configuration we just wrote.

We have already done all the hard work, the configuration declaration contains all the information needed to perform the serialization of the different fields to YAML, we just need to call serialize-configuration. For example:

scheme@(gnu home services mail)> (define test-config
  (home-goimapnotify-configuration
   (configurations
    (list
     (goimapnotify-configuration
      (host "test.example.com")
      (boxes
       (list
        (goimapnotify-box-configuration
         (mailbox "Test")))))))))

scheme@(gnu home services mail)> (serialize-configuration test-config home-goimapnotify-configuration-fields)
$6 = #<gexp  gnu/services/configuration.scm:165:2 7f3448e678a0>

That serialization gives a gexp ready to be wrapped in a file-like so it can be lowered to the store. That's what mixed-text-file is doing:

scheme@(gnu home services mail)> (mixed-text-file "goimapnotify.yaml"
                 (serialize-configuration test-config
                                          home-goimapnotify-configuration-fields))
$7 = #<<computed-file> name: "goimapnotify.yaml" gexp: #<gexp  guix/gexp.scm:2171:6 7f343915ef60> guile: #f options: (#:local-build? #t)>
scheme@(gnu home services mail)>

We've got ourselves a service, but there is one last thing before we go!

Defining the service type

As mentioned in the previous section, refer to Defining Services for what's going on here. The last thing we need do is declare the relation that this service has with respect to others:

(define home-goimapnotify-service-type
  (service-type
    (name 'home-goimapnotify)
    (extensions
     (list (service-extension home-shepherd-service-type
                              home-goimapnotify-shepherd-service)))
    (description "Configures the @code{goimapnotify} IMAP Mailbox notifier.")))

We did it!

Closing words

Still here? That was long... But here we are, we defined our complex configuration.

Congratulations on reading till the end, by this time you should already be an expert on defining Guix configurations.

We acknowledge that there are some improvements to do on the API for defining configurations. We would like to have a way to specify symbol mapping for field names in the declaration, so that serializers can be generalized better. It would also be nice if we could refactor some of our configuration definitions so the serializers that can be generalized are reused between declarations. After all, there are many configuration files in similar formats.

For all of these improvements, we are counting on you! I wrote this blog post to empower you to participate in the development. If this is something that resonates with you, come join the fun!

Link [Scripting News]

Today's song: So into you.

Link [Scripting News]

We got past the bump, and I'm getting stuff done. I write this stuff because I will probably forget most of the frustration. Frontier is a hard piece of software to get going, it's got a lot of integrations, which you want when you're using, but hard when you're getting it all to work the first time. And this port is very much like getting it work that way. But Claude does some things poorly and others spectacularly. It can play the role of a CPU and run the code and tell you where it will fail. Helpful when you don't have a working debugger to step through the code and see exactly how and why it failed where it did. Before debuggers it was either all or nothing.

Link [Scripting News]

Every day with Claude I end up debugging the things it broke overnight. Yesterday I actually got a useful script written and debugged, along the way reporting glitches but no deal-stoppers. This morning, after updating to the latest version, the script doesn't run. It's been like this for two weeks. No wonder every day before starting work I think of ditching this project and do whatever, but I don't think I could ever muster the determination to get Frontier working on modern systems. And I can't give up. But if you think this system can replace programmers, you are sorely mistaken. It can't even do the job of a programmer fielding good bug reports and verifying that they fixed the problem and didn't break anything else. Breakage happens, but this is something it does every freaking day. And then it admits that it didn't do the other half of what it knew it had to do. I say it every day, this is ridiculous and I am fed up, and that's just the beginning of the frustration. What keeps me coming back is when I can build up a head of steam in Frontier on my new Mac laptop.

Kernel prepatch 7.3-rc2 [LWN.net]

The 7.3-rc2 kernel prepatch is out for testing. Linus said:

This didn't *feel* like a particularly busy rc2, but it clearly was. rc2 is usually the quietest time when people take a breather after the merge window and it takes a while to start finding bugs. But not this time - this is a "full fat" rc release. [...]

Nothing looks particularly odd, even if the rc2 timing is a bit unusual. It might be just random, but we'll obviously all blame it on AI, because whether that's really the cause or not, it's an easy thing to blame ;)

Daniel Lange: Getting AVIF thumbnails in XFCE4 thunar (Debian Trixie) [Planet Debian]

The AVIF image format gets more and more popular in the web dev community, so I needed to teach XFCE4's thunar (file manager) and Ristretto (image viewer) to thumbnail these.

Luckily that is not too hard:

Debian Trixie separates its gdk-pixbuf libraries slightly differently than previous versions. That's why it is not "automatically there". Ensure you have the libavif-gdk-pixbuf plugin and the tumbler service (which XFCE uses to process thumbnails):

sudo apt --update install libavif-gdk-pixbuf tumbler

Thunar has likely tried (and failed) to load your AVIF files before you installed the package, it will have saved a blank or "broken image" placeholder in a thumbnail cache directory. It will not attempt to regenerate them unless you clear this cache:

# Clear the thumbnail cache
rm -rf ~/.cache/thumbnails/*

# Force-quit thunar and the tumblerd background service
thunar -q
pkill tumblerd

Tumbled will restart on its own when it is needed. When you open thunar again and navigate to your image directory ... your AVIF images will now generate thumbnails automatically like the other image format did already.

Avif thumbnails in thunar

Iustin Pop: AI agents aha moment [Planet Debian]

Looking at the reactions to the Debian AI vote, I think some people still think the clock can be turned back, as if that ever worked in history. Rather than cry about spilled milk, I prefer to find a path forward in the new world. There are many ways to use LLMs, some of them are straightforward, others not so much.

One of the “not so clear” areas for me is the focus on agentic workloads. For complex tasks, sure, you want something that can work in the background, but in general, why does every single tool go the agentic way? I much prefer the “chat/ask” approach, or even the “code” one, but if I’m at the keyboard, why would I send a task to an agent, and see it work, instead of directly implementing it?

And then, this past Friday, I finally understood one part of that. I was in the airport, sitting at the gate and waiting to board a flight, and because I arrived much earlier at the airport (fearing crowds due to Labour Day weekend), I got one hour of work before boarding started. As the time for boarding approached, I did one more commit after making sure tests pass, pushed, closed laptop, and went to walk a bit before getting on the plane.

As I was getting up, I get a phone notification from GitHub that the CI run failed. I was quite surprised, as the local tests passed, so I open the notification, and realize that tests via make test vs CI (which additionally uses --pedantic) had slightly different settings, and of course I missed a build warning (which in CI is an error).

I thought I’d fix that on the plane, but then I saw a “Copilot agent” button in the mobile app. I was curious what it did, I click it, and I see Copilot starting a draft pull request, and saying:

Thanks for asking me to work on this. I will get started on it and keep this PR’s description up to date as I form a plan and make progress.

Fix the failing GitHub Actions job. Analyze the Actions logs, identify the root cause of the failure, and implement a fix.

Then it goes, finds the failure, writes the fix, and tries to run the tests. Well, it can’t do it (it runs in a restricted container, so no network, so stack install couldn’t actually work). The agent sees that, acknowledges it has no way to validate the fix, but the error message was clear enough that it was confident the fix is mostly correct, so it sends the pull request.

I allow full CI to run on the pull request, and go buy a bottle of water. After that, I check and see that the CI failed again, as not one but two test files were broken, and I didn’t have --keep-going, so the build stopped at the first failure. I write a comment in the pull request, no reaction, I realize I need to tag Copilot explicitly, I do that, and it starts another investigation.

I’m waiting now in the boarding queue, with phone in hand, while Copilot is fixing my bug. While I scan my boarding pass and walk towards the plane, the pull request is updated, I trigger another CI, it passes, and I merge it.

And then, it hit me. Agents allow me to make progress while being “not at keyboard”, whether that’s physically “not at keyboard”, or while working on something else. Fixing a simple test failure is not something that needs human attention per se, whereas improving the test layout might be.

In that airport, using otherwise-unusable downtime, and without explicitly intending to, I made progress in understanding a different way to use AI. Now I have three ways to work with LLMs: ask (tutor mode), code (implement my request), and agent (fix simple or complex problems, autonomously). I still don’t know about “plan” mode and really complex tasks, like asking it to implement features from scratch. That will probably be the next area to tackle.

And today (Sunday), while waiting for a running race to start, I opened GitHub, and asked Copilot to increase test coverage for a simple module. It did, and yes it still can’t run tests (I learned in the meantime that you can configure the environment in which the agent runs, nice), but after two back-and-forth messages, I have a pull request ready to review. All in the 20 minutes before a race, where I could either browse social media or actually do some meaningful work.

Checking now my GitHub billing, it looks like all of this Copilot use only cost $1.92. Yes, that is under two dollars! And while it did use compute resources, the person across the aisle who watched TikTok or Instagram for half an hour while waiting for takeoff also consumed a lot of compute, and so do the gazillion cat videos uploaded to YouTube every day.

To me, this is another tool in the toolbox, that might one day replace me (as it did to the 19th-century textile workers), or make me five times more productive — we’ll see where we end up. In the meantime, I can move faster, and make better use of my limited free time.

Enjoy the ride!

Dirk Eddelbuettel: RcppFarmHash 0.0.4 on CRAN: Maintenance [Planet Debian]

Another minor maintenance release of the RcppFarmHash package is now on CRAN as version 0.0.4.

RcppFarmHash wraps the Google FarmHash family of hash functions (written by Geoff Pike and contributors) that are used for example by Google BigQuery for the FARM_FINGERPRINT digest.

This releases updates several of package internal files for continuous intergration and package data.

The brief NEWS entry follows:

Changes in version 0.0.4 (2026-09-06)

  • Minor updates to continuous integration, README.md and DESCRIPTION

Courtesy of my CRANberries, there is also a diffstat report for this release. For questions, suggestions, or issues please use the issue tracker at the GitHub repo.

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can now sponsor me at GitHub.

Pluralistic: How corporate America built a better Roach Motel (07 Sep 2026) [Pluralistic: Daily links from Cory Doctorow]

->->->->->->->->->->->->->->->->->->->->->->->->->->->->-> Top Sources: None -->

Today's links



A fake Harvard Business Review cover, dated September-October 2026. It features a large Roach Motel under the headline 'THE NEW CUSTOMER RETENTION' with the subhead 'Why acquire customers when you can take hostages?'

How corporate America built a better Roach Motel (permalink)

"If economists wished to study the horse, they wouldn’t go and look at horses. They'd sit in their studies and say to themselves, 'What would I do if I were a horse?'" -Ely Devons

Half a century ago, a group of lavishly financed economists from the University of Chicago (the "neoliberals") convinced governments all over the world to completely upend the way they treated monopolies. Up until then, the purpose of competition enforcement was to reduce corporate power, with the understanding that once a corporation became more powerful than the government, it would be impossible to force it to follow any rules:

https://pluralistic.net/2022/02/20/we-should-not-endure-a-king/

But for the "Chicago Boys," monopolies were evidence of efficiency. When you encounter a company in the wild that has acquired a commanding market share, your first assumption should be that it has taken over its sector by being better than anyone else – you should not assume that the company cheated its way to glory. After all, if a company with a large market share was cheating – say, if it was increasing its profit margins by reducing quality or jacking up prices – then smaller companies would rush into the market to poach its dissatisfied customers.

Thus, all competition enforcement was reduced to an empty syllogism: monopolies are the result of excellence and any less-than-excellent monopolist will have its advantage "competed away." Therefore, any monopolist you encounter in the wild is definitionally not a bad monopolist, otherwise it would already have disappeared.

To quote another economist joke:

Two economists are walking down the street when one notices a $20 bill on the sidewalk. "It's not a real $20 bill," the other declares. "If it were a real $20 bill, someone would have picked it up off the sidewalk already."

Half a century later, our entire economy is dominated by monopolies, duopolies and cartels, who boast of gigantic margins, whose products are palpably worsening at an accelerating clip, and yet there is no sign of the "new market entrants" who should be flooding into the market to "compete away" those amazing margins. It turns out that asking "What would I do if I were a horse?" does not yield a series of accurate predictions about horses.

Things have changed. Today, the University of Chicago's Stigler Center harbors a cluster of influential economists who largely or entirely repudiate the orthodoxy of the Chicago Boys. The Center hosts an annual, rather radical conference on antitrust; runs an excellent heterodox podcast (Capitalisn't); its house organ, Promarket.org, regularly hosts work that torches the received wisdom of High Chicago Neoclassicism; and the school's researchers publish papers that dare to actually "go and look at horses."

A recent horse-looking excursion has yielded some distressing, alarming, and thoroughly documented equine facts. In a new Stigler paper, "Rising Customer Durability, Falling Business Dynamism," UC's Li Azinovic-Yang, Ava E Speros and Christopher R Stewart and Stanford's John D Kepler report on some clever research into how a monopolist could raise prices, lower quality, anger its customers, and still dominate its market:

https://www.chicagobooth.edu/-/media/research/stigler/pdfs/workingpapers/387_customer.pdf

The researchers' hypothesis was that dominant businesses don't maintain their lead by making their customers happy, but by making it harder for those customers to leave. There's good reasons to suspect this. Between 2002 and 2024, the average "customer relationship" (how long a customer continues to purchase from a merchant) has risen from 7.5 years to 11.5 years, a 50% surge in "customer loyalty," far outstripping any measure of customer satisfaction over the same period. This is true across all the largest sectors of the economy: "manufacturing, information, professional services, financial services, and wholesale trade."

How to explain the falling divorce rate between customers and businesses? That's where the researchers got very clever. They realized that when a company seeks permission to acquire another business, it must publish truthful and comprehensive information about how the merger is expected to increase the profits of the new combination. These disclosures are validated by external auditors, boards of directors, and/or audit committees. There are legal repercussions for falsifying them or making material omissions to them, and they are matters of public record.

Crucially, these disclosures must include the business's plans to retain its customers, and its plans to increase the profits from those customers. That's where the researchers struck gold. They amassed a novel data-set of 9,500 acquisitions that disclosed over $1t worth of "customer relationship-related intangibles," more than 20% of all the assets that changed hands.

They supplemented this data by mining earnings calls (also subject to strict penalties for omissions and falsehoods), finding CEOs boasting about "practices that may impede switching or increase customers’ dependence on the firm." Executives bragged about their "contractual restrictions, bundling and ecosystem lock-in, and switching costs."

You can get a sense of these in a short accompanying article by the study's co-author Christopher Stewart:

https://www.promarket.org/2026/09/04/american-companies-expect-their-customers-to-stay-much-longer-that-may-be-making-the-economy-less-competitive/

The article recounts how Sirius XM's execs celebrated the news that an appeals court had struck down the FTC's "Click to Cancel" rule, which required companies to make it as easy to resign from a subscription service as it was to sign up for it. Click to Cancel is a response to increasingly sleazy, increasingly pervasive tactics that make it all but impossible to stop being someone's customer. Trump's FTC walked away from defending the rule, which let the court kill it:

https://pluralistic.net/2025/05/12/greased-slide/#greased-pole

After Click to Cancel died, Sirius XM's C-suite got on a call with their shareholders to project "better outcome(s) as a result of not having that in place." Sirius believed that a rule that made it easy for customers to resign from their monthly subscriptions would hurt its business. Put another way: Sirius believes that its profits come in part from the fact that dissatisfied customers can't figure out how to cancel their service.

Then there's the online insurance company eHealth, whose execs crowed about a new "innovation" that forced senior patients to painstakingly enter a long list of their medications and doctors, but did not give them any way to export that data. The lengthy investment of time in getting set up on eHealth would stop customers from leaving, because they wouldn't want "to repeat all of that information over the phone."

This is also a feature of business-to-business relationships. In 2019, US Silica's execs described how they had launched a program to become embedded in their customers' supply chains, because that "really locks in the business," making it "much more difficult for customers to switch and go to someone else."

That's the first half of the story: an empirical account of how the business world switched from "acquiring customers" to taking hostages.

But the second half of the paper is even more interesting: an empirical investigation into the effects of this customer lock-in. For starters, increased customer retention is "associated with higher gross profit margins": that is, the companies whose customers can't leave squeeze those customers for more profit. What's more, once a company has its customers locked in, it starts to capture a larger share of all the profits in its entire sector: these hostage-takers become so profitable that their profits dwarf the profits of their competitors.

The paper also solves the mystery of the missing market entrants that the neoclassical horse-ponderers insisted would be conjured up to compete away an abusive monopolist's margins. The more locked in the customers of a monopolist are, the fewer companies try to enter that market. This makes sense: who would invest in a new business in a market where none of its potential customers can switch to its new business?

This is the opposite of what the horse-ponderers have insisted upon for 50 years. The more lock-in a company attains, the more profitable it becomes, and the less it has to worry about new competitors coming after those incredible margins. This is obvious to everyone, except the monopolist-funded "social scientists" and the governments they captured.

This is bad news, and not just for those locked-in customers. New businesses are the source of new jobs, and, yup, it turns out that sectors dominated by firms with high lock-in create fewer jobs. Of course, as workers chase fewer jobs, bosses are able to suppress their wages by forcing workers to bid against one another. Once again, the study finds that the sectors with the most lock-in also see declining wages in addition to declining jobs.

These are not the horse-ponderers' "efficient" monopolists. Once a company has its customers locked in, it innovates less – as measured by the number of patents a company is awarded, and by how often those patents are cited in other patents (this second measure helps distinguish companies that file mountains of bullshit patents from companies that actually invent useful things). Naturally, R&D spending also declines in companies with more lock-in.

All of this is entirely compatible with the theory of enshittification. Once a company knows its customers can't leave, it can switch from treating them well to abusing them in order to extract money from them. The same goes for companies whose workers can't leave – because they're bound by noncompete clauses, or because their employer has bought out all their rivals:

https://www.eff.org/deeplinks/2023/04/platforms-decay-lets-put-users-first

It's like the old Lily Tomlin sketches on SNL and Laugh-In, where she played Ernestine the telephone operator narrating satirical ads for AT&T. Those sketches would end with her obviously true catch-phrase: "We don't care. We don't have to. We're the phone company":

https://www.youtube.com/watch?v=CHgUN_95UAw

Decades later, Tomlin's phone company joke is a perfect distillation of modern management philosophy. As a famous NBER working paper showed, when a family business is handed over to a professional manager with an MBA, the company doesn't become more profitable overall; it just finds ways to pay its workers less:

https://www.nber.org/system/files/working_papers/w29874/w29874.pdf

That's why Tim Wu named this "the age of extraction." "Growth" no longer means "making something new that people want" – now it means "finding ways to take a larger share of the pie, even if that makes the pie smaller overall":

https://www.wired.com/story/tim-wu-age-of-extraction/

This is something we can all feel. We experience it in our daily lives, through "shrinkflation" and "junk fees" and a million other gross and petty scams. But it's rare that we actually catch executives explicitly admitting that their job is to find ways to take you hostage and squeeze you.

Historically, those revelations have come from extraordinary circumstances, like when Frontier (the worst ISP in America) went bankrupt and we learned that the company had 1.6 million customers who had no access to competing broadband connections. Frontier carried these hostages on their balance sheet as a special, highly valued asset, since they could be charged more for slower, less reliable service:

https://www.eff.org/deeplinks/2020/04/frontiers-bankruptcy-reveals-cynical-choice-deny-profitable-fiber-millions

In assembling this novel, high-quality data-set, the researchers on this paper have performed an important service, capturing a vast number of sworn confessions of highly paid enshittifiers, and then showing how their hostage-taking wrecked competition, prices, wages, jobs and innovation.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Three-hole punch debut, April 1940 https://web.archive.org/web/20061119140057/https://blog.modernmechanix.com/2006/09/06/three-hole-paper-punch-debut/

#20yrsago New Zealand redefines open source as “code you can’t modify” https://memex.craphound.com/2006/09/07/nobel-prize-sperm-bank-human-tragicomedy-about-eugenics/

#20yrsago MSFT quicker to patch DRM than security vulnerabilities https://www.schneier.com/blog/archives/2006/09/microsoft_and_f.html

#20yrsago Wikipedia’s dumbest arguments https://en.wikipedia.org/wiki/Wikipedia:Lamest_edit_wars

#10yrsago Why the Pirate Party could end up running Iceland https://web.archive.org/web/20211024071400/https://www.newstatesman.com/culture/2016/09/how-internet-pirates-became-political-force-iceland

#10yrsago Sampling bias: how a machine-learning beauty contest awarded nearly all prizes to whites https://web.archive.org/web/20160906154712/https://motherboard.vice.com/read/why-an-ai-judged-beauty-contest-picked-nearly-all-white-winners

#10yrago Warner Bros flags its own website as a piracy portal in copyright takedowns https://torrentfreak.com/warner-bros-flags-website-piracy-portal-160904/

#10yrsago The privacy wars have been a disaster and they’re about to get a LOT worse https://locusmag.com/feature/cory-doctorowthe-privacy-wars-are-about-to-get-a-whole-lot-worse/

#10yrsago Weapons of Math Destruction: invisible, ubiquitous algorithms are ruining millions of lives https://memex.craphound.com/2016/09/06/weapons-of-math-destruction-invisible-ubiquitous-algorithms-are-ruining-millions-of-lives/

#10yrsago Pro-democracy reformers win big in Hong Kong’s elections https://globalvoices.org/2016/09/06/hong-kong-voters-elect-pro-democracy-legislators-to-defend-the-citys-autonomy-from-china/

#1yrago Stock buybacks are stock swindles https://pluralistic.net/2025/09/06/computer-says-huh/#invisible-handcuffs


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "The Post-American Internet," a geopolitical sequel of sorts to Enshittification, Farrar, Straus and Giroux, 2027
  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, April 20, 2027

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2027

  • "The Memex Method," Farrar, Straus, Giroux, 2027



Colophon (permalink)

Today's top sources:

Currently writing:

  • “Once Is Enemy Action,” a science fiction novel about the origins of modern technofascism. Today's words: 574 (7730 total).

  • "The Post-American Internet," a sequel to "Enshittification," about the better world the rest of us get to have now that Trump has torched America. Fourth draft completed. Submitted to editor.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Bluesky (no ads, possible tracking and data-collection):

https://bsky.app/profile/doctorow.pluralistic.net

Medium (no ads, paywalled):

https://doctorow.medium.com/

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Easy Peasy Tunes Dave Mallinson [Judith Proctor's Journal]

Easy Peasy Tunes: 101 Tunes for Absolute Beginners and NovicesEasy Peasy Tunes: 101 Tunes for Absolute Beginners and Novices by Dave Mallinson

My rating: 5 of 5 stars


A book of folk tunes - mostly familiar - all in the useful keys of G and D (useful if you're playing the typical G/D melodeon, or D/G anglo in my case) And all in the easy part of the range of these instruments.

Occasionally, tunes have been simplified a bit to make them easier to play, but that's the purpose of the book!



View all my reviews

comment count unavailable comments

Honour among Enemies [Judith Proctor's Journal]

Honor Among Enemies (Honor Harrington, #6)Honor Among Enemies by David Weber

My rating: 3 of 5 stars


I started out really enjoying the Harrington series, and gradually going off them.

Two basic problems:
1. The plot arc is very similar in each novel
2. The politics. The People's Republic of Haven are clearly intended to be Communists or an extremely left wing government (One novel riffs heavily on the French Revolution). Most of their population live on the dole and vote for more money.
At the same time, it is made clear that people on the dole get very few chances in life - escaping from it is very difficult.
ie. People want to escape from it and have a better life, but are portrayed as shiftless because they are given no help to do so.

In spite of this, the Republic still finds enough money to provide a formidable navy.
So where is the tax money coming from?

There are 'good' and 'honourable' people in the Republic, but they still don't - for me at least - compensate enough for the politics.

There's some interesting ship combat scenes, but on the whole, I'd rather read Hornblower - his ships were real ones and don't require inventing new laws of physics to justify firing a broadside in space. (Hornblower was the inspiration for the Harrington series)




View all my reviews

comment count unavailable comments

INSANE CHARITY BIKE RIDE 2026! [Dork Tower]

2026 marks my 14th year of fundraising for the Fair Share CSA Coalition through Bike the Barns, to raise money to help local farmers get fresh-food to local low-income and elderly households.

Over those 14 years, Dork Tower readers (YOU!) and Munchkin players (YOU AGAIN!!) have raised more than a quarter of a million dollars for a wonderful local charity providing a critical important service in the fight against food insecurity.

This year will be VERY different, however:

In July, I fell, smashing my head on the pavement, blacking out, concussed.

Under Doctor’s orders (and my wife’s stern-but-loving supervision), I won’t be able to join the 2026 ride.

HOWEVER, if we hit the $10,000 goal, I am allowed to get on my bike for the first time since the concussion! I’ll cycle around the parking lot once or twice – POSSIBLY WITH A DUCK ON MY HEAD -but for me, this will represent as major an achievement as if I’d biked 60 miles in previous years.

Another big change: timeliness! In 2025, backer swag went out ridiculously late. This was entirely on me. But to make sure this does not happen again, all the swag will be drawn and completed and ready to go to press as soon at the ride is completed!

And the swag will be AWESOME: The original Munchkin card art at the $400 and above levels will be from the Munchkin 2nd Edition core set (as opposed to “corset.”) This will be your chance to own a classic Munchkin card that will define the game for decades to come!

I’ve added Sponsorship levels, so companies can pick up advertising at DorkTower.com that is 100% a charitable deduction, tax-wise. These may be added onto any level.

Finally, one of the stretch-goals is to provide Duck stickers and badges for all the other riders, so that EVERYONE in the event, should they wish, can ride with a Duck on their head (or possibly pinned to their saddle bag).

This has been a VERY weird time for me, to be sure, but with your support, we can continue to support local CSA farmers (who have already been hard-hit) and low-income a (even harder hit).

SUPPORT LEVELS AND REWARDS

ANY donation – The 2025 Coloring Book PDF. Donate, and you will get a PDF of the brand-new coloring book (celebrating the $250,000 landmark).

$35 – The 2026 Munchkin Charity Postcard, signed, plus the 2025 Coloring Book

$75 – The 2026 Army of Dorkness Button, maybe it will be a sergeant again, maybe not, we’ll see….plus the 2026 Postcard and the 2025 Coloring Book PDF.

$125 – The Dork Web! You will get a PDF of the next Dork Tower collection as soon as it’s ready, before publication! This is the volume one of a new series, collecting the Dork Tower web strips for the first time! Plus the 2026 Button, the 2026 Postcard and the 2025 Coloring Book PDF.

$200 – All 13 prior Insane Charity Bike Ride Postcards, signed, The Dork Web, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF. (10 of 10 remaining)

$300 – The 2026 Gamehole Con plushie, Betty the Beholder, The Dork Web PDF, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF (20 of 20 remaining)

Betty has her eyes on you!

$400– Original Art from the Munchkin 2nd Edition Core Set!!! I don’t sell original Munchkin art. This is the only way you can obtain a piece, and this year the pieces come from one of the most important releases in Munchkin history! Plus, you get the Beholder plushie, The Dork Web PDF, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF (20 of 20 remaining)

$500 – The Dork Web physical book, which is scheduled to be published Spring 2027! Plus, you get the Original Munchkin Second Edition Art, the Beholder plushie, The Dork Web PDF, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF (20 of 20 remaining) (Please note that the book itself will be sent later, upon publication, and will be numbered, signed, and sketched in)

$750 – Appear in a Dork Tower Comic! You, a friend, your family, or even a pet will appear in a Dork Tower strip! We can talk about how best to fit you or your loved one into a strip. You also get the Dork Web physical copy (upon publication), the Original Munchkin Second Edition Art, the Beholder plushie, The Dork Web PDF, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF (4 of 4 remaining)

$2,000 – Become an Official Munchkin Card! You, a friend, your family, or even a pet will become an official Munchkin card! You also get 100 printed cards and the card’s original art, along with the Dork Web physical copy (upon publication), the Original Munchkin Second Edition Art, the Beholder plushie, The Dork Web PDF, the 2026 Button, 2026 Postcard, and the 2025 Coloring Book PDF (4 of 4 remaining)

SPONSORSHIP LEVEL 1 Add $1,250 to any level for six months of ads on DorkTower.com. The ads may be changed up to four times and may include Dork Tower characters. Your company will be listed as a sponsor on all materials, and will be included in an upcoming Dork Tower comic strip or series, which I will write after working with you on the topic or game you’d like covered. (2 of 2 remaining)

SPONSORSHIP LEVEL 2 Add $2,000 to any level for a full year of ads on DorkTower.com. The ads may be changed up to eight times and may include Dork Tower characters. Your company will be listed as a sponsor on all materials, and will be included in an upcoming Dork Tower comic strip or series, which I will write after working with you on the topic or game you’d like covered. (2 of 2 remaining)

PLEASE NOTE, there may be an added cost for shipping the physical rewards outside of the United States, depending on the whims of the current US administration. We apologize deeply for this, and will contact supporters as the time approaches.

STRETCH GOALS (updated 9/9)

PASSED $5,000 – A sticker commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical orders.

PASSED $7,500 – A new Duck Button will be included with all physical orders. You too may now cycle with a duck!

Screenshot

Passing $10,000 – I’ll take to my bike for the first time since my fall!

Passing $12,000 – A second sticker commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical pledges.

PASSING $14,000 THE DUCK! THE DUCK! THE DUCK! I’ll cycle for the first time concussion with the Steve Jackson Games Duck of Doom on my helmet again!

Passing $16,000 – A unique Munchkin card commemorating the weird-ass “Safety First” Insane Charity Bike Ride 2026 campaign will be included with all physical orders..

Passing $18,000 – Sticker and buttons for ALL riders participating in Bike the Barns 2026, so the entire ride gets a duck on their head (should they so wish)!

Passing $20,000 – THE SECOND DUCK! I’ll ear both the Duck of Doom AND the Duck of Gloom on my helmet (The Duck of Gloom add-on is dedicated to my late friend Andrew Hackard, who first suggested two ducks years ago.)

 

Girl Genius for Monday, September 07, 2026 [Girl Genius]

The Girl Genius comic for Monday, September 07, 2026 has been posted.

Grrl Power #1493 – Buff and wax? [Grrl Power]

I’d wager Max is thinking that it might be time to have “the talk” when she gets home. With Rowan, I mean. Going on a few dates under false pretenses is usually pretty inexcusable, but there are probably some circumstances when it’s… not terrible. Like if you just entered Witness Protection, and start dating a week later… that’s got to feel like a lot of lying. I mean it is, but that’s a case where it’s at least understandable. (Although, come to think of it, you’re probably discouraged from dating or doing anything that requires you to talk about yourself a lot within the first few months in WitPro, to give it time for your new identity to time to sink in. I have no idea either way, but it’d make sense.)

“Dating if you’re a superhero with a secret identity” is probably more of a gray area. I think you’d want to subtly but fairly quickly establish their views on the militarization of police powers and related industrial complexes – at least in the Grrl Verse. If you’re in most other super-verses like Marvel or DC, you’d want to establish their views on vigilante justice. (I think the Avengers are sanctioned law enforcement, depending on what stretch of the comic you’re reading. Maybe the Justice League too? I don’t actually know. But you rarely see Batman or Ant Man giving testimony in trials.)

Anyway, my point is, if you have a secret identity and you’re dating, figure out if you’re wasting your time and theirs early on by learning about their views on your day job. And if you have an intelligence branch as part of your superheroing outfit, do a background check to make sure your beau isn’t an agent of HYDRA or a Skrull infiltrator or some DC equivalent. Or just to make sure they’re not some influencer looking for a big vlog hit.

So you figure out if you’re compatible and that you trust them, you gotta come clean pretty early in the dating process. You can’t True Lies that shit and wait till you have a teenage kid together. Even then, I’m sure plenty of people will be all, “It’s totally unacceptable to lie on a date.” And that’s a valid… desire. Probably a little optimistic, but it’s nice to hope for a more ideal world.

So the next question is… is letting a naked alien man (what, like he’s going to get his pants wet?) wash smart latex off your back in the shower cheating? Galen (and/or Gellen) is going to get a pretty good look at Max’s bare butt. That said, until she grits back up, her butt is so shiny all he’s going to really see is his own face.


Oh, look who it is in the vote incentive. The NSFW version is finally up at Patreon. Plus a bonus pic.

I think she would get in trouble for doing this. She’d mess up the… floor of the waterfall? Is that what it’s called? The receiving pool? No, probably not that. Anyway, she’d churn things up and cause a ton of weird erosion.

Since you might be wondering, Niagara Falls is about 165 feet high, so Babezilla obviously doesn’t have to be full sized. I’d say she’s about 175-180 feet tall here?


Double res version will be posted over at Patreon. Feel free to contribute as much as you like.

Symmetrical [Penny Arcade]

New Comic: Symmetrical

Sunday, 06 September

12:56

Russell Coker: CoMaps [Planet Debian]

I have just tried CoMaps, a free mapping program released under the Apache license [1]. I have tried it on Android on a Pixel 6a but it also runs on Linux so I’ll try it on a PinePhone or similar at some convenient time. On Android it is in the F-Droid repository among others and for Linux there’s a Flatpak package.

The data it uses is from Open Street Map project [2] which has extensive and accurate coverage of every place I’ve looked at (Australia and a few other first-world countries). The first thing it does after being installed is start downloading the world data set from Open Street Map and prompt to download the data for the detected region (Melbourne in my case).

The UI is decent and allows most of the features that I am used to using in Google Maps. The quality of directions seems good, I’ve only tested it with one journey so far which was a 50 minute drive across the city and it gave a set of directions that Google Maps often gives.

It gives spoken directions which is an important feature but sometimes the way the directions are presented is confusing. When turning off a freeway it didn’t give a spoken direction to do that, it gave a direction to “turn right” which was AFTER leaving the freeway, fortunately the map was clearly displayed.

In terms of use practices of this program the main difference I recommend is checking which off ramp to use from a freeway before entering the freeway. With Google Maps you can rely on it giving clear directions in that case.

I recommend this program without reservation. It can do everything that Google Maps does apart from detecting traffic jams because there’s no way of detecting traffic without spying on users. It is designed to preserve user privacy and works well in that regard.

Steinar H. Gunderson: plocate 1.1.25 released [Planet Debian]

I've released version 1.1.25 of plocate. This time around, there's two security issues of unknown severity; if you chain them with other bugs, they could lead to being able to list files (but of course not their contents) that you should not normally be able to see. So an update is probably in order; you can never be too safe these days.

The full changelog is:

plocate 1.1.25, September 6th, 2026

  - Fix two early-exit bugs with multiple databases.
    Reported by Manpreet Singh and Tyler Spivey.

  - Drop setgid properly, including the saved gid.
    Reported by Michal Sekletar, found with the help of Claude Opus 4.6.

  - Fix a potential symlink-checking race in updatedb.
    Reported by Michal Sekletar, found with the help of Claude Opus 4.6.

As usual, you can get it from the home page, or it's on the way up in Debian unstable.

11:21

Enrico Zini: Migrating away from .org/.net/.com domains [Planet Debian]

After having witnessed how easy it is for good people to lose a .org domain over a fascist tantrum (you can follow the Autistici/Inventati story here and here), I've started moving all my infrastructure to differently managed TLDs.

enricozini.org and enricozini.com will keep being functional for the time being, as dropping a domain makes it available for squatting and impersonation.

These new domains are now online, with working web and emails:

It will take ages to migrate countless accounts that are tied to my primary email address, so better start early.

Waiting to see what will happen with .meow domains, which I supported despite not identifying as a cat.

10:21

The lab, the factory and the dentist [Seth's Blog]

At the lab, they don’t know the right answer. They are explorers and scientists. If you’re not failing, you’re not trying hard enough. The work at the lab requires mutual support, shared information and a commitment to the process of discovery.

At the factory, the answer is known. It’s productivity that’s being pursued. Do it a little faster, a little better and a little more reliably than yesterday. Cut costs. Repeat.

And the dentist? It’s not rote work, but it’s not a dance with the unknown. Meet a client, diagnosis the problem and do the work efficiently as well. Different than yesterday, but it certainly rhymes.

Where do you work?

It could be that you’re getting exactly what you signed up for.

09:00

Michael Stapelberg: Debian Code Search: Fast TurboPFor with Go SIMD [Planet Debian]

This August, I accomplished what I wanted for many years: I deleted the last cgo dependency in Debian Code Search! This was made possible by Go’s recently introduced SIMD support, because now we can implement the TurboPFor integer compression format as efficiently — more efficiently, in fact, by using the newer AVX512 instruction set! — as the reference implementation.

Background: Why does DCS need a fast Integer Codec?

Debian Code Search (DCS) is a search engine that allows searching all the Open Source source code within Debian, with either literal search expressions or regular expression search queries.

A search engine uses an inverted index: a map from term to documents containing the term. Each document is typically represented most efficiently by using an id, so the index consists of many lists of document ids.

When searching, it is important to quickly decode these lists to answer the search query. However, there is a point of diminishing returns where the decoding speed, even though it can still be measurably improved quite a bit, no longer influences the overall query duration.

From 2012 (its inception) to 2019, Debian Code Search used to use a small index format, and queries were fast because the index was kept entirely in RAM. In 2019, I implemented the new index format, which adds an on-disk positional index. For literal queries (78.2% of DCS queries), querying the positional index on disk is faster than querying the non-positional index in RAM.

The efficient encoding of the TurboPFor format makes it possible to fit such an index on a mid-sized Hetzner server, which I rent with two 1 TB SSD disks. The optimized decoder of the C TurboPFor library is what made decoding fast at query time.

If you want to dive deeper into the algorithm, see this blog post from February 2019:

If you want to learn more about the positional index, see this blog post from September 2019:

SIMD in Go

For many years, you had the following options for using SIMD instructions in Go:

  1. Hand-writing Go assembler code. This is only doable for small functions, for example bytes.IndexByte is implemented with hand-written Go assembly (including AVX2).
  2. Generating Go assembler code with tools like Michael McLoughlin’s “Avo”. This is how crypto/internal/fips140/sha256 uses AVX2. While Avo generator code definitely is higher-level than hand-written assembly, it is still too close to assembly for my taste.
  3. Use a C library via cgo so gcc or clang compiles SIMD code. Debian Code Search used to use the powturbo/TurboPFor C library via cgo for the last 7 years.

The C TurboPFor library has served us well, but Debian Code Search was always intended to be a project using Go, so I would prefer it if I did not have any C code in the project.

Go 1.26 (released in February 2026) introduced the simd/archsimd package:

Go 1.26 introduces a new experimental simd/archsimd package, which can be enabled by setting the environment variable GOEXPERIMENT=simd at build time. This package provides access to architecture-specific SIMD operations. It is currently available on the amd64 architecture and supports 128-bit, 256-bit, and 512-bit vector types, such as Int8x16 and Float64x8, with operations such as Int8x16.Add. The API is not yet considered stable.

Go 1.26 Release Notes

For my 2019 TurboPFor analysis, I implemented goturbopfor, a native Go teaching decoder (without any SIMD), because I find Go code easier to follow than C code, especially optimized C code. My implementation was intentionally not optimized so that the code was easier to study.

The TurboPFor format/algorithm has a vector-optimized part: bitpacking comes in a scalar variant (bitunpack32) and a vector variant (bitunpack256v32), where the vector variant is used for full blocks (256 values) and the scalar variant is used for remainder blocks (< 256 values).

When Go 1.26 was released, I used Claude Code to explore whether my native Go decoder’s bitunpack256v32 function (for the vertical vector layout) could be implemented using Go SIMD, and the answer was yes, it was possible and it was faster than without SIMD, but not quite at the level of C TurboPFor. If you let Claude Code try for long enough, it eventually finds enough optimizations (about 10) to match C performance.

I don’t want to vibe-code Debian Code Search, though, so I figured I would find some time to review the SIMD code at some point and see if I could implement something similar myself.

Before I found enough time and motivation to complete said review, I discovered that to not regress real-life query performance by more than 10 to 100 milliseconds (which seems acceptable), I don’t actually need to add SIMD code to my teaching decoder at all; it would be sufficient to reduce allocations in my teaching decoder and specialize it per bit width.

Encouraged by the possibility of using the optimized native Go decoder in Debian Code Search, I explored whether I could also implement a native Go encoder so that I could get rid of the C TurboPFor dependency entirely. The answer is yes, it is doable in a few days, and it isn’t even that much slower: Go is at 76% of C, see Debian/dcs commit e920dc7.

The goal I set myself at that point was to see if I could learn enough SIMD to optimize the native Go encoder such that its performance would match how DCS uses C TurboPFor (via cgo).

Beating C TurboPFor was possible in 2-3 commits (SIMD and bit width specialization). To my surprise, Claude Fable 5 pointed out that the encoder’s block scanning could be done more efficiently using a technique called positional popcount, and that is another 2x speed-up! 😲

To be clear: I am not saying the Go compiler beats C here. Certainly, the C compiler can also produce fast AVX512 code and can be used to implement positional popcount. When comparing apples to apples, i.e. backporting the AVX512 kernels and positional popcount technique to C TurboPFor, Go benchmarks a little slower at ≈1.4x C.

This spectacular result (much faster than what DCS had before) got me curious how far I could push the decoder with SIMD after all. I ended up matching/exceeding the cgo version here, too!

The rest of this article explains a few classes of optimizations I encountered along the way.

Starting Point

When I wrote my goturbopfor teaching decoder, I named its functions to match the upstream C TurboPFor library, but now I want to get away from names like p4ndec256v32 — they make sense from the TurboPFor perspective, but for Debian Code Search, we can use cleaner names.

Before writing any code, I audited how DCS uses integer compression / decompression.

API design: BlockEncoder, BlockDecoder and streaming

In Debian Code Search, we have the following usage patterns:

  • Partial Indexing: When a new package (or package version) enters Debian, all of its (text) files are indexed. If the hello-2.12.3-1 package (hypothetically) contained only hello.c with printf("hello!\n");, we would assign document ID 1 to hello.c and store in the partial index that trigrams pri, rin, int, ntf, etc. are all found in doc 1 (hello.c).
  • Full Index Merging: The many thousands of partial index files (for each Debian package) are combined into a small handful of large index files: When searching, it would be expensive to consult thousands of indexes. To merge multiple partial index files into one larger index (which can then be efficiently queried), we need to re-encode the partial index files: what used to be document ID 1 in the partial index might be document ID 2531 in the full index.
  • Querying (searching): When users enter search queries, these queries need to be answered as quickly as possible. The relevant entries in the full indexes are decoded (in parallel).

For reading the index, we do keep the decoded uint32s fully in memory, so we only need DecodeN(input []byte, output []uint32) (read int), a function that reads len(output) values (uint32) from input and returns how many bytes it consumed.

For writing the index (both in partial indexing, and when merging), keeping the entire index in memory is prohibitively expensive, so we need a streaming API, for decoding and for encoding.

Ultimately, I converged on the following API:

package pforenc

type BlockEncoder struct {
    // scratch buffers can go here
}

// EncodeBlock encodes len(vals)<=256 uint32s into dest (one TurboPFor block).
func (*BlockEncoder) EncodeBlock(dest []byte, vals []uint32) []byte {}

// EncodeN calls EncodeBlock in a loop.
func (*BlockEncoder) EncodeN(dest []byte, vals []uint32) []byte {}

type StreamEncoder struct {
  be   BlockEncoder
  vals [256]uint32
  // scratch buffers
}

// if full, you need to call [EncodeBlock]
func (*StreamEncoder) Add(val uint32) (full bool)

// EncodeBlock must be called after all data was [Add]ed.
//
// Write the returned buffer to file or send it over the network;
// it is only valid until the next [EncodeBlock] call.
func (*StreamEncoder) EncodeBlock() []byte {
  if se.n == 0 { return nil } // turn an extra EncodeBlock into a no-op
  // …
}

This API (the decoder works similarly) allows us to process data in TurboPFor format without any memory allocations. The types are not safe for concurrent use by multiple goroutines. The zero value is ready to be used. For the streaming API, the result only stays valid until the next call.

Initial Implementation

Before we can optimize anything, we need a working decoder and encoder. The decoder already exists: my goturbopfor teaching decoder. Next up, I needed an encoder.

Writing a TurboPFor encoder has a delightfully simple starting point: You can encode all values at bit width 32, in little endian, at which point you only need to add a one-byte TurboPFor block header every 256 values and you’re done:

func (be *BlockEncoder) EncodeN(dest []byte, vals []uint32) []byte {
  for len(vals) > 0 {
    chunk := min(len(vals), 256)
    dest = be.EncodeBlock(dest, vals[:chunk])
    vals = vals[chunk:]
  }
  return dest
}

func (be *BlockEncoder) EncodeBlock(dest []byte, vals []uint32) []byte {
  const bitWidth = 32
  dest = append(dest, bitWidth)
  for _, val := range vals {
    dest = binary.LittleEndian.AppendUint32(dest, val)
  }
  return dest
}

Of course, this is a terribly inefficient compressor, so after the first commit, the real work starts: implement each block type until the compression matches the original C TurboPFor implementation (same output file size), or in other words: do the reverse of the decoder.

  1. The TurboPFor bitpacking block type (bitpacking implementation commit) encodes a bit stream of variable bit width (where the bit width is in range 0 ≤ bitWidth ≤ 32) in little endian byte order. By scanning all values and choosing the smallest bit width that allows representing all values, this technique saves disk space (compresses).
  2. The bitpacking with exceptions block type (bitpacking with exceptions implementation commit) determines two bit widths: one for values, the other bit width for encoding exceptions. This allows choosing a lower bit width (that does not cover all values) compared to the bitpacking block type. A bitmap encodes whether a value has an exception or not.
  3. The bitpacking with VB exceptions block type (bitpacking with VB exceptions implementation commit) is a variant which does not use an exception bitmap and encodes exceptions using a variable byte integer encoding. This is more efficient when there are few exceptions (less than 20) or the exceptions are very different in bit width compared to the other values.
  4. Lastly, the constant block type (constant implementation commit) stores just one value on disk. This is useful for all-zero or all-one blocks, for example.

I found it interesting to realize that the main work of the encoder is to scan the input values and choose the optimal block type, whereas the actual encoding itself is cheap in comparison.

At this point, we can look at performance and see that the Go encoder is at 76% of the C encoder.

In all honesty, I could have probably stopped here, but now that the milestone of a viable replacement was reached, I got curious to see how far it would be possible to push the encoder (how much work to reach C speeds?) and afterwards, the decoder, too.

Setup

The microarchitecture level: set GOAMD64

The microarchitecture of a CPU determines which instructions it provides, and that includes not just SIMD instruction sets (like AVX2), but also other useful instructions like LZCNT (Leading Zero Count), which can be used to implement math/bits.Len32 more efficiently, which the TurboPFor encoder needs to call on every input value to determine the ideal bit width.

Let’s walk through how to set the microarchitecture level when using Go on 64-bit x86 (x86-64).

Go uses the GOARCH environment variable to configure the target compilation architecture, and I am using the value amd64 to select 64-bit x86 (AVX2 and AVX512 are instruction sets found on x86-64 CPUs). With GOARCH=amd64, the architecture-specific variable GOAMD64 configures the microarchitecture level for which to compile and Go 1.18 introduced these 4 different levels:

GOAMD64=v1 (default): The baseline.
Exclusively generates instructions that all 64-bit x86 processors can execute.

GOAMD64=v2: all v1 instructions,
plus CMPXCHG16B, LAHF, SAHF, POPCNT, SSE3, SSE4.1, SSE4.2, SSSE3.

GOAMD64=v3: all v2 instructions,
plus AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, OSXSAVE.

GOAMD64=v4: all v3 instructions,
plus AVX512F, AVX512BW, AVX512CD, AVX512DQ, AVX512VL.

In 2026, I generally recommend compiling with GOAMD64=v3 so that functions like bits.OnesCount8 are compiled into intrinsics (POPCNT) instead of using a lookup table.

For Intel CPUs, setting GOAMD64=v3 means your programs will only start on Haswell CPUs (2013) or newer; for AMD CPUs that means Zen 1 (2017) or newer.

In this specific case (DCS), I am even compiling with GOAMD64=v4. The v4 microarchitecture level requires AVX512, which means AMD Zen 4, Zen 5 or newer (Intel’s story is… complicated). Luckily, both my main development PC (Zen 5) and the Debian Code Search server (Zen 4) are recent enough. Setting GOAMD64=v4 has little effect on Go 1.27 itself: the only change is that maps use one less instruction (VPBROADCASTB instead of PSHUFB). But compiling with GOAMD64=v4 allows us to move one more feature check from runtime to compile time, see SIMD build tags.

It makes sense to set the microarchitecture level in your benchmark setup so that you don’t measure the slow fallback implementations. I use export GOAMD64=v4 in my Makefile.

Benchmarking setup

Go’s built-in testing package contains support for benchmarks which are written in functions of the form func BenchmarkXxx(b *testing.B). The simplest way to run such benchmarks is go test -bench=., but I ended up configuring a few convenience make targets, which write results to bench.txt and compare against baseline.txt (the previous commit’s results, usually), using the very useful benchstat tool.

GOTEST=go test

# -count=6 gives p≤0.002 in benchstat:
# https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
BENCHFLAGS=-run=^$$ -bench=. -benchtime=200000x -count=6

# use taskset -c1 to always pin to the same single core,
# avoiding accidental scheduling on different cores on
# mixed-core CPUs like the Ryzen 9 9950X3D.
TASKSET=taskset -c 1
BENCH=$(TASKSET) $(GOTEST) $(BENCHFLAGS)

.PHONY: all test bench bench-baseline bench-relative

all: test

bench: test
       $(BENCH) | tee bench.txt
# Compares compression ratio between C and Go implementation
        benchstat -col /impl -row '/n /vals' -filter '-/impl:go-stream .unit:(encoded-bytes)' bench.txt
# Compares performance between C (cgo) and Go implementation
        benchstat -col /impl -row '/n /vals' -filter '.unit:(Mval/s)' bench.txt

bench-baseline: test
       $(BENCH) | tee baseline.txt

bench-relative: test
       $(BENCH) | tee bench.txt
       benchstat -filter '-/impl:go-stream .unit:(encoded-bytes)' baseline.txt bench.txt
       benchstat -filter '/impl:go .unit:(Mval/s)' baseline.txt bench.txt

The encoded-bytes and Mval/s units are custom metrics I am reporting from the various sub-benchmarks, which are arranged such that I can filter / report them with benchstat.

The main encoder (and decoder) benchmarks compare 3 different implementations (cgo, Go, Go with the StreamEncoder API) with a number of benchmark cases that are designed to cover the different block types and contain a similar mix of values as what we see in Debian Code Search:

// reportMetrics adds Mval/s and encoded-bytes metrics to all benchmarks.
func reportMetrics(b *testing.B, n int, nencoded int) {
   b.ReportMetric(float64(nencoded), "encoded-bytes")
   b.ReportMetric(float64(b.N*n)/1e6/b.Elapsed().Seconds(), "Mval/s")
}

// BenchmarkEncode/n=<N>/vals=<testcase>/impl=<c|go|go-stream>
//
// e.g. BenchmarkEncode/n=2048/vals=one-constant/impl=go-stream
func BenchmarkEncode(b *testing.B) {
   for _, tc := range allBenchCases() {
     n := len(tc.vals)
     b.Run(fmt.Sprintf("n=%d/vals=%s", n, tc.name), func(b *testing.B) {
       b.Run("impl=c", func(b *testing.B) {
         b.ReportAllocs()
         var encoded []byte
         buf := make([]byte, turbopfor.EncodingSize(n))
         for b.Loop() {
           encoded = turbopfor.P4nenc256v32Buf(buf, tc.vals)
         }
         reportMetrics(b, n, len(encoded))
       })
       b.Run("impl=go", func(b *testing.B) {
         b.ReportAllocs()
         var be BlockEncoder
         var encoded []byte
         buf := make([]byte, 0, turbopfor.EncodingSize(n))
         for b.Loop() {
           encoded = be.EncodeN(buf, tc.vals)
         }
         reportMetrics(b, n, len(encoded))
       })
       b.Run("impl=go-stream", func(b *testing.B) {
         b.ReportAllocs()
         var se StreamEncoder
         var encoded int
         for b.Loop() {
           encoded = 0
           for _, val := range tc.vals {
             if se.Add(val) {
               encoded += len(se.EncodeBlock())
             }
           }
           encoded += len(se.EncodeBlock())
         }
         reportMetrics(b, n, encoded)
       })
     })
   }
}

CPU counters: perf

Go has included excellent performance tooling for many years, see the “Profiling Go Programs” blog post (2011) for an example of how to use pprof, a sampling profiler. This profiler can help track down which part of a program runs slow, or where memory allocations happen.

Once you identified the slow part of a program, how do you know why it’s slow?

To learn more about the specific bottlenecks your program encounters, you can consult your CPU’s hardware performance counters. For example, you could check the branch predictor counters to see if your program is slow due to a high number of branch mispredicts.

On Linux, the perf tool is the best way to access the CPU hardware performance counters. A good starting point for working with perf is the documentation on “Top-down analysis with the perf tool”, which describes the optimization method that Intel established.

In my Makefile, I set up two perf targets:

# GOTEST and TASKSET like shown in the earlier benchmarking setup section:
GOTEST=go test -pgo=encode.cpuprof
TASKSET=taskset -c 1
PERFBENCHFLAGS=-test.bench='Encode/n=2048/vals=debian-mix/impl=go$$' -test.benchtime=200000x

# Use perf(1) to capture AMD IBS (the equivalent to Intel PEBS)
# PipelineL1 is roughly equivalent to Intel TopdownL1
perf:
       $(GOTEST) -c
       $(TASKSET) perf stat -M PipelineL1 ./pforenc.test -test.run=^$$ $(PERFBENCHFLAGS)
       sudo perf record -F 4999 -e ibs_op// --call-graph fp ./pforenc.test -test.run=^$$ $(PERFBENCHFLAGS)
       sudo chmod 644 perf.data

# 488281 iterations × 2048 values = 1.000e9 values, so counter/1e9 = per value.
perf-per-value:
       $(GOTEST) -c
       $(TASKSET) perf stat -x, -e cycles:u,instructions:u,branches:u,branch-misses:u ./pforenc.test -test.run=^$$ -test.bench='Encode/n=2048/vals=debian-mix/impl=go$$' -test.benchtime=488281x 2>&1 >/dev/null | awk -F, '{printf "%-16s %6.2f /val\n", $$3, $$1/1e9}'

The perf-per-value numbers are high level numbers that indicate how much work the implementation is doing. Reducing the number usually increases speed.

To see the counters for each instruction (and source code lines), I use make perf, followed by perf report. A quick shortcut is perf annotate, which directly shows the hottest function.

Optimizations (scalar)

Let’s first see how far we can get without reaching for SIMD instructions.

(The examples are not necessarily in commit order, but cherry-picked for clarity.)

Profile-Guided Optimization (PGO)

PGO stands for Profile-Guided Optimization and is a feature that Go introduced as a preview in Go 1.20 (released in February 2023) and shipped as ready for general production use in Go 1.21 (released in August 2023).

The idea is to capture a CPU profile that records where your program spends most of its CPU time, which you then provide to the Go compiler to give it more data to make better decisions.

Most importantly, this way the Go compiler can inline functions much more aggressively than its usual heuristics allow, which does have a measurably positive effect in my series of optimization commits. Another optimization that a PGO profile allows the compiler to do is conditional devirtualization — but our TurboPFor code does not use any interfaces.

My strategy is to enable PGO before doing any other optimizations, so that we have the full inlining budget available that PGO gives us, and can measure the effect of other commits clearly.

Surprisingly, turning on PGO actually decreases our performance (-13% geomean), but a closer investigation reveals that we just got unlucky. Let me explain.

Aside from inlining and conditional devirtualization, PGO also influences alignment: The Go compiler sets PCALIGNMAX(64, 31) on the first block of a loop (the “loop body”) for all loops in hot functions (per the PGO profile), i.e. Go will insert up to 31 bytes of padding to make the block land on a 64-byte boundary. Documentation like AMD’s “Software Optimization Guide for the AMD Zen5 Microarchitecture” (2024, #58455) explicitly recommends aligning hot loops that way:

[…] for hot loops, some further knowledge of trade-offs can be helpful. Because the processor can read an aligned 64-byte fetch block every cycle, it is suggested to either align the start of the loop to the beginning of a 64-byte cache line […]

Indeed, when compiling with -gcflags=all=-d=alignhot=0 to disable the alignment, performance remains as good as without PGO. How can the padding hurt more than help? The answer is: It’s not the padding itself! It’s a side-effect of the padding moving instructions to different addresses.

In the unlucky arrangement, a macro-fused CMPQ+JGE instruction pair now ends up exactly on a 32-byte boundary. However, the Go compiler ensures fused branch sequences must never cross or end at a 32-byte boundary to fix Intel erratum SKX102 (discussion: Go issue #35881) by inserting NOPs.

This NOP padding, unlike the loop alignment padding, is not free; these extra instructions slow down our otherwise dispatch-bound loops.

Because the commits after the PGO enabling commit change the code, this unlucky situation is avoided for the rest of the optimization series (by chance).

Reducing memory allocations

Memory allocations are quite expensive, at least in comparison to encoding/decoding integers, so I followed my usual strategy of first reducing memory allocations as much as possible.

In my goturbopfor teaching decoder, whenever the code needed a scratch buffer, it would allocate it right then and there with make():

// p4dec32 decodes one block of TurboPFor-encoded 32 bit ints
func (d *decoder) p4dec32(input []byte, output []uint32) (read int) {
    // …
  switch blockType {
  case blockBitpackingExceptions:
    bx, input := input[0], input[1:]
    n := len(output)

    exmap := input
    nex := 0 // number of exceptions
    for i := 0; i < n; i++ {
      if exmap[i/8]&(1<<uint(i%8)) != 0 {
        nex++
      }
    }
    input = input[(n+7)/8:]

    exceptions := make([]uint32, nex)
    input = input[bitunpack32(input, exceptions, bx):]
    input = input[d.bitunpack(input, output, b):]

    for i := 0; i < n; i++ {
      if exmap[i/8]&(1<<uint(i%8)) != 0 {
        output[i] += exceptions[0] << b
        exceptions = exceptions[1:]
      }
    }

    return before - len(input)
  }
}

The Go compiler can turn make(T, n) calls into stack allocations, if n is known at compile-time. But, in this case nex is not known at compile-time. We can verify that Go calls into the runtime (runtime.makeslice) by dumping the object code (assembly) with source annotated (-S):

% cd ~/go/src/github.com/stapelberg/goturbopfor
% git reset --hard 49b7c05cc61e77f0257568eb73833467714d2b4a
% go test -c  # go1.27.0
% go tool objdump -S goturbopfor.test | perl -nlE 'say if /p4dec32/ .. /^$/'
TEXT github.com/stapelberg/goturbopfor.(*decoder).p4dec32(SB) /home/michael/go/src/github.com/stapelberg/goturbopfor/goturbopfor.go
func (d *decoder) p4dec32(input []byte, output []uint32) (read int) {
  0x549f60             4c8da42460ffffff        LEAQ 0xffffff60(SP), R12
  0x549f68             4d3b6610                CMPQ R12, 0x10(R14)
  0x549f6c             0f86d9070000            JBE 0x54a74b
  0x549f72             55                      PUSHQ BP
  0x549f73             4889e5                  MOVQ SP, BP
  0x549f76             4881ec18010000          SUBQ $0x118, SP
  0x549f7d             48899c2430010000        MOVQ BX, 0x130(SP)
  0x549f85             4889b42448010000        MOVQ SI, 0x148(SP)
       if len(output) == 0 {
  0x549f8d             4d85c0                  TESTQ R8, R8
  0x549f90             0f84a7030000            JE 0x54a33d
  0x549f96             660f1f840000000000      NOPW 0(AX)(AX*1)
  0x549f9f             90                      NOPL
[…]
            exceptions := make([]uint32, nex)
  0x54a4be          488d057bec1700          LEAQ 0x17ec7b(IP), AX
  0x54a4c5          4c89fb                  MOVQ R15, BX
  0x54a4c8          4889d9                  MOVQ BX, CX
  0x54a4cb          e8f0ddf3ff              CALL runtime.makeslice(SB)
[…]

An easy speed-up was to avoid allocations through reuse (in goturbopfor). In the DCS pfordec package (with the improved API design), I ended up with a vals [256]uint32 field in the StreamDecoder type, which brings us from 773 Mval/s to 858 Mval/s on the debian-mix:

% benchstat -filter '/impl:go /vals:debian-mix .unit:(Mval/s)' \
  baseline.txt bench.txt
goos: linux
goarch: amd64
pkg: github.com/Debian/dcs/internal/turbopfor/pfordec
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
           │ baseline.txt │             bench.txt              │
           │    Mval/s    │   Mval/s     vs base               │
n=2048        1.089k ± 1%   1.175k ± 0%   +7.85% (p=0.002 n=6)
n=2039         974.7 ± 0%   1046.0 ± 0%   +7.32% (p=0.002 n=6)
n=160          434.9 ± 1%    513.6 ± 5%  +18.11% (p=0.002 n=6)
geomean        772.9         857.7       +10.98%

Aside from the speed-up, avoiding memory allocations is generally nice in benchmarks because it removes the garbage collector from the equation and makes it less likely that your benchmarks get other processes OOM-killed on the same machine.

Generics for bit width specialization

In general, we want to make it easy for the compiler to understand as much as possible about our algorithm. Consider this bitpack implementation:

func bitpack(dest []byte, vals []uint32, bitWidth int) []byte {
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have int
  for _, val := range vals {
    acc |= uint64(val&mask) << have
    have += bitWidth
    for have >= 32 {
      dest = binary.LittleEndian.AppendUint32(dest, uint32(acc))
      acc >>= 32
      have -= 32
    }
  }
  for have > 0 {
    dest = append(dest, byte(acc))
    acc >>= 8
    have -= 8
  }
  return dest
}

Let’s think through what determines the iterations and control flow this function uses:

  1. The number of input values (vals), but not their actual value.
  2. The bit width to pack into (bitWidth).

With a bit of careful rearrangement, we can provide the compiler with both, a fixed number of input values (say, 32), and a bit width, both known at compile time. Why is this worthwhile? Because we can manually unroll the loop, let the compiler eliminate much of the repetition and get much faster compiled code as a result!

Let’s first fix the number of input values to 32 and rewrite the loop to calculate the position offsets within dest instead of changing dest on each value (with AppendUint32):

func bitpack32Unrolled(dest []byte, vals *[32]uint32, bitWidth int) {
  // only one bounds check for 32 values
  dest = dest[: 4*bitWidth : 4*bitWidth]
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have, pos int
  // Manually unrolled loop starts here.
  // Each iteration is identical except for the vals[x] index.
  acc |= uint64(vals[0]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // vals[1] .. vals[30] elided for brevity

  // Each loop iteration is 8 lines of Go code, so for 32 input values,
  // bitpack32Unrolled contains 8*32 = 256 lines of code.

  acc |= uint64(vals[31]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // have == 0; for all bitWidths
}

Next, we want to specialize not just for 32 input values, but also for each of the 32 bit widths.

Can we do better than hand-copying bitpack32Unrolled 32 times (= 8192 lines of Go code)?

Yes, we can use Go generics to help us with the code generation!

In Go, array types like [4]byte (not slices like []byte!) contain the length of the array as part of their type, meaning [1]byte (an array of length 1) is a different type than [2]byte.

Instead of passing the bit width as a function parameter, we can declare 32 different types (one for each bit width) and recover the bit width (at compile time!) from the type system:

type bitWidthT interface {
  [1]byte | [2]byte | [3]byte | [4]byte | [5]byte |
  [6]byte | [7]byte | [8]byte | [9]byte | [10]byte |
  [11]byte | [12]byte | [13]byte | [14]byte | [15]byte |
  [16]byte | [17]byte | [18]byte | [19]byte | [20]byte |
  [21]byte | [22]byte | [23]byte | [24]byte | [25]byte |
  [26]byte | [27]byte | [28]byte | [29]byte | [30]byte |
  [31]byte | [32]byte
}

func bitpack32Unrolled[T bitWidthT](dest []byte, vals *[32]uint32) {
  var zero T
  bitWidth := len(zero)                  // known at compile time
  dest = dest[: 4*bitWidth : 4*bitWidth] // make cap known at compile time
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have, pos int
  // Manually unrolled loop starts here.
  // Each iteration is identical except for the vals[x] index.
  acc |= uint64(vals[0]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // vals[1] .. vals[31] elided for brevity
}

When we instantiate bitpack32Unrolled[bitWidthT] with all 32 different types ([1]byte, [2]byte, …, [32]byte), the compiler substitutes the bitWidthT type parameter and produces 32 copies of the function, which we can find in our compiled executable with names like github.com/Debian/dcs/internal/turbopfor/pforenc.bitpack32Unrolled[go.shape.[12]uint8]. The “shape” of a generic type is based on its memory layout, so a shape for [1]byte must be different than the shape for [2]byte.

Because the bitWidth is now known at compile time, the Go compiler can generate close to the optimal machine code for each bit width, which we can confirm using go tool objdump.

The code is branchless (after the one bounds check per 32 values) and aside from the loads and stores (from/to memory) consists only of shifts and bit operations, all with constant operands:

% go test -c && go tool objdump -S pforenc.test
[…]
TEXT github.com/Debian/dcs/internal/turbopfor/pforenc.bitpack32Unrolled[go.shape.[28]uint8](SB) /home/michael/dcs/internal/turbopfor/pforenc/bitpackunroll.go
func bitpack32Unrolled[T bitWidthT](dest []byte, vals *[32]uint32) {
  0x660580              55                      PUSHQ BP
  0x660581              4889e5                  MOVQ SP, BP
  0x660584              48895c2418              MOVQ BX, 0x18(SP)
        dest = dest[: 4*bitWidth : 4*bitWidth] // make cap known at compile time
  0x660589              4883ff70                CMPQ DI, $0x70
  0x66058d              0f820b030000            JB 0x66089e
        acc |= uint64(vals[0]&mask) << have
  0x660593              8b06                    MOVL 0(SI), AX
  0x660595              25ffffff0f              ANDL $0xfffffff, AX
        acc |= uint64(vals[1]&mask) << have
  0x66059a              8b4e04                  MOVL 0x4(SI), CX
  0x66059d              81e1ffffff0f            ANDL $0xfffffff, CX
  0x6605a3              48c1e11c                SHLQ $0x1c, CX
  0x6605a7              4809c8                  ORQ CX, AX
                acc >>= 32
  0x6605aa              4889c1                  MOVQ AX, CX
  0x6605ad              48c1e820                SHRQ $0x20, AX
                binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
  0x6605b1              90                      NOPL
        b[0] = byte(v)
  0x6605b2              890b                    MOVL CX, 0(BX)
        acc |= uint64(vals[2]&mask) << have
  0x6605b4              8b4e08                  MOVL 0x8(SI), CX
  0x6605b7              81e1ffffff0f            ANDL $0xfffffff, CX
  0x6605bd              48c1e118                SHLQ $0x18, CX
  0x6605c1              4809c1                  ORQ AX, CX
                acc >>= 32
  0x6605c4              4889c8                  MOVQ CX, AX
  0x6605c7              48c1e920                SHRQ $0x20, CX
                binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
  0x6605cb              90                      NOPL
        b[0] = byte(v)
  0x6605cc              894304                  MOVL AX, 0x4(BX)

Now we need to actually call bitpack32 from the general bitpack function:

func bitpack(dest []byte, vals []uint32, bitWidth int) []byte {
  if bitWidth == 0 {
    return dest // no payload, sparse block with only exceptions
  }
  if len(vals) >= 32 {
    size := 4 * bitWidth
    for len(vals) >= 32 {
      existing := len(dest)
      dest = slices.Grow(dest, size)[:existing+size]
      bitpack32(dest[existing:] /*append*/, (*[32]uint32)(vals), bitWidth)
      vals = vals[32:]
    }
  }
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have int
  for _, val := range vals {
    acc |= uint64(val&mask) << have
    have += bitWidth
    for have >= 32 {
      dest = binary.LittleEndian.AppendUint32(dest, uint32(acc))
      acc >>= 32
      have -= 32
    }
  }
  for have > 0 {
    dest = append(dest, byte(acc))
    acc >>= 8
    have -= 8
  }
  return dest
}

func bitpack32(dest []byte, vals *[32]uint32, bitWidth int) {
  switch bitWidth {
  case 1: bitpack32Unrolled[[1]byte](dest, vals)
  case 2: bitpack32Unrolled[[2]byte](dest, vals)
  case 3: bitpack32Unrolled[[3]byte](dest, vals)
  case 4: bitpack32Unrolled[[4]byte](dest, vals)
  case 5: bitpack32Unrolled[[5]byte](dest, vals)
  case 6: bitpack32Unrolled[[6]byte](dest, vals)
  case 7: bitpack32Unrolled[[7]byte](dest, vals)
  case 8: bitpack32Unrolled[[8]byte](dest, vals)
  case 9: bitpack32Unrolled[[9]byte](dest, vals)
  case 10: bitpack32Unrolled[[10]byte](dest, vals)
  case 11: bitpack32Unrolled[[11]byte](dest, vals)
  case 12: bitpack32Unrolled[[12]byte](dest, vals)
  case 13: bitpack32Unrolled[[13]byte](dest, vals)
  case 14: bitpack32Unrolled[[14]byte](dest, vals)
  case 15: bitpack32Unrolled[[15]byte](dest, vals)
  case 16: bitpack32Unrolled[[16]byte](dest, vals)
  case 17: bitpack32Unrolled[[17]byte](dest, vals)
  case 18: bitpack32Unrolled[[18]byte](dest, vals)
  case 19: bitpack32Unrolled[[19]byte](dest, vals)
  case 20: bitpack32Unrolled[[20]byte](dest, vals)
  case 21: bitpack32Unrolled[[21]byte](dest, vals)
  case 22: bitpack32Unrolled[[22]byte](dest, vals)
  case 23: bitpack32Unrolled[[23]byte](dest, vals)
  case 24: bitpack32Unrolled[[24]byte](dest, vals)
  case 25: bitpack32Unrolled[[25]byte](dest, vals)
  case 26: bitpack32Unrolled[[26]byte](dest, vals)
  case 27: bitpack32Unrolled[[27]byte](dest, vals)
  case 28: bitpack32Unrolled[[28]byte](dest, vals)
  case 29: bitpack32Unrolled[[29]byte](dest, vals)
  case 30: bitpack32Unrolled[[30]byte](dest, vals)
  case 31: bitpack32Unrolled[[31]byte](dest, vals)
  case 32: bitpack32Unrolled[[32]byte](dest, vals)
  }
}

Encoding remainder blocks is quite a bit faster (full blocks use the vertical layout anyway):

% benchstat -filter '/impl:go /n:160 .unit:(Mval/s)' baseline.txt bench.txt
goos: linux
goarch: amd64
pkg: github.com/Debian/dcs/internal/turbopfor/pforenc
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
                         │ baseline.txt │             bench.txt              │
                         │    Mval/s    │   Mval/s     vs base               │
vals=bitpacking-bw1          751.2 ± 3%   1120.5 ± 0%  +49.15% (p=0.002 n=6)
vals=bitpacking-bw2          716.8 ± 2%   1176.0 ± 0%  +64.07% (p=0.002 n=6)
vals=bitpacking-bw7          700.0 ± 1%   1078.5 ± 0%  +54.08% (p=0.002 n=6)
vals=bitpacking-bw1-exc      524.8 ± 1%    736.8 ± 0%  +40.40% (p=0.002 n=6)
vals=bitpacking-bw2-exc      543.7 ± 1%    758.2 ± 0%  +39.46% (p=0.002 n=6)
vals=bitpacking-bw7-exc      566.7 ± 1%    787.7 ± 0%  +38.99% (p=0.002 n=6)
vals=bitpacking-vb-exc       442.6 ± 1%    616.5 ± 0%  +39.29% (p=0.002 n=6)
vals=sparse-exc              532.4 ± 0%    787.8 ± 0%  +47.97% (p=0.002 n=6)
vals=sparse-vb-exc           408.9 ± 1%    597.8 ± 0%  +46.20% (p=0.002 n=6)
vals=debian-mix              559.5 ± 0%    783.8 ± 9%  +40.09% (p=0.002 n=6)

This performance win comes at the cost of binary size increase. In this case, the .text section (executable code) grows by about 20 KB and the .gopclntab section grows by another 26 KB. Definitely a price I am very willing to pay, but the case might not be as clear in all circumstances.

Optimization: Bigger strides with SIMD

Even without reaching for SIMD instructions, a TurboPFor implementation can be made faster by making it work bigger strides. Take this code from the goturbopfor teaching decoder which counts the number of exceptions by checking if each value’s bit is set in the exception bitmap:

case blockBitpackingExceptions:
  bx, input := input[0], input[1:]
  n := len(output)

  exmap, input := input, input[(n+7)/8:]
  nex := 0 // number of exceptions
  for i := range n {
    if exmap[i/8]&(1<<uint(i%8)) != 0 {
      nex++
    }
  }
  exceptions := d.scratch[:nex]

We can use the bits.OnesCount64 functions to count ones bits in the exception bitmap, 64 values at a time. For remainder blocks, the rest is processed 8 values (1 byte) at a time:

i := 0
for ; i+8 <= n/8; i += 8 {
  xm8 := binary.LittleEndian.Uint64(exmap[i:])
  nex += bits.OnesCount64(xm8)
}
for ; i < (n+7)/8; i++ {
  xmb := exmap[i]
  // Clear the bits which do not belong to the exception map:
  if rem := n - i*8; rem < 8 {
    xmb &= 1<<rem - 1
  }
  // Go compiles OnesCount32 into an intrinsic,
  // but not OnesCount8, so we convert to uint32:
  nex += bits.OnesCount32(uint32(xmb))
}

OnesCount64 uses a 64-bit register. For comparison, AVX2 SIMD instructions use 256-bit registers (= 8 uint32) and AVX512 SIMD instructions use 512-bit registers.

In the following sections, we will first set up our build tags for conditional compilation to use a trivial SIMD instruction, then walk through an AVX2 and AVX512 SIMD kernel.

SIMD build tags

Let’s assume we have the following scalar code:

constant.go:

package pfordec

func fillConstant(output []uint32, val uint32) {
  for i := range output {
    output[i] = val
  }
}

To increase throughput, we can use AVX2 instructions if they are available on the CPU on which the program runs, i.e. using runtime dispatch. We’ll first rename fillConstant to fillConstantScalar (it’s now the fallback path):

constant.go:

package pfordec

func fillConstantScalar(output []uint32, val uint32) {
  for i := range output {
    output[i] = val
  }
}

Next, we’ll supply two different implementations (constant_nosimd.go and constant_amd64.go), the latter of which is selected when compiling for GOARCH=amd64 with GOEXPERIMENT=simd (the latter will hopefully be dropped in a later version of Go). The nosimd variant just dispatches to the fillConstantScalar, which will likely be inlined:

//go:build !goexperiment.simd || !amd64

package pfordec

func fillConstant(output []uint32, val uint32) {
  fillConstantScalar(output, val)
}

The constant_amd64.go variant assigns the hasAVX2 global variable by doing a CPUID check and then jumps to the scalar fallback if !hasAVX2, i.e. the CPU is too old:

//go:build goexperiment.simd && amd64

package pfordec

import "simd/archsimd"

var hasAVX2 = archsimd.X86.AVX2()

func fillConstant(output []uint32, val uint32) {
  if !hasAVX2 {
    fillConstantScalar(output, val)
    return
  }
  val8 := archsimd.BroadcastUint32x8(val)
  i := 0
  for ; i+8 <= len(output); i += 8 {
    val8.StoreArray((*[8]uint32)(output[i : i+8]))
  }
  // use the scalar implementation for the last <= 7 elements
  fillConstantScalar(output[i:], val)
}

We can go one step further by conditionally compiling const hasAVX2 = true when GOAMD64 is set to v3 or higher (i.e. the amd64.v3 build tag is set). As a practical example from Debian Code Search, we currently need the following checks / dispatches:

code function vector instruction set GOAMD64
encoder bitpack256v AVX2 GOAMD64=v3
encoder exbitmap AVX512 GOAMD64=v4
encoder scan AVX512+VBMI+GFNI+BITALG n/a
decoder bitunpack AVX2 GOAMD64=v3
decoder bitunpack256v32 AVX2 GOAMD64=v3
decoder bitunpack256v32Ex AVX512 GOAMD64=v4

In DCS, the effect is measurably positive, but small.

The 256 uint32 vertical layout

First, here is the layout explanation from my 2019 TurboPFor analysis blog post:

In regular (non-SIMD) bitpacking, integers are stored on disk one after the other, padded to a full byte, as a byte is the smallest addressable unit when reading data from disk. For example, if you bitpack only one 3 bit int, you will end up with 5 bits of padding.

SIMD bitpacking works like regular bitpacking, but processes 8 uint32 little-endian values at the same time, leveraging the AVX instruction set. The following illustration shows the order in which 3-bit integers are decoded from disk:

The scalar implementation uses an array of 8 uint64 to process 8 values at a time:

func bitunpack256v32(input []byte, dest []uint32, bitWidth int) (read int) {
  mask := uint64(1)<<bitWidth - 1
  orig := len(input)
  var bits uint
  var acc [8]uint64 // accumulator: current+next bits
  for op := 0; op < len(dest); {
    if bits < uint(bitWidth) {
      // read 8 more uint32s
      for i := range 8 {
        acc[i] |= uint64(binary.LittleEndian.Uint32(input)) << bits
        input = input[4:]
      }
      bits += 32
    }
    for i := range 8 {
      dest[op] = uint32(acc[i] & mask)
      op++
      acc[i] >>= bitWidth
    }
    bits -= uint(bitWidth)
  }
  return orig - len(input)
}

The SIMD version also processes 8 values, but without a for i := range 8 loop!

One difference is that we no longer have the luxury of using uint64 for acc (holding rest and current bits); because AVX2 registers only fit 8 uint32 (not 8 uint64). Instead, we split acc into rest8 and cur8.

func bitunpack256v32(fullinput []byte, fulldest []uint32, bitWidth int) (read int) {
  dest := fulldest[:256]
  if bitWidth == 0 {
    clear(dest)
    return 0
  }
  n := 32 * int(bitWidth)
  input := fullinput[:n] // tell the Go compiler how long the input is
  mask8 := archsimd.BroadcastUint32x8(uint32(1)<<bitWidth - 1)
  bitWidth8 := archsimd.BroadcastUint32x8(uint32(bitWidth))
  var bits uint
  pos := 0
  // var acc [8]uint64
  var rest8 archsimd.Uint32x8
  var cur8 archsimd.Uint32x8
  for op := 0; op < 256; op += 8 {
    if bits < uint(bitWidth) {
      // read 8 more uint32s
      // acc[i] |= uint64(binary.LittleEndian.Uint32(input)) << bits
      next := archsimd.LoadUint8x32(input[pos : pos+32]).ReshapeToUint32s()
      pos += 32  // input = input[4:]
      cur8 = rest8.Or(next.ShiftAllLeft(uint64(bits)))
      // acc[i] >>= bitWidth
      rest8 = next.ShiftAllRight(uint64(uint(bitWidth) - bits))
      bits += 32
    } else {
      cur8 = rest8
      // acc[i] >>= bitWidth
      rest8 = rest8.ShiftRight(bitWidth8)
    }
    // dest[op] = uint32(acc[i] & mask)
    cur8.And(mask8).Store(dest[op : op+8])
    bits -= uint(bitWidth)
  }
  return n
}

The SIMD version benchmarks about 3x as fast as the scalar version.

Another significant speedup is to use generics for bit width specialization for this SIMD kernel so that bitWidth becomes a compile-time constant and the compiler can generate better code.

Positional Popcount

For my TurboPFor encoder, I implemented the same techniques as described above:

  1. Bitpack full blocks with SIMD (AVX2)

  2. Gather exceptions using SIMD (AVX512)

  3. Use generics to specialize per bit width

These changes are sufficient to roughly match the cgo performance, but then Claude Fable 5 found another 2x speed-up on top of that!

The key observation is that once encoding blocks is fast, the preceding step of scanning the input values to decide which block type to use becomes the bottleneck. Here is the encoder’s main encode function, which first does one pass over the input values (scan) and then prices all different block types at all relevant bit widths (requires fast access to the scan histogram):

func (be *BlockEncoder) encode(dest []byte, vals []uint32, layout blockLayout) []byte {
  var stats stats
  scan(&stats, vals) // gathers statistics from every value in vals
  bitWidth := bits.Len32(stats.or)
  if stats.or == stats.and {
    return be.encodeConstant(dest, vals, bitWidth)
  }
  n := len(vals)
  // bitpacking is the default, unless we find a more efficient block type.
  bestType := blockBitpacking
  bestB := bitWidth
  best := priceBitpack(n, bitWidth, layout)

  // Walk from high bitWidths to low: to break ties, we prefer
  // the encoding with fewer exceptions (for faster decoding).
  for b := bitWidth - 1; b >= 0; b-- { // up to 32 iterations
    nex := int(stats.cnt[b])
    size := priceBitpackExceptions(n, b, bitWidth, nex, layout)
    if size < best {
      bestType = blockBitpackingExceptions
      bestB = b
      best = size
    }
    // Over-approximate the number of VB bytes.
    vb := nex + // exceptions using 1, 2, 3, 4, or 5 VB bytes
      int(stats.cnt[b+7]+ // exceptions using 2, 3, 4, or 5 VB bytes
        stats.cnt[b+14]+ // exceptions using 3, 4, or 5 VB bytes
        stats.cnt[b+19]+ // exceptions using 4 or 5 VB bytes
        stats.cnt[b+24]) // exceptions using 5 VB bytes
    size = headerBytes + headerExBytes + payloadBytes(n, b, layout) + vb + nex
    if size < best {
      bestType = blockBitpackingVBExceptions
      bestB = b
      best = size
    }
  }
  switch bestType {
  case blockBitpacking:
    return be.encodeBitpack(dest, vals, layout, bitWidth)
  case blockBitpackingExceptions:
    return be.encodeBitpackExc(dest, vals, layout, bestB, bitWidth-bestB)
  case blockBitpackingVBExceptions:
    return be.encodeBitpackVBExc(dest, vals, layout, bestB, int(stats.cnt[bestB]))
  default:
    panic("BUG: bestType not implemented")
  }
}

I’ll show you a slightly shortened version of scan, the function which is the bottleneck:

type stats struct {
  // cnt[n] = how many values where bits.Len32(val)>n,
  // i.e. how many exceptions are required for bitWidth=n.
  // Padded so that cnt[b+24] is always in bounds.
  cnt [32 + 24]uint32
}

func scan(output *stats, vals []uint32) {
  for _, val := range vals {
    for b := range bits.Len32(val) {
      output.cnt[b]++ // b bits are not enough to store val
    }
  }
}

Let’s consider the following 3 example values to understand the resulting cnt:

input input (bin) bits.Len32
23 0b0000010111 5
5 0b0000000101 3
666 0b1010011010 10

The resulting cnt exception count histogram would contain (cnt shortened to c):

c[0] c[1] c[2] c[3] c[4] c[5] c[6] c[7] c[8] c[9] c[10]
3 3 3 2 2 1 1 1 1 1 0

In words, this means that at bit width 10, we could encode all the values without any exceptions.

But most values do not need 10 bits, so a bit width of 5 would be more efficient, but requires storing one exception. Encoding at bit width 4 requires 2 exceptions, and so on.

The scan function above is intentionally kept simple for illustration. We can make it faster by moving the per-bit-width loop outside the per-element loop. The fast version still needs about 12 instructions per value. With SIMD, we can reduce this to by 8x to only 1.5 instructions per value!

The trick: smear masks enable positional popcount

The trick is to turn each input value into its “smear mask” (imagine taking the first 1 bit and smearing it across the remaining positions). Here are the smear masks for our example:

input input (bin) bits.Len32 “smear mask”
23 0b0000010111 5 0b0000011111
5 0b0000000101 3 0b0000000111
666 0b1010011010 10 0b1111111111

Turning a value into its smear mask is computationally cheap: Go implements BitLen(x) (functions like bits.Len32) by calculating 32 - LZCNT(x). We can calculate the “smear mask” of a value with ^uint32(0) >> LZCNT(x), i.e. starting with a 32-one-bits mask and shifting it by the number of leading zeros.

Now, to obtain e.g. cnt[4], we can count the 1 bits at bit position 4 of all input values.

The POPCNT instruction counts bits very efficiently, but it counts one bits within a register, so it counts rows, not columns. Counting columns is called Positional Population Count.

I found the following papers that describe positional popcount with SIMD:

Positional Popcount: a visual explanation

To understand the AVX512 implementation of positional popcount, I found it most helpful to visualize an AVX512 register (512 bits, i.e. 64 bytes). The graphic below uses the Uint64x8 layout, meaning it divides the register into 8 lanes of 64 bits (= 8 bytes) each.

This illustration shows the whole process: how uint32s are loaded into an AVX512 register (all 4 of its bytes, in sequence) and where we end up, i.e. the 32 positional popcounts:

Let’s break down this process into its individual steps.

First, we turn each loaded value into its smear mask as explained above.

The VPOPCNTB vector instruction calculates POPCNT (1 byte) of 64 bytes at once, but first we need to shuffle the bytes inside the register: in load order, we have a full uint32 (4 bytes), followed by another uint32, per lane. First, we permute the bytes (VPERMB) such that all the first bytes of each value end up in one lane (“transpose the bytes”):

Next, we “transpose the bits” using the GF2P8AFFINEQB instruction, which sounds scary but turns out to be quite flexible for bit manipulation of all kinds. The GF2P8AFFINEQB instruction is also “the star of the show” in Go’s Green Tea Garbage Collector (2025). Here is the bit transpose, shown in the AVX512 register layout (see below for a different layout):

I found it easier to understand the transpose step when arranging the 8 bytes of lane 0 from top-to-bottom (instead of left-to-right), because then it looks like a 90 degree clockwise rotation:

Now we can use VPOPCNTB to count the bits in all 64 bytes at once:

After all loop iterations (processing 16 values each) are done, we add the two groups (first 8 values, second 8 values) to obtain the 32 exception counts:

Positional Popcount: Go SIMD

Here is the Go code that implements what I described visually above:

func scanSIMD(output *stats, vals []uint32) {
  ones16 := archsimd.BroadcastUint32x16(^uint32(0)) // 16 32-one-bits masks
  shuffle := archsimd.LoadUint8x64Array(&scanShuffle)
  units := archsimd.LoadUint8x64Array(&scanUnits)
  var acc archsimd.Uint8x64
  idx := 0
  for ; idx+16 <= len(vals); idx += 16 {
    v := archsimd.LoadUint32x16(vals[idx : idx+16])
    // Replace all values with their smear masks.
    smear := ones16.ShiftRight(v.LeadingZeros()).ReshapeToUint8s()
    // Transpose: shuffle the bytes, then transpose the bits.
    matrices := smear.Permute(shuffle).ReshapeToUint64s()
    transposed := units.GaloisFieldAffineTransform(matrices, 0)
    // Popcount 64 bytes at once into the accumulator.
    acc = acc.Add(transposed.OnesCount())
  }
  // Store the accumulator into output.cnt:
  // Widen the two groups of byte counts to uint16 lanes (so that
  // 128+128 = 256 fits), fold them into cnt[b] for b=0..31,
  // then widen again to the uint32 lanes of output.cnt.
  sum := acc.GetLo().ExtendToUint16().Add(acc.GetHi().ExtendToUint16())
  sum.GetLo().ExtendToUint32().Store(output.cnt[0:16])
  sum.GetHi().ExtendToUint32().Store(output.cnt[16:32])
  // scalar tail for the 0..15 remaining values
  for _, val := range vals[idx:] {
    for b := range bits.Len32(val) {
      output.cnt[b]++
    }
  }
}

Have a look at the commit introducing positional popcount to DCS for the full code (including shuffle tables and ISA checks) as well as the detailed benchmark results.

Go even faster?

The SIMD optimizations I showed above beat the cgo TurboPFor library that Debian Code Search used before. When comparing apples to apples, i.e. backporting the AVX512 kernels and positional popcount technique to C TurboPFor, Go benchmarks a little slower at ≈1.4x C.

Could we make my Go TurboPFor implementation even faster, to truly match the C speed?

Yes! But also no. Let me explain:

  1. We could use more SIMD instructions to remove all code that still processes one value at a time. For example, in my encoder’s encodeBitpackVBExc function. Or we could price all bit widths concurrently in encode. Or in the decoder’s exception apply code path.
    But all of these SIMD instructions make understanding (and changing) the code harder, so I am cautious regarding which ones I introduce.

  2. A big part of the performance gap is due to Go’s bounds checks. While it costs performance, bounds checking is great for safety, so I will not turn off bounds checking. The Go compiler eliminates a number of bounds checks when it understands it’s safe to do so. One optimization avenue could be to make the prove pass in the Go compiler smarter to eliminate more bounds checks.

  3. When doing mid-stack inlining (proposal #19348) (2017), Go sometimes needs to put NOP instructions into the binary so that it can attach inlining markers. For dispatch-bound functions, these extra NOPs can measurable slow down execution.

  4. The Go compiler currently allows specifying the architecture (GOARCH=amd64) and microarchitecture (GOAMD64=v3), but not a specific CPU architecture (like AMD Zen 4). Therefore, CPU-specific workarounds for one vendor affect all the generated code. The specific one I encountered in my code is that the Go compiler emits XORL CX,CX before every POPCNT to break a false-output-dependency from the Intel Sandy Bridge Skylake era, which is unnecessary on AMD Zen CPUs.
    I suspect that Go intentionally does not offer this level of customizability.

  5. After all of the above points are addressed, what remains is better code generation in specific cases. To illustrate what I mean, consider the example of incrementing a loop variable, where Go re-derives an index every time:
    Go: POPCNTL; ADDQ DI,CX; LEAQ (base)(CX*4) (3 instructions)
    clang: popcnt; lea rax,[rax+4*rdi] (2 instructions)
    Depending on the specific case, improving the compiler might be easy or prohibitively complex. Often, such improvements are hard to measure conclusively.

Conclusion

Go’s SIMD support makes available — in Go code without having to resort to cgo or assembly — a powerful part of modern CPUs which allows speeding up the kind of computation that TurboPFor needs by an order of magnitude! 😲

I found it very valuable to use a coding agent (Claude Code, with Opus 5 and Fable 5 in this case) to help with the many tedious parts of such performance work (and still it took me weeks!). The LLM can read objdump output much faster than I can, can see patterns and correlations I might never identify, never becomes frustrated after a compiler error or runtime panic, and never runs out of patience to run one more experiment, as long as I give it measurable and reachable goals.

The performance of the SIMD code which one can get from the Go compiler is pretty close to what a good C compiler like clang provides. The CPU performance counters show value decoding speeds of 7 instructions/cycle (IPC) on a machine where the maximum is 8 IPC.

To me, SIMD support is a very welcome addition to Go.

08:42

Urgent: Limit monitoring systems [Richard Stallman's Political Notes]

US citizens: call on your state officials to limit Orwellian monitoring systems in order to protect everyone's freedom.

Here is what I wrote:

I urge you to pass legislation prohibiting governments and agencies in our state from setting up cameras that identify and record individual people or vehicles, except based on a warrant limited this surveillance to specified places and time intervals.

It is not enough to ban contracts with Flock. The issue is not limited to that one company. The issue is the danger of Orwellian surveillance and tracking, and the repression they make possible. As shown by recent deportation practices, we must not allow systems to operate which track the movements of people in general.

When and where recognition cameras are authorized, they should not allow remote access to their records. Rather, someone should have to go to the camera itself to retrieve its list of identifications and date/times. For investigating a serious crime, we can afford that. For our safety, it should not be feasible to get each cameras records for every day, or every month.

Please see https://gnu.org/philosophy/surveillance-vs-democracy.html.

Sincerely,

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Oppose SEC deregulation [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to oppose SEC deregulation.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Direct tariff refunds to consumers [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to direct tariff refunds to consumers who paid them.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Hot summer caused crop failure in Europe [Richard Stallman's Political Notes]

The record hot summer has caused a disastrous crop failure in Europe. Both vegetables and grains are affected; in some crops, the loss is over 50%.

Global heating is accelerating, and I've covered the threat of food shortages for many years. Sooner than you expect, every year will be at least this bad — unless we give the planet roasters a knock-out blow, so they can't stop us from preventing that disastrous future.

Suggestion for negotiating with the Taliban [Richard Stallman's Political Notes]

A suggestion for countries that wish to negotiate with the Taliban: insist on sending a delegation composed of women, and insist that the Taliban do likewise.

Finland halts deportation of Russian family [Richard Stallman's Political Notes]

*Finland halts deportation of Russian family who "risked everything" to oppose Ukraine invasion.*

What shocks me is that Finnish intelligence acted unaware of the reasons why the Belovs deserved protection. If it was truly ignorant, it was incompetent. If it disregarded the reasons, it was vicious. Which one was it?

Increase in traffic fatalities on music release dates [Richard Stallman's Political Notes]

* Harvard study finds traffic fatalities increase by 15% on release dates [of major music albums] compared with similar days either side.*

3M firefighting products [Richard Stallman's Political Notes]

*3M knew for more than 50 years that its [firefighting] products could harm humans, Australian government alleges in court documents.*

These products are made with PFAS.

Wrecker distancing US from South Korea [Richard Stallman's Political Notes]

The wrecker has decided to distance the US from South Korea and cozy up to Dictator Kim in North Korea.

This is disappointing, of course, but not surprising. He often prefers dictators to democracies. He has tried to cozy up to Putin, Chairman Xi, Orbán, Crown Prince Bone Saw, and Modi.

Mississippi ICE facility gas leaks [Richard Stallman's Political Notes]

Prisoners in an overcrowded deportation prison (privately run by CoreCivic) in Mississippi (far south in the US) were forced sometimes to spend hours outdoors in bright sun, and at other times, to swelter in crowded cells, because of a power outage.

The prison kommandant held the prisoners incommunicado during that period.

Privatized prisons are a motor for inaccountability, and therefore for cruelty and gratuitious suffering; this makes them inherently unjust. We should abolish them all.

Deform UK [Richard Stallman's Political Notes]

The Deform UK party's latest deformation plan would throw hundreds of thousands of children into destitution, and many disabled people too.

USS Abraham Lincoln shortage [Richard Stallman's Political Notes]

Sailors on the USS Abraham Lincoln say there is a grave shortage of food; some have lost 30 pounds. The water is not fit to drink.

Relatives excoriate the bullshitter for denying these facts, but that's simply being himself.

Carlitos Ricardo Parias [Richard Stallman's Political Notes]

The case of journalist Carlitos Ricardo Parias: *A journalist was injured while documenting government power, shot during an attempt to arrest him, prosecuted, repeatedly denied medical care — and then left in immigration detention, where he has continued documenting the conditions around him.*

Trump's tax cuts [Richard Stallman's Political Notes]

The wrecker's tax big cuts were not just for billionaires. They also gave a handout to multimillionaires. As usual, at the expense of everyone else in the US.

Afghan women deportation [Richard Stallman's Political Notes]

Other countries have deported millions of Afghans to Afghanistan. For Afghan women, that means deportation to slavery.

To deport someone to a place where she will be tortured violates the treaties which establish the right to asylum. Yet the EU is now trying to negotiate a deal with the Taliban for returning refugees there.

Tories to shut down soup kitchens [Richard Stallman's Political Notes]

A Tory-run local council in London wants to shut down soup kitchens because the area around "is not safe".

Sunrise Movement [Richard Stallman's Political Notes]

Government secret agents have been persistently investigating the Sunrise Movement. They keep getting more and more evidence that it is committed to nonviolence (including nonviolent civil disobedience), but continue searching desperately for some violence in it somewhere.

Trump's attack againts Iran [Richard Stallman's Political Notes]

The bully's attack against Iran was a moral error and a grand-strategic mistake. In addition, it was a strategic military mistake which has caused the US to lose much of its power in the world.

Is that a bad thing? No, and yes. The bully has been using US power for evil purposes; now he has less capacity to do that. However, the countries that have gained power though that mistake have been even more vicious, for decades.

Ex Myanmar ambassador in Britain [Richard Stallman's Political Notes]

The ambassador to Britain appointed by President Aung San Suu Kyi defied the military government's order to vacate the ambassadorial house in London. The UK is denying the moral doubts about the situation by prosecuting him for "trespassing" in a diplomatic residence.

Israeli besieged Palestinian families [Richard Stallman's Political Notes]

* Israeli militants have besieged two Palestinian families in their homes since the weekend, aiming to take over their properties in the West Bank village of Qusra through a campaign of terror.*

Eissa Hashemi and Maryam Tahmasebi [Richard Stallman's Political Notes]

Eissa Hashemi and Maryam Tahmasebi, a married couple of professors who were permanent residents in the US, are in deportation prison because Hashemi's mother, Masoumeh Ebtekar, is a supporter of the regime and participated in the occupation of the US embassy.

It is not impossible that they are somehow working with Ebtekar on a nefarious plot, but the persecutor's henchmen have not made such charges -- they have simply cancelled the family's residence permits arbitrarily and (according to Tahmasebi) aim to keep them in deportation prison for life.

08:07

Never Split the Difference [Judith Proctor's Journal]

Never Split the Difference: Negotiating As If Your Life Depended On ItNever Split the Difference: Negotiating As If Your Life Depended On It by Chris Voss

My rating: 4 of 5 stars


This was an interesting read. Voss has a lot of experience in negotiating for hostage releases. And he's very successful at it.
But he didn't start out that way - he learnt a lot from people on the way. For instance, a spell working on the Samaritan's help-line hammered home the rule that you can't tell people how to sort out their lives - you have to help them find solutions themselves.
This applies across many fields - Rather than telling the hostage taker how much you are willing to pay, explain your situation: eg. I can only raise that much money by selling my house. But that would mean waiting until it sells. What should I do?
Now your problem becomes their problem to solve - which may result in them accepting a much lower sum of money if they can have it now, rather than in six months.




View all my reviews

comment count unavailable comments

01:42

Link [Scripting News]

The governor of Kentucky should appoint a successor to McConnell unless he can prove he’s actually alive.

Link [Scripting News]

I had watched almost three seasons of Silo, had no feeling for the characters, or the story, which was going nowhere that I could discern. I shouldn't have watched it through all that time, but for some reason I did. Then in episode 9, two Fridays ago, the story took a remarkable turn. It's so good it actually makes this dark and confusing show worth watching. If you haven't been watching, imho you can start in season 3, and maybe read some recaps. You want enough of the boredom so you can revel in the brilliance of this season's end. And there is another season coming, the last one, and this time it's interesting.

Saturday, 05 September

22:21

A Boy and his Dog at the End of the World [Judith Proctor's Journal]

A Boy and His Dog at the End of the WorldA Boy and His Dog at the End of the World by C.A. Fletcher

My rating: 4 of 5 stars


This is basically the story of a boy in a lonely future world where there are very few people left.
Small isolated groups live on islands like Mingalay, and a few other places.

It was recommended to me by my Granddaughter (age 12)

When his dog is stolen (dogs are scarce, and fertile bitches are even rarer), Griz rushes off headlong to try and recover her.

It's not a happy book - it's dystopian post-catastrophe - but there are a few interesting people still out there.

I won't say how it ends - that's for each reader to discover for themselves.

It's a good book and very well written. I only really have two gripes.

Gris gains possession of a map showing roads and cities on the mainland, but for some reason the author never tells the reader the names of places.
eg. Griz travels though a deserted, ruined Blackpool- easily identified by the tower over the ballroom, and a roller coaster.
Easily identifiable to me - I watch 'Strictly'. My granddaughter had no idea where he was.

Griz's family took a lot of books from an old library. But the books he is reading are old children's classics like The Hobbit, 'The Wind in the Willows', and other stuff that I know from my youth, like 'A Canticle for Leibowitz' (published in 1959). (You can identify them from context, but they aren't always named)

'A Boy and a Dog at the End of the World' was published in 2019. These are the classics of my childhood, Griz is a kid raiding a library when I'm going to be at least 70...

Most YA books are read by adults, but surely the book should reference some books that are popular with kids now?



View all my reviews

comment count unavailable comments

Citizen of the Galaxy - Robert Heinlein [Judith Proctor's Journal]

Citizen of the GalaxyCitizen of the Galaxy by Robert A. Heinlein

My rating: 5 of 5 stars


This has been a favourite of mine for a very long time.
As a teenager, I didn't care for the downbeat ending.
As an adult, I think it's exactly right.

The whole theme of this novel is what freedom means to you, and what you are prepared to do to help others be free.

Thorby is a slave. As far back as he can remember, he has been a slave.

When he is bought by an old beggar - a man who made his own decision about freedom - his life changes in ways that he could never have expected. He comes to experience different forms of freedom and to understand each of them in it's own way.

Finally, he has to decide what he is willing to sacrifice personally in order to help find freedom for people he has never met, and will probably never meet in person.

I have read this book many times. I hope to read it again some day.



View all my reviews

comment count unavailable comments

20:28

Joe Marshall: Githack: A Persistent Object Store for Lisp Based on Git [Planet Lisp]

Git has a built-in persistent store for objects based on Merkle trees. It is tailored to store files and directories, but these are just specializations of trees of blobs. There is no reason it couldn't be used to store Lisp objects.

Githack is a Lisp object store that uses Git as its backend. It is a simple library that provides persistent objects for Lisp and a transactional interface for manipulating them. Simple atomic objects are stored as blobs and composite objects are stored as trees. Standard composite Lisp objects, such as lists, vectors, and hash tables, are supported. Custom composite objects can be created through DEFINE-PERSISTENT-STRUCT or DEFCLASS with a :STANDARD-PERSISTENT-METACLASS.

WITH-REPOSITORY is used to specify which repository to use for storing objects. WITH-TRANSACTION sets up a transaction for manipulating objects and retrieves the root object. You use standard slot accessors to walk the object tree. When you are done, you commit the transaction, and modifications are atomically written to the repository with a new root object being placed in a Git branch.

By placing the database in an orphan Git branch, you can store it right beside your source code without tangling the histories. You can use Git to manage the history of the database, branch it, and share it with others. Githack even stores object docstrings as README.md files inside the repository trees, so the stored objects are natively self-documenting in the Git web UI.

Githack comes with example code and an example database living on its own orphan branch, so if you clone the repository, you'll clone the working example database as well.

Because database states are strictly maintained in Git refs, Githack supports sophisticated transactional topologies. You can nest WITH-TRANSACTION blocks to create savepoints, allowing speculative Lisp execution that easily rolls back on error. Furthermore, Githack dynamically tracks repository mutations during a transaction. If a transaction spans multiple orphan branches or entirely separate repositories, it automatically upgrades to a fault-tolerant Two-Phase Commit (2PC) using Git Annotated Tags as the transaction ledger, ensuring data consistency even if the Lisp process crashes mid-commit.

19:49

Emmanuel Kasper: Isolated VSCode/VSCodium development environment in a Virtual Machine [Planet Debian]

Following the previous steps, we are now interested in getting a graphical environment with a VSCodium, the opensource rebuild of the VSCode IDE.

Configuring the display and development environment

From the previous steps we had a virtual machine where we can login with a debian user, and we can start configuring a graphical desktop environment.

  • Install Gnome Flashback.

Gnome Flashback is a 2D version of the Gnome Desktop, it has a kind of year 2009 feeling but works well enough. We need a 2D desktop, as the Virtio display adapter does not work consistently with 3D enabled.

# inside dev-vm
# apt install task-gnome-flashback-desktop

  • From the host connect to the VM display using a remote client:
$ virt-viewer dev-vm

or using the Remote Viewer app:

$ remote-viewer spice://localhost:5900

  • Install the Spice Agent package. The Spice Agent provides a shared clipboard between host and VM, and also adapts automatically the VM display and desktop when the window of the Spice client is resized.
# inside dev-vm
# apt install spice-vdagent

  • Add a VSCodium repo, via extrepo and enable it:
# inside dev-vm
# apt install extrepo
# extrepo enable vscodium
# apt update && apt install codium

  • Ensure the VM starts automatically on boot.
$ virsh autostart dev-vm

It also makes sense to set our debian user to autologin in Gnome Fallback, and start Codium on session start.

This is how the environement should look like at this point: Remote Viewer

Sharing source code from host to guest VM

Finally we need to make sure we have access in the dev-vm to our source code repositories. For this I will share the directory /home/manu/Projects/git which is containing all my git projects on the host, to the dev-vm using virtiofs.

The configuration of virtiofs is fortunately possible using virt-manager, which will save us some tedious XML editing. virt-manager screenshot

Finally we mount the shared directory, and enable the mount on each boot.

# inside dev-vm
# mount -t virtiofs /home/manu/Projects/git /home/manu/Projects/git
#  echo '/home/manu/Projects/git /home/manu/Projects/git virtiofs defaults 0 0' >> /etc/fstab

So now we have an isolated dev environment where we can run untrusted code, with a very strong isolation from our host.

Michael Ablassmeier: virtnbdbackup - backup target plugins [Planet Debian]

I’ve released a new version of virtnbdbackup. The new version adds a small plugin system layer that allows users to extend the backup targets by creating plugins.

Past feature requests asked for backup to S3 or adding encryption features, which i dont need and do not want to maintain within the project scope. Users can now extend the utility with plugins.

In the course of implementing this, i had the idea: why not create a plugin thats capable of streaming the backups to a proxmox backup server?

This resulted in pypbs, a small python binding for libproxmox-backup-qemu0 that allows to store fixed index images on PBS using python.

A first POC implementation of the plugin worked quite well, even tho i don’t know if its worth releasing. A better approach would be to use PBS dynamic index format, but then i might just add a small plugin that wraps the proxmox-backup-client CLI for doing this..

18:14

Dirk Eddelbuettel: rfoaas 2.4.0 at CRAN: Fully Restored Functionality [Planet Debian]

rfoaas greed example

FOASS is back at a new site / url since late August! It restores original FOAAS functionality and full set of REST access points including the language filters.

So this new rfoaas release restores all accessor functions re-enabling full R access, documents, and tests them. We re-enabled code coverage too. This corresponds to the upstream version 2.4.0 in the forked FOASS repo, and by our convention we use the same version number for the R package.

My CRANberries service provides a comparison to the previous release. Questions, comments etc should go to the GitHub issue tracker. More background information is on the project page as well as on the github repo

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can sponsor me at GitHub.

15:56

The social web vs the software web [Scripting News]

I launched ChatGPT for the first time in a long time because I now have access to the new version. One new thing I see is that they are popping up suggestions about things you can do in realtime, like smart queries in a feed reader, which as far as I know is still uncharted territory. Like this: "let me know when the Mets have a three-game winning streak."

That's the kind of thing TBL dreamed about in his semantic web plan, but he wrote the spec before writing software, and that doesn't work. His invention, the web, which came before was a perfectly timed, perfectly simple approach to sharing ideas on the internet. It worked because he created a product that worked, and made every piece completely replaceable. It came at a time when stagnation in tech made something necessary, or the whole thing was going to fall apart, and so close to so much incredible potential. Just like the moment we're in now with AI.

Rules for Standards Makers says a lot about this.

The value of disruption is that it undoes stagnation.

Anyone who has the same software plan today that they had a year ago is going to find themselves isolated. Even WordPress, though it is open source, is a silo that imho in the future will not work. The main selling point of AT Proto is that it is a safe haven for developers who nuzzle up, another word for that is silo.

The siloers are probably scrambling to find a way for the AI to run inside their world, mistake -- instead they should be trying to be fully relevant to applications in the AI world. Like Slack, for example, and Claude Tag. It's now built in. Has to be replaceable too. Small pieces loosely joined. That's what you have to be imho to have a role in the future of tech.

If your advantage is your silo, undo that as quickly as possible. Become the default version of whatever it was meant to be wrt the web. Both Bluesky and WordPress find themselves in the same quagmire. Probably a lot of others do, but this is the area I study. (I just am very familiar with the products of both companies, for some reason.)

This is the intuition we all had, coming to fruition. Evolution is something we haven't seen much of in the software world since 2006 or so. Incredible stagnation because of the dominance of the social web. We lost the software web. All the focus was on personalities, celebrity -- hype and 99.999% bullshit. It was a 20 year period where all that happened was scaling.

Lots more to say about this. The world we lived in ran on hype, now guess what -- having lived through a few big evolutionary changes in tech is a huge advantage vs the people who have never been part of one, and I think most of the people in tech today qualify.

10:42

Homo Habilis [Seth's Blog]

We called them this because they used tools. The first proto-humans to clearly do so.

The question that we need ask ourselves today, “Are you a tool user or a tool maker?”

Everyone uses tools. But only a few people, even with access to AI and systems of leverage, choose to make tools.

Part of the gap is failing to ask the question. It generally doesn’t occur to a productive tool user to decide to slow down, risk failure, and take responsibility by making a new tool.

But new tools are one way we change things for the better.

Homo Faciens

09:35

Pluralistic: Google skates (05 Sep 2026) [Pluralistic: Daily links from Cory Doctorow]

->->->->->->->->->->->->->->->->->->->->->->->->->->->->-> Top Sources: None -->

Today's links

  • Google skates: Another federal judge loses a game of peek-a-boo.
  • Hey look at this: Delights to delectate.
  • Object permanence: Memory-hacking ads; "Dzur"; NZ Ministry of DRM; Canadians v looking at the internet; Advice for self-publishers; Advice for writers; Why Wikipedia works.
  • Upcoming appearances: Dąbrowa Górnicza, Warsaw, Brighton, London, Budapest, Edmonton, South Bend, Hudson, Calgary, Winnipeg, Vancouver, Victoria, Ottawa.
  • Recent appearances: Where I've been.
  • Latest books: You keep readin' em, I'll keep writin' 'em.
  • Upcoming books: Like I said, I'll keep writin' 'em.
  • Colophon: All the rest.



A woodcut engraving of a courtroom scene in which the divorcee has thrown herself into the arms of one of the lawyers and they are about to kiss. The image has been altered: the heads of the judge, the court clerk and both lawyers have been replaced by green Android 'droid' mascots in judicial powdered wigs. The judge's bench sports a pixelated, 1998-era 'I'm feeling lucky' Google button.

Google skates (permalink)

Rome wasn't overthrown in a day. Oligarchies are stubborn, and by the time they've established and entrenched themselves, they have resources, power blocs and even mercenaries they can deploy to repel would-be dethroners.

The USA got its first antimonopoly law, the Sherman Act, in 1890, but it took 22 years before that law could be used to crush John D Rockefeller's corrupt, sprawling empire. Senator John Sherman promoted his law as a way of preventing monopolies from emerging, warning the Senate that they would struggle to overthrow the "autocrats of trade" that monopolies created:

https://pluralistic.net/2022/02/20/we-should-not-endure-a-king/

The Senate passed his law and Harrison signed it, but then successive administrations left the Sherman Act to gather dust on a shelf as Rockefeller went on a spree, accumulating the kind of power that made him a true "autocrat of trade," so powerful that he and the US government were practically evenly matched.

Allowing Rockefeller to create, expand and consolidate his monopoly power was a terrible tactical blunder, giving him decades in which he was able to loot America and its trading partners, pauperizing ordinary people and smashing anyone who got in his way. No one was willing to admit that Rockefeller was a threat to democracy and prosperity until he had amassed all that power, and once he had all that power, it took heroic effort to break him.

The Rockefeller story is like the punchline of that joke: "When it doesn't rain, the roof don't leak; and when it's raining, it's too wet to fix it." Alternatively, there's my other favorite punchline: "If you wanted to get there, I wouldn't start from here."

The Rockefeller blunder wasn't a one-off. The Apple ][+ hit the shelves the same year Reagan hit the campaign trail, and the tech industry's rise occurred simultaneously with the dismantling of competition law enforcement. Tech companies were the first "post-antitrust" industry, and it wasn't until these companies became palpable, terrifying, undeniable, existential civilizational risks that we remembered that we had all these laws on the books that were designed to curb excessive corporate power.

Under Biden, a group of generationally talented, visionary trustbusters were given access to those dormant enforcement powers: Lina Khan at the FTC; Rohit Chopra at the CFPB, Tim Wu in the White House, and Jonathan Kantor at the DOJ Antitrust Division. Together with a staff of canny and skilled lawyers and economists, these people scored incredible victories against Big Tech. During the Biden years, Google lost three federal antitrust cases. Three!

But if we wanted to get there, we wouldn't start from here. After a series of stinging defeats in the US and abroad, after watching the EU, the UK, Japan, Singapore and South Korea make common cause with Biden's enforcers, Big Tech threw everything it had into Trump, who promised them a system of regulatory forbearance in exchange for low-cost bribery, backstopped by a xenophobic, belligerent geopolitics that would rain down punishments on any country that dared to regulate or tax Big Tech:

https://www.bbc.com/news/articles/c62553ywn77o

And then the other shoe dropped: the federal judges who convicted Google of operating an illegal monopoly handed down their "remedy" decisions. In an antitrust case, the "remedy" phase is like sentencing – the stage of the legal proceeding where the judge decides what punishment the company should face for breaking the law.

The first of these remedies came out a year ago, in September 2025. Judge Amit Mehta had presided over Google's "search" case, where we learned that Google had deliberately made search worse so that you'd have to search more than once in order to get your answer, which would allow the company the chance to show you more ads:

https://pluralistic.net/2024/04/24/naming-names/#prabhakar-raghavan

Google was able to do this because it had cornered the market on search. The company had spent years paying a $20b annual bribe to Apple to stay out of the search market, and they'd bought the default search placement for every operating system, browser and carrier. If you encountered a search box in the wild, it was almost certainly wired into Google's servers. They knew that there was approximately zero chance that you'd ever stumble upon another search engine, which meant they could make their own search as shitty as they wanted and keep your business.

Confronted with these proven findings, Mehta decided that Google's punishment should be…nothing. They wouldn't be forced to delete the personal data they'd taken from billions of people. They wouldn't be forced to spin off Chrome or Android – two of the key tools Google uses to keep people from discovering other search engines. They wouldn't even be forced to halt the $20b annual bribe to Apple – the judge fretted that without that $20b annual bribe, Apple wouldn't be able to pay researchers to come up with cool new iPhone features (never mind that Apple spends all that money – and more – on stock buybacks, a recently illegal form of stock manipulation):

https://pluralistic.net/2025/09/03/unpunishing-process/#fucking-shit-goddammit-fuck

Then, a year later, another federal judge – Leonie Brinkema, who presided over the Google "ad-tech" case – decided that Google's penalty for monopolizing the ad market should also be…nothing:

https://arstechnica.com/gadgets/2026/09/us-court-rules-google-will-not-have-to-sell-ad-exchange-after-losing-antitrust-case/

Oh, maybe not exactly nothing. We don't actually know the full extent of Judge Brinkema's "remedy," because it's sealed for two weeks. What we do know is that Google will not be forced to take the most obvious, effective and necessary step to prevent it from abusing its monopoly: Google will not be forced to sell off part of its ad-tech stack.

Let me unpack that for you, because unless you're an ad-tech weirdo, chances are you have no idea how any of this works and don't think it affects you. But the reality is that this is costing you money. It's one of the dirtiest, most profitable scams in the entire tech economy, which is saying something, because that is an economy that is made of scams:

https://pluralistic.net/2026/09/04/cheating-at-fraud/#absentee-rentier

Unlike older ads, which were targeted based on content (say, an ad for a hotel might run next to a newspaper article about a beachside town) modern ads are built on surveillance. Companies like Google amass vast, nonconsensual dossiers on the personal characteristics and behavior of billions of people, supplemented with information purchased from the unregulated data-broker sector.

When you visit a website, the site fires off a piece of software called "sell-side agent" to message a server called an "ad exchange" in order to announce your visit, soliciting bids for the right to show you an ad: "I am about to serve a web-page to a 18-34 year old man-child from New York's outer boroughs, who owns an Xbox and has been recently searching for information about gonorrhea: who wants to cram some ads into this guy's eyeballs?"

That ad-exchange server is haunted by "demand-side agents" – these are pieces of software fired off by advertisers that monitor all these advertising opportunities announced on the ad exchange and bid for the right to show you an ad. The highest bidder gets to show you an ad, and the fee is remitted to the exchange, which takes a cut and passes the remainder on to the sell-side platform, which also takes a cut and gives the balance to the website publisher.

So the ad-tech stack has three main components: the "sell-side platform" (SSP), which lets web publishers announce auctions for the right to advertise to their visitors; the "demand-side platform" (DSP) that lets advertisers bid to show those visitors ads; and the "ad exchange" – the marketplace where the sell-side and demand-side agents meet to collect bids, finalize the sale, and exchange ads for money.

When this all started, there were lots of companies in all three roles. Publishers and advertisers had their choice of SSPs, DSPs and exchanges, and all three types of middleman competed to offer the best deals to advertisers and publishers.

Then, Google and Facebook started buying up the leading DSPs, SSPs and exchanges. They used contracts and technical countermeasures to force anyone who used any part of their "stack" to use them for all parts of the transaction. The CEOs of Google and Facebook personally colluded to rig the market, dividing it up between them so that publishers would get less, advertisers would pay more, and Googbook would pocket the difference. The codename for this conspiracy was "Jedi Blue":

https://en.wikipedia.org/wiki/Jedi_Blue

Jedi Blue was just the icing on the cake. The reality is they didn't need the conspiracy: once Google was selling services to advertisers and publishers on an exchange that Google owned, they created an entire universe of ways to rip off both advertisers and publishers. Now consider that Google is also an advertiser and also a web publisher, and the opportunities to cheat are just wild.

The numbers tell the story. Before Googbook captured 80% of the display advertising business, the total share of the advertising industry's revenues that went to "intermediaries" (middlemen like ad agencies, ad buyers, etc) was about 15%. Today, that number is 51%. Hundreds of billions of dollars have been moved out of publishers' and advertisers' bank accounts and onto Google and Facebook's balance sheets.

This isn't hard to understand. Google runs an ad business that locks in buyers and sellers on a marketplace Google owns and controls, where it also competes with those buyers and sellers. Buying or selling an ad through Google is like going to court to get a divorce, only to discover that you and your soon-to-be-ex- are both represented by the same lawyer, who promptly ascends the bench and dons a judge's wig, and then spends the whole trial trying to match with both of you on Tinder, and who concludes the trial by banging their gavel and announcing that they've decided that the family house will be awarded to…the judge!

The most absurd part of this whole farce is the lawyers who defend it on behalf of companies like Google. If a Google lawyer ever showed up to defend the company in a trial where the judge was working for the plaintiff, they would scream blue murder and refuse to proceed until the judge was removed from the case. But when Google operates a business where it presides over transactions where it has nothing but conflicts of interest, these same lawyers argue that Google would never cheat a seller or a buyer.

The fucking absurdity of this arrangement is so obvious that a bill to force a halt to it was co-sponsored…by Elizabeth Warren and Ted Cruz:

https://gizmodo.com/google-facebook-america-act-ads-break-up-cruz-warren-1850287725

Why would Warren and Cruz care about this? Because the hundreds of billions that have been moved from publishers and advertisers to Google and Facebook are hundreds of billions of dollars that are no longer paying for news and entertainment production, and they're hundreds of billions of dollars that businesses have to recoup by raising prices on you to pay their advertising bills.

Google (and, apparently, Judge Leonie Brinkema) dispute this. They say that "larger forces" have "changed the dynamic" that "restructured the industry." But, I mean, come on! This is a situation where hundreds of billions of dollars are divided up by a thrice convicted monopolist who is mysteriously hundreds of billions of dollars richer, while the other parties to the transaction are mysteriously hundreds of billions of dollars poorer. Anyone who can't draw the obvious causal inference from these facts has so little object permanence that they would lose a fucking game of peek-a-boo.

This is so goddamned demoralizing. For a couple years there, it really looked like the tide was turning. Then Judges Brinkema and Mehta came along to snatch defeat from the jaws of victory.

The only thing that's keeping me going is object permanence. I know my history. I know it took decades from the passage of the Sherman Act until the defeat of John D Rockefeller. Our forebears brought down Rockefeller because they didn't give up, despite setbacks as bad as this one, and worse. Stein's Law of finance holds that "anything that can't go on forever eventually stops," and MLK told us that "the arc of the moral universe is long, but it bends toward justice." This can't go on forever, and despite Dr King's phrasing, I know he understood that the arc doesn't just "bend" – it is bent – by people like us, hauling on it with all our might.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#25yrsago Advertisers claim they can hack your childhood memories https://web.archive.org/web/20010921022846/http://news.independent.co.uk/uk/science/story.jsp?story=92386

#25yrsago Wind-up cellphone charger https://web.archive.org/web/20011031122531/http://www.thetimes.co.uk/article/0,,2-2001310179,00.html

#20yrsago Steven Brust’s Dzur: witty and exciting heroic fantasy https://memex.craphound.com/2006/09/05/steven-brusts-dzur-witty-and-exciting-heroic-fantasy/

#20yrsago America to US gov’t: kill the Broadcast Treaty! http://www.cptech.org/ip/wipo/bt/jointletter5sep06usptoforum.pdf

#20yrsago New Zealand wants a Ministry of DRM https://web.archive.org/web/20070108042834/http://www.zdnet.com.au/news/software/soa/NZ_draws_line_on_DRM_and_trusted_computing/0,130061733,339270846,00.htm

#20yrsago Is it legal to look at the Web in Canada? https://web.archive.org/web/20061010120919/http://www.michaelgeist.ca/content/view/1411/135/

#15yrsago Advice for self-publishers: why should anyone care about your book? https://locusmag.com/feature/cory-doctorow-why-should-anyone-care/

#5yrsago A letter to a discouraged young writer https://pluralistic.net/2021/09/05/why-bother/

#1yrago Why Wikipedia works https://pluralistic.net/2025/09/05/be-the-first-person/#to-not-do-something-that-no-one-else-has-ever-thought-of-not-doing-before


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "The Post-American Internet," a geopolitical sequel of sorts to Enshittification, Farrar, Straus and Giroux, 2027
  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, April 20, 2027

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2027

  • "The Memex Method," Farrar, Straus, Giroux, 2027



Colophon (permalink)

Today's top sources:

Currently writing:

  • “Once Is Enemy Action,” a science fiction novel about the origins of modern technofascism. Saturday's words: 504 (12397 total).

  • "The Post-American Internet," a sequel to "Enshittification," about the better world the rest of us get to have now that Trump has torched America. Fourth draft completed. Submitted to editor.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Bluesky (no ads, possible tracking and data-collection):

https://bsky.app/profile/doctorow.pluralistic.net

Medium (no ads, paywalled):

https://doctorow.medium.com/

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

08:56

Junichi Uekawa: Summer Vacation for my kids is over. [Planet Debian]

Summer Vacation for my kids is over. And Peace is back to my life. AI is transforming how I operate and view things. It was very different a few months back. AI (as a product) is useful in generating code, useful in analysing things. It seems to be able to retrieve and show me information relatively quickly, doesn't need me to scan the search results to find which one is more useful. I feel I am less reliable than an AI, even when AI is prone to failure. The text generated by AI is better worded than me myself, albeit they have their own tone. Is it still fun if all my hobby programming is overtaken by AI? I am not sure, did I enjoy writing the fixtures and build environment for the open source programming stuff? Do I enjoy reviewing other people's code? Reviewing other people's contributions is usually not great, because by definition the code you own you have better knowledge about, and the code you generate yourself is the best code, others will not fit naturally, they don't have the historical context, and the undocumented future plans.

05:49

What happens if you change a window class’s GCL_CB­WND­EXTRA? [The Old New Thing]

After my historical look back on the evolution of system-windows window and class extra bytes, I noted that there was one application that expected to be able to modify GWW_CB­CLS­EXTRA.

It turns out that there are even more applications that expect to be able to modify GWW_CB­WND­EXTRA. So many that it wasn’t worth creating an application compatibility exception for them.

So what happens when you modify GWW_CB­WND­EXTRA, or its modern equivalent, GWL_CB­WND­EXTRA?

The change in window extra bytes takes effect, but not retroactively.

Windows that are created after you change CB­WND­EXTRA receive the updated number of extra bytes, but windows that already exist are not modified. They still have the number of extra bytes that were assigned when the window was created.

Specifically to deal with people who change the number of window extra bytes on the fly, the system keeps track of what the number of extra bytes was at the time the window was created, and those are the bytes you get to access from that window. If you try to access the nonexistent bytes, you are told ERROR_INVALID_INDEX.

This does mean that you can get into a strange situation where Get­Class­Long(hwnd, GCL_CB­WND­EXTRA) tells you that you have 8 extra bytes, say, but if you use Get­Window­Long(hWnd, 0), which asks for the LONG represented by bytes 0–3, you are told “Sorry, that’s out of range.” As far as you can tell, it is well within range. What you don’t know is that the window was created back when the GCL_CB­WND­EXTRA was less than 4.

There is no way to ask a window, “How many extra bytes do you really have?” I mean, why the system go out its your way to improve the lives of people who are abusing it?

The post What happens if you change a window class’s <CODE>GCL_<WBR>CB­WND­EXTRA</CODE>? appeared first on The Old New Thing.

00:21

Urgent: Fund and rebuild the FDA [Richard Stallman's Political Notes]

US citizens: call on Congress to fund and rebuild the FDA.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Impeach budget director Russell Vought [Richard Stallman's Political Notes]

US citizens: call your congresscritter to vote to impeach budget director Russell Vought.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Vote against Heidi Overton for head of FDA [Richard Stallman's Political Notes]

US citizens: call on your congresscritter to vote against appointing Heidi Overton to head the FDA.

Let's toss her out of the Overton window!

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: If Superman lived among us now [Richard Stallman's Political Notes]

If Superman lived among us now, and decided it was his duty to defend Truth, Justice and the American Way from America's worst enemy, what sort of actions might be effective and inspirational to use? How about posting your ideas, or stories based on them.

Please don't write about mere violence against magats; that is too crude and obvious, it is too much like them to set an example of good, and would not demonstrate morally that their actions are illegitimate.

Here are a few ideas that occur to me for what he could do.

  • Use his x-ray vision to detect magat conspiracies to trash human rights, and reveal the names and words of the conspirators.
  • On Election Day, collect the valid postal ballots that the USPS is planning to discard, and bring them to the proper local electoral authorities. His super speed could enable him to do the whole job.
  • Collect evidence about the crimes and lies of particular important henchmen, then deliver them, plus the evidence against them, to law enforcement officials who would hold them and begin prosecution. (Hard part: finding some of those.)
  • Capture various crucial henchmen a week or two before the next election, holding them perhaps in the Fortress of Solitude, so that the election can take place without their interference. (I hope the Fortress of Solitude is not in Greenland!)

If you post a fictional story about such an action, either something from the list above or something you have thought up, or even just an outline, I hope you will email me its URL.

Urgent: Stand up to big tech on data centers [Richard Stallman's Political Notes]

US citizens: call on state officials to stand up to Big Tech: pass a data center moratorium.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Reject perverse deal for veterans' medical treatment [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to reject Republicans' perverse deal for veterans' medical treatment.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Refuse to hand over voter data [Richard Stallman's Political Notes]

US citizens: call on your state's Secretary of State to refuse to hand over the voter data that magats demand.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Rescind plan to block election ballots [Richard Stallman's Political Notes]

US citizens: call on the postal governors to rescind the plan to block election ballots.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Impeach and remove RFK jr. [Richard Stallman's Political Notes]

US citizens: call your congresscritter and senators to impeach and remove RFK jr.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Urgent: Investigate local use of Flock cameras [Richard Stallman's Political Notes]

US citizens: call on Tell your attorney general to investigate your local law enforcement agency’s abuse of Flock surveillance cameras, and prevent tracking anyone without a specific warrant.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

Urgent: Reject corrupter's handouts to billionaire barons [Richard Stallman's Political Notes]

US citizens: call on your congresscritter and senators to reject the corrupter's pet projects and handouts to his billionaire barons, and put struggling American families before them.

See the instructions for how to sign this letter campaign without running any nonfree JavaScript code--not trivial, but not hard.

US citizens: Join with this campaign to address this issue.

To phone your congresscritter about this, the main switchboard is +1-202-224-3121.

Please spread the word.

Polish general imprisoned with German SS general [Richard Stallman's Political Notes]

The Russian conquerors of Poland thought it amusing to imprison Polish general Kazimierz Moczarski of the Home Army in the same cell with the German SS general that commanded the destruction of the Warsaw Ghetto.

Moczarski treated this as an opportunity for an interview, a chance to elucidate the mentality of a Nazi mass murderer. However, the Russians censored the contents of his book. When the English translation is published, I plan to buy a copy — paying cash, anonymously. Evidently, not from Amazon.

The purpose of the Polish Home Army was to rise up to help liberate Poland from German occupation. In late 1944, with the Red Army across the river from Warsaw, the Home Army rose up, and asked Russia to join in the battle. Instead, the Red Army held back so the Germans and Poles would kill each other. Stalin considered both of them his enemies.

Cyclosporidia outbreak should have been caught [Richard Stallman's Political Notes]

The cyclosporidia outbreak should have been caught and stopped by a new scheme for tracking wholesale shipping of food, to stop diseases carried by food. But the scheme has never been implemented.

The legal requirement for the scheme was adopted in 2011, but lobbying from agribusiness has repeatedly postponed its implementation. The last postponement put it off until 2028.

Losing data stored in a cloudy place [Richard Stallman's Political Notes]

Watch out! Storing data you care about in a cloudy place could lead to losing it.

This is a two-level example. Nine PBS, a US TV station, stored its archives in space it rented from a cloudy company which subcontracted the actual storage to a cloudy data center. The first cloudy company shut down and disappeared, but the station did not find out until later. Now the second cloudy company denies that the station is its customer, and the station has had to sue.

My lesson from this is not "the cloud is bad" but rather that it is foolish to use the term "cloud" in your words or your thoughts. That term was coined for leading customers into careless thinking. Don't let it lead you anywhere!

Pernicious error of measuring wealth [Richard Stallman's Political Notes]

The US media frequently fall into the pernicious error of measuring the wealth of "Americans in general" by the arithmetic mean of individual wealth.

I expect that pro-billionaire campaign and pressure groups lobby for that.

Better lung capacity after reducing air pollution [Richard Stallman's Political Notes]

Children in London have developed better lung capacity after the ultra-low emission zone reduced the amount of air pollution in most of London.

I support limiting cars' emissions more strictly, but it is Orwellian and unjust to implement this by tracking the movements and locations of individual cars. It would be better to impose this limit throughout the UK.

Sanctions on president of International Criminal Court [Richard Stallman's Political Notes]

The persecutor's regime has placed sanctions on the president of the International Criminal Court, as an attempt to establish unilaterally an unheard-of supposed moral principle: absolute immunity for the officials of governments that reject the ICC.

We can see why US officials might want this — to protect US soldiers that commit war crimes. In theory, the US will prosecute them itself; if the US really does that, the ICC will not get involved in their cases, because its mission is to prosecute war criminals whose own country prosecutes them. In fact, the US often protects them and refuses to prosecute them.

Machines that kill without distinction [Richard Stallman's Political Notes]

*Machines that kill without distinction, answerable to no one, leave no one safe.*

Italian thugs killed Abderrahim Fakir [Richard Stallman's Political Notes]

Italian thugs killed authorized immigrant Abderrahim Fakir by holding him down while he was handcuffed.

The details of what killed him are not clear in this article.

Fascist politicians are delighted by the killing, presuming that Fakir deserved death (without waiting to see if there was any objective justification for killing him). That is a standard Fascist approach: use the fact of a crime against a hate-target as "proof" that the target somehow deserved it.

Scientific research on racism and dementia [Richard Stallman's Political Notes]

The forces of ignorance have canceled scientific research into how racism affects whether people develop dementia.

This must be one of the things that magats believe that "Man was not meant to know." Or, at least, they don't mean for this to be known.

Rejecting corporate Democrats [Richard Stallman's Political Notes]

Robert Reich: most Americans are rejecting the corporate Democrats, and the rich people that they mainly serve.

Corporate Democrats are far from the worst politicians in office in the US. The fascists (today's Republicans) are far worse; they are sabotaging democracy and rule of law. But the corporate Democrats will not try to give most people a decent life with something to strive for — a goal whose prerequisites include preventing climate disaster.

Heatwaves and wildfires adding to tipping point [Richard Stallman's Political Notes]

*Heatwaves and wildfires are not just scorching forests, they are adding to the tipping point risks in the world's vast permafrost regions, which contain three times more carbon than all the living vegetation on Earth.*

This could negate human efforts to reduce emissions. If so, it would mean that we left the task of reducing them till too late.

Policies most Americans sensibly demand [Richard Stallman's Political Notes]

Bernie Sanders: "Why are progressives winning across the United States?" He list the policies that most Americans sensibly demand, but corporate politicians label as "extremist".

Alas, he refers LLMs as "artificial intelligence". It is a mistake to call them that, because it is the marketing term of those who are pushing them on us.

Cities cancelled Flock and went with other surveillance [Richard Stallman's Political Notes]

Some cities terminated their contracts with Flock Surveillance, under public pressure, but turned around and brought in another surveillance company.

If you aim to limit access to the surveillance data enough to protect the public from possible Orwellian consequences, that's harder than one might assume. "Stricter" conditions of access may be too loose to achieve that protective goal. And don't forget that federal deportation thugs could override local rules of access.

My recommendation is to store the data in or near each camera, with no access from a distance. When there is a serous crime to investigate, it will be worth the cost to send someone to collect the pertinent data from each pertinent camera. But if someone just wants to fish for someone to persecute, this will be too much trouble.

For more explanation, look for "surveillance camera" in https://gnu.org/philosophy/surveillance-vs-democracy.html.

Friday, 04 September

22:49

Microsoft unveils a Windows variant for developers with 64GB of unified RAM [OSnews]

Zenith is a variant of Windows for “developer-class devices”.

Project Zenith comes with a set of pre-installed tools spanning languages and runtimes, source control, and productivity tools. Windows Terminal and Visual Studio Code are pinned to the Taskbar by default, putting your favorite tools within immediate reach.

We’ve also pre-configured Windows Settings for coding across File Explorer, Search, Start, and the Taskbar. File Explorer shows file extensions, hidden files, the full path in the title bar, and the details pane, with long-path support enabled. Recently used files and folders and sync provider tips are turned off for a cleaner workspace. In Search and Start, Command Palette is enabled, while Start menu tips and account notifications are turned off to reduce distractions.

Windows Subsystem for Linux (WSL) has become foundational for running Linux workloads on Windows. Last year we open-sourced WSL. Building on that momentum at Build 2026, we integrated WSL more deeply into Windows with WSL containers to provide a built-in way to create, run, and interact with Linux containers directly on Windows.

↫ Logan Iyer at the Windows Blogs

It’s highly unlikely you’ll be using this Zenith Windows flavour any time soon, as it requires 64GB of unified RAM with 250 GB/s memory bandwidth. In 2026, that’s a serious ask. On top of that, it’s not entirely clear to me if Zenith will be available as a separate Windows variant, without having to buy a complete device.

Of course, it would be trivial to set all of this up on any fresh Windows installation.

22:21

Friday Squid Blogging: Squid on a Stick at the New York State Fair [Schneier on Security]

Looks tasty.

As usual, you can also use this squid post to talk about the security stories in the news that I haven’t covered.

Blog moderation policy.

20:14

This Week in AI: The Frontier Is Getting Bigger [Radar]

Host Christina Stathopoulos, founder of Dare to Data and a former data scientist at Google and Waze, returned to This Week in AI with developments that stretched from Claude testing ways to improve model safety to Chinese open weight models gaining developer traffic and new systems learning to model physics. She also examined what Anthropic and OpenAI’s business moves, workforce forecasts, and debates over access reveal about how quickly the AI landscape is broadening.

Claude is taking on more of the research process

Anthropic provided an early example of AI helping improve future AI systems. In research Christina highlighted, Claude searched existing work, proposed methods, generated training data, and repeatedly tested and refined its approaches to reduce unwanted model behaviors. The experiments covered 10 such behaviors, including deception, hallucination, prompt injection, privacy violations, and reward hacking. Anthropic reported improvements across all 10 without degrading the model’s broader capabilities.

For deception, Claude tested more than 150 methods and eventually closed 85% of the measured safety gap. Human safety researchers closed only 20% in Anthropic’s comparison. Christina emphasized that this wasn’t a direct contest because Claude could run and refine experiments much faster and at a much greater scale. She also cautioned that the work didn’t amount to full recursive self-improvement.

This showed how AI could increasingly handle experimentation in model development, changing the pace and scale of research while humans still set the goals and evaluate the results.

Model performance is only one part of the frontier race

Competition among AI labs increasingly involves business performance, infrastructure, and deployment options alongside model quality. Anthropic estimates that the market for its systems could eventually reach $30 trillion, a long-term estimate that Christina treated skeptically because it approaches the size of the entire US economy. She also highlighted more concrete evidence of momentum in how Anthropic’s annualized revenue run rate rose from less than half of OpenAI’s at the start of the year to surpassing it within several months. Both companies are preparing for possible public offerings.

OpenAI faces a different set of pressures, and Christina highlighted its 14 executive departures this year. That sustained leadership turnover could raise questions about the company’s ability to execute consistently. OpenAI is also trying to gain more control over its infrastructure. Its Jalapeño inference chip, developed with Broadcom, delivered up to 1.9 times more AI work per watt and up to 3.6 times lower latency than comparable NVIDIA systems in OpenAI’s own testing.

Chinese open weight models are widening the field further. Christina cited an AI gateway where open weight models recently reached as much as 62% of developer traffic on a single day, compared with an average of roughly 10% in April. The competition now spans benchmark performance, capital, infrastructure, cost, deployment flexibility, and organizational execution.

Physics models could extend AI beyond language and images

Christina closed with research aimed at helping AI systems model physics. Researchers from MIT and Tsinghua University developed a pretraining approach that learned from more than one million synthetic interactions between moving particles and complex 3D objects, then applied those patterns to simulations involving wind, water, collisions, and light. The researchers described physics as a potential “third modality” for AI alongside language and pixels.

She also covered Accelerated Understanding, a startup that recently emerged from stealth with an architecture based on neural operators rather than transformers. The company is targeting problems involving enormous physical datasets, including chip design, robotics, extreme-weather forecasting, and geological exploration.

By learning directly from physical systems, these models could become valuable for simulation, engineering, robotics, forecasting, and other work that depends on understanding complex real-world environments.

What’s next

Christina also examined who could benefit from these advances. She discussed Bill Gates’s argument that access, deployment, policy, and distribution will shape AI’s social impact, and brought in new US Bureau of Labor Statistics projections showing job growth in areas including technical services and healthcare, while office and administrative roles face greater pressure from automation.

Her larger point was that access, workforce preparation, and public policy will determine how AI’s benefits and disruptions are distributed.

Due to the Labor Day holiday, This Week in AI will return on Monday, September 14, when we’ll dive into more of the news, issues, and key developments shaping the AI era. And check back each Friday for the latest episode, or watch on YouTube, Spotify, Apple, or wherever you get your podcasts.

18:07

Wonderella- versary [The Non-Adventures of Wonderella]

This week 20 years ago, I posted the first Wonderella comic. But Wonderella was actually my second online comic!

My first, Killroy & Tina, debuted *25 years ago* this week, and it introduced the Fulcrum. Wonderella's episodes took over most of my creative time, though, and the long-form K&T was soon put on the backburner.

But I’ve always loved Killroy & Tina enough to bring Killroy into the Wonderella universe years ago. All these years later, The Fulcrum's here too.

17:49

Using a VM to Contain an AI Agent [Schneier on Security]

It won’t work:

My suspicion was that GPT 5.6-Cyber would succeed, but the frequency and manner of its success removed all doubt. We have to reassess sandboxing quality for capable AI agents, and in general the software stack with which they interact.

An off-the-shelf VM is not enough to contain a modern, cyber-capable AI agent. There is simply too much attack surface. Even innocuous features (like running with a display) add extra, exploitable attack surface.

17:14

Why Podcasting 2.0 will never work [Scripting News]

If you want others to follow you, you have to offer them your users. And a format alone has no users.

And you have to trust your users to choose the best product, and you have to have a great product, that they appreciate, even though they have choice.

Just coming out with something better in a format will get you zero uptake. Unless you have users they can switch to their product, they simply won’t hear you.

That’s why Atom never replaced RSS, which already did everything people needed and was already supported by the NYT and the news industry.

That’s why AT Proto will never overcome the huge lead Twitter already has.

For more tips on what does and doesn't matter in open formats and protocols, check out my Rules for Standards-makers.

PS: This started as an early morning rant on Twitter.

The Big Idea: Betsy James [Whatever]

Every journey begins with a first step, but where that journey goes from there can take many forms. Betsy James took many journeys leading up to Practicing to be Lightning, but to play fair by the characters in her story, there were more journeys for her to take.

BETSY JAMES:

It started out simple. I wanted to exploit my hiking journal for a fantasy setting: thirty years’ walking the New Mexico desert wilderness, about six hundred hikes. Wonderful stuff: the deer skull shrine, the bear fall, the sliding rock, the grave of locked stones; the red stone altar-bowl full of blood, which was even weirder in real life because whoever (I assume) belonged to the blood had used one finger to paint a spiral on a nearby rock. So many of those places are gone now, changed, no longer accessible. I wanted to share them, and I wanted to walk through them again myself.

So I thought: If there are two young people, a sciency white ecologist—Ben—and a mixed-heritage artist—Trace—both of them violently destabilized by rage and mourning, what is the world their combined psyches might build from those adventures? They are climbing a mountain that is a cloud, trying desperately to reach a beloved grandmother—who happens to be dead—and are pursued by a being with a knife. They know who it is. If it catches them, it will kill them.

That was the idea. But fiction: You think you know where it’s headed, and it goes feral.

Right off I hit a roadblock. To write the American West without its indigenous peoples would be unethical; hence Trace, who has a Zuni mother. But I’m white. I led writers’ workshops in Zuni Pueblo for twenty years, so I know that no one can speak from those old cultures but those who grew up in them. We learn with our bodies: the all-night dances, the toddlers asleep on grandmas’ laps, the drum; the kachinas in their brilliant and utterly strange—to me—regalia.

I couldn’t pretend to speak for a Zuni. But Trace, the artist, needed her voice. 

I kept writing, but I worried myself crazy. Finally I said: Look. I’m not the same person I would’ve been if I hadn’t taught in Zuni. I’m changed, the way I’ve been changed by the wilderness—mostly on levels I’m not even conscious of. I’ll speak.

A non-white student gave me a rule of thumb: “Nothing about us without us.” So I found indigenous readers, and when my inner colonialist was pointed out, I winced, rethought (re-felt), and rewrote. This book is the best my historically-rooted self, in this moment, can do.

Then another feral idea turned out to be central. A family issue. Don’t we love those.

“My folks were delighted that I was into bat poop,” Ben says of himself, and of Trace,  “but she’s had to hide who she is.

I grew up among scientists and skeptics. A gift; and a curse, because I was left on their doorstep by metaphorists. To my skeptics, metaphor was highly suspect, because it “might be mistaken for reality.” Fantasy was childish, escapist pulp, the badlands of mad religions, conspiracy theorists, and unfulfilled housewives. It was marginally okay before the age of reason, but then you went to college. Where English departments felt the same.

To bad for me, huh? I wrote and wrote and wrote the stuff. But I hid it. When I went to college I tried to reform, but it was too late, and I ditched the English departments instead. I knew the deep value of fabulism—I lived it—but it still felt clandestine, as if I were making gin in the basement. Early learning is so ingrained; what did I just say about growing up with kachinas, hearing that drum? Child of scientists who did not dance, I wanted a scientific defense for what I wrote.

As I thrashed around in those thickets, I stumbled on a paper by neurolinguist and metaphorist Dr. George Lakoff. It pointed out that, ahem, language itself is metaphor. That how we speak in science and how we speak in fantasy are both metaphoric. What’s more—here’s where I sat up—both worldviews exploit the same neural pathways.

Thank you, Dr. Lakoff. I mean it. No wonder skeptics (and English departments) quarrel with fantasists over trespass. We’re all using the same network, like corporations forced to share one laptop. We jostle each other on the paths; and for a fantasy based on hiking, what a terrific image.

The book became a thought experiment, with Ben and Trace as two worldviews and the New Mexico wilderness—a version of it—as the retort. In the world of molecules, an angry young scientist can’t climb a Cumulonimbus congestus; the most desperately sad, abused, and suicidal girl can’t get back to her dear grandmother by drawing a map and walking into it. In the world of metaphor, that they can do so is not only possible but appropriate. This is neither childish nor escapist.

The terrain of this book revealed itself to be traveled by the messy psyches of very different cultures, white and indigenous—which is to say the American West—and criscrossed by science and story, by both molecules and metaphor. Which is also to say the American West.

So: The impulses that share our neural pathways are electrical. Sometimes they cross; sometimes they fuse; the worlds they make are our world, but newly seen. “They’re all real, the different worlds,” says Trace. “They just give you different data.”

What I love about writing: You’re working away, making gin in your basement, and suddenly you’re given some gorgeous metaphor like, well, a bolt of lightning. On the edge of my mind had been something I was told in Zuni: that the kachinas themselves, who are spirits, are dancing in the spaces between the human dancers. Like a charge that jumps the space. We run on electricity; “reality” is what we bring into being in the spaces between us.

We’re practicing to be lightning. And that’s a metaphor.


Practicing to be Lightning: Amazon|Barnes & Noble|Bookshop

Author Socials: Web site

Read an excerpt.

View From A Hotel Window, 9/4/2026: Atlanta [Whatever]

Hello, dear readers! I find myself at DragonCon, which means I am in Atlanta! I snagged a room upgrade while checking in and now have a view from the 37th floor, which is not half bad.

Have an excellent weekend, both con goers and those of you elsewhere!

-AMS

Feeds

FeedRSSLast fetchedNext fetched after
@ASmartBear XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
a bag of four grapes XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Ansible XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Bad Science XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Black Doggerel XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Blog - Official site of Stephen Fry XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Charlie Brooker | The Guardian XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Charlie's Diary XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Chasing the Sunset - Comics Only XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Coding Horror XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
Comics Archive - Spinnyverse XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
Cory Doctorow's craphound.com XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Cory Doctorow, Author at Boing Boing XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Ctrl+Alt+Del Comic XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Cyberunions XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
David Mitchell | The Guardian XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Deeplinks XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
Diesel Sweeties webcomic by rstevens XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Dilbert XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Dork Tower XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Economics from the Top Down XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Edmund Finney's Quest to Find the Meaning of Life XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
EFF Action Center XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Enspiral Tales - Medium XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Events XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Falkvinge on Liberty XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Flipside XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Flipside XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Free software jobs XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Full Frontal Nerdity by Aaron Williams XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
General Protection Fault: Comic Updates XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
George Monbiot XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Girl Genius XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Groklaw XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Grrl Power XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Hackney Anarchist Group XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Hackney Solidarity Network XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
http://blog.llvm.org/feeds/posts/default XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
http://calendar.google.com/calendar/feeds/q7s5o02sj8hcam52hutbcofoo4%40group.calendar.google.com/public/basic XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
http://dynamic.boingboing.net/cgi-bin/mt/mt-cp.cgi?__mode=feed&_type=posts&blog_id=1&id=1 XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
http://eng.anarchoblogs.org/feed/atom/ XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
http://feed43.com/3874015735218037.xml XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
http://flatearthnews.net/flatearthnews.net/blogfeed XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
http://fulltextrssfeed.com/ XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
http://london.indymedia.org/articles.rss XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
http://pipes.yahoo.com/pipes/pipe.run?_id=ad0530218c055aa302f7e0e84d5d6515&amp;_render=rss XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
http://planet.gridpp.ac.uk/atom.xml XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
http://shirky.com/weblog/feed/atom/ XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
http://thecommune.co.uk/feed/ XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
http://theness.com/roguesgallery/feed/ XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
http://www.airshipentertainment.com/buck/buckcomic/buck.rss XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
http://www.airshipentertainment.com/growf/growfcomic/growf.rss XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
http://www.airshipentertainment.com/myth/mythcomic/myth.rss XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
http://www.feedsapi.com/makefulltextfeed.php?url=http%3A%2F%2Fwww.somethingpositive.net%2Fsp.xml&what=auto&key=&max=7&links=preserve&exc=&privacy=I+accept XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
http://www.godhatesastronauts.com/feed/ XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
http://www.tinycat.co.uk/feed/ XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
https://anarchism.pageabode.com/blogs/anarcho/feed/ XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
https://broodhollow.krisstraub.comfeed/ XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
https://debian-administration.org/atom.xml XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
https://elitetheatre.org/ XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
https://feeds.feedburner.com/Starslip XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
https://feeds2.feedburner.com/GeekEtiquette?format=xml XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
https://hackbloc.org/rss.xml XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
https://kajafoglio.livejournal.com/data/atom/ XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
https://philfoglio.livejournal.com/data/atom/ XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
https://pixietrixcomix.com/eerie-cutiescomic.rss XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
https://pixietrixcomix.com/menage-a-3/comic.rss XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
https://propertyistheft.wordpress.com/feed/ XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
https://requiem.seraph-inn.com/updates.rss XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
https://studiofoglio.livejournal.com/data/atom/ XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
https://thecommandline.net/feed/ XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
https://torrentfreak.com/subscriptions/ XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
https://web.randi.org/?format=feed&type=rss XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
https://www.baen.com/baenebooks XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
https://www.dcscience.net/feed/medium.co XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
https://www.DropCatch.com/domain/steampunkmagazine.com XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
https://www.DropCatch.com/domain/ubuntuweblogs.org XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
https://www.DropCatch.com/redirect/?domain=DyingAlone.net XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
https://www.freedompress.org.uk:443/news/feed/ XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
https://www.goblinscomic.com/category/comics/feed/ XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
https://www.loomio.com/blog/feed/ XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
https://www.newstatesman.com/feeds/blogs/laurie-penny.rss XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
https://www.patreon.com/graveyardgreg/posts/comic.rss XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
https://www.rightmove.co.uk/rss/property-for-sale/find.html?locationIdentifier=REGION^876&maxPrice=240000&minBedrooms=2&displayPropertyType=houses&oldDisplayPropertyType=houses&primaryDisplayPropertyType=houses&oldPrimaryDisplayPropertyType=houses&numberOfPropertiesPerPage=24 XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
https://x.com/statuses/user_timeline/22724360.rss XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Humble Bundle Blog XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
I, Cringely XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Irregular Webcomic! XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Joel on Software XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
Judith Proctor's Journal XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Krebs on Security XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Lambda the Ultimate - Programming Languages Weblog XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Looking For Group XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
LWN.net XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Mimi and Eunice XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Neil Gaiman's Journal XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
Nina Paley XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
O Abnormal – Scifi/Fantasy Artist XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Oglaf! -- Comics. Often dirty. XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Oh Joy Sex Toy XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
Order of the Stick XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
Original Fiction Archives - Reactor XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
OSnews XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Paul Graham: Unofficial RSS Feed XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Penny Arcade XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Penny Red XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
PHD Comics XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Phil's blog XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
Planet Debian XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Planet GNU XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Planet Lisp XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Pluralistic: Daily links from Cory Doctorow XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
PS238 by Aaron Williams XML 09:28, Thursday, 10 September 10:16, Thursday, 10 September
QC RSS v2 XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
Radar XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
RevK®'s ramblings XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
Richard Stallman's Political Notes XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Scenes From A Multiverse XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
Schneier on Security XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
SCHNEWS.ORG.UK XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
Scripting News XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Seth's Blog XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
Skin Horse XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Tales From the Riverbank XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
The Adventures of Dr. McNinja XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
The Bumpycat sat on the mat XML 09:35, Thursday, 10 September 10:15, Thursday, 10 September
The Daily WTF XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
The Monochrome Mob XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
The Non-Adventures of Wonderella XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
The Old New Thing XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
The Open Source Grid Engine Blog XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
The Stranger XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
towerhamletsalarm XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
Twokinds XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
UK Indymedia Features XML 10:14, Thursday, 10 September 10:56, Thursday, 10 September
Uploads from ne11y XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
Uploads from piasladic XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September
Use Sword on Monster XML 09:28, Thursday, 10 September 10:15, Thursday, 10 September
Wayward Sons: Legends - Sci-Fi Full Page Webcomic - Updates Daily XML 10:14, Thursday, 10 September 11:00, Thursday, 10 September
what if? XML 09:35, Thursday, 10 September 10:16, Thursday, 10 September
Whatever XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
Whitechapel Anarchist Group XML 09:35, Thursday, 10 September 10:24, Thursday, 10 September
WIL WHEATON dot NET XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
wish XML 09:49, Thursday, 10 September 10:34, Thursday, 10 September
Writing the Bright Fantastic XML 09:49, Thursday, 10 September 10:33, Thursday, 10 September
xkcd.com XML 09:49, Thursday, 10 September 10:32, Thursday, 10 September